Skip to content

Commit 285530a

Browse files
committed
feat: add lambda expressions and import modifier syntax to ZynML
Lambda expressions: - Syntax: def(x): x * 2, def(x, y): x + y, def(): 42 - Added TypedLambdaParam handling in interpreter - Full integration with TypedLambda AST Import modifier expressions: - Syntax: import asset("image.jpg") as Image - Supports domain-specific loaders (asset, image, audio, model, font) - TypedImportModifier AST node with loader, path, target_type Other changes: - Replaced outdated examples with accurate ZynML syntax examples - Updated stdlib prelude and tensor modules - Added 7 new parser tests for lambda and import expressions - 209 tests passing
1 parent 63c343b commit 285530a

24 files changed

Lines changed: 1572 additions & 678 deletions

.vscode/extensions/zynml-syntax/CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
# Changelog
22

3+
## [0.2.0] - 2026-01-17
4+
5+
### Changed
6+
7+
- Removed `pub` keyword (all items are public by default)
8+
- Added Python-style private item convention (`_` prefix)
9+
- Private functions, methods, and variables now have distinct scopes:
10+
- `entity.name.function.private.zynml` for private function definitions
11+
- `entity.name.function.call.private.zynml` for private function calls
12+
- `entity.name.function.method.private.zynml` for private method calls
13+
- `variable.other.private.zynml` for private variables
14+
- `variable.parameter.private.zynml` for private parameters
15+
316
## [0.1.0] - 2026-01-17
417

518
### Added

.vscode/extensions/zynml-syntax/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "zynml-syntax",
33
"displayName": "ZynML Language Support",
44
"description": "Syntax highlighting for ZynML - Machine Learning DSL for Zyntax",
5-
"version": "0.1.0",
5+
"version": "0.2.0",
66
"publisher": "zyntax",
77
"engines": {
88
"vscode": "^1.75.0"

.vscode/extensions/zynml-syntax/syntaxes/zynml.tmLanguage.json

Lines changed: 46 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@
185185
},
186186
{
187187
"name": "storage.modifier.zynml",
188-
"match": "\\b(mut|pub|const|extern|opaque)\\b"
188+
"match": "\\b(mut|const|extern|opaque)\\b"
189189
},
190190
{
191191
"name": "keyword.declaration.zynml",
@@ -308,23 +308,51 @@
308308
"functions": {
309309
"patterns": [
310310
{
311+
"comment": "Private function definition (starts with _)",
312+
"name": "meta.function.definition.private.zynml",
313+
"match": "\\b(def|fn)\\s+(_[a-zA-Z0-9_]*)",
314+
"captures": {
315+
"1": { "name": "storage.type.function.zynml" },
316+
"2": { "name": "entity.name.function.private.zynml" }
317+
}
318+
},
319+
{
320+
"comment": "Public function definition",
311321
"name": "meta.function.definition.zynml",
312-
"match": "\\b(def|fn)\\s+([a-zA-Z_][a-zA-Z0-9_]*)",
322+
"match": "\\b(def|fn)\\s+([a-zA-Z][a-zA-Z0-9_]*)",
313323
"captures": {
314324
"1": { "name": "storage.type.function.zynml" },
315325
"2": { "name": "entity.name.function.zynml" }
316326
}
317327
},
318328
{
329+
"comment": "Private function call (starts with _)",
330+
"name": "meta.function.call.private.zynml",
331+
"match": "\\b(_[a-zA-Z0-9_]*)\\s*(?=\\()",
332+
"captures": {
333+
"1": { "name": "entity.name.function.call.private.zynml" }
334+
}
335+
},
336+
{
337+
"comment": "Public function call",
319338
"name": "meta.function.call.zynml",
320-
"match": "\\b([a-z_][a-zA-Z0-9_]*)\\s*(?=\\()",
339+
"match": "\\b([a-z][a-zA-Z0-9_]*)\\s*(?=\\()",
321340
"captures": {
322341
"1": { "name": "entity.name.function.call.zynml" }
323342
}
324343
},
325344
{
345+
"comment": "Private method call (starts with _)",
346+
"name": "meta.method.call.private.zynml",
347+
"match": "\\.(_[a-zA-Z0-9_]*)\\s*(?=\\()",
348+
"captures": {
349+
"1": { "name": "entity.name.function.method.private.zynml" }
350+
}
351+
},
352+
{
353+
"comment": "Public method call",
326354
"name": "meta.method.call.zynml",
327-
"match": "\\.([a-z_][a-zA-Z0-9_]*)\\s*(?=\\()",
355+
"match": "\\.([a-z][a-zA-Z0-9_]*)\\s*(?=\\()",
328356
"captures": {
329357
"1": { "name": "entity.name.function.method.zynml" }
330358
}
@@ -338,12 +366,24 @@
338366
"variables": {
339367
"patterns": [
340368
{
369+
"comment": "Private parameter (starts with _)",
370+
"name": "variable.parameter.private.zynml",
371+
"match": "\\b(_[a-zA-Z0-9_]*)\\s*(?=:)"
372+
},
373+
{
374+
"comment": "Public parameter",
341375
"name": "variable.parameter.zynml",
342-
"match": "\\b([a-z_][a-zA-Z0-9_]*)\\s*(?=:)"
376+
"match": "\\b([a-z][a-zA-Z0-9_]*)\\s*(?=:)"
377+
},
378+
{
379+
"comment": "Private variable (starts with _)",
380+
"name": "variable.other.private.zynml",
381+
"match": "\\b_[a-zA-Z0-9_]*\\b"
343382
},
344383
{
384+
"comment": "Public variable",
345385
"name": "variable.other.zynml",
346-
"match": "\\b[a-z_][a-zA-Z0-9_]*\\b"
386+
"match": "\\b[a-z][a-zA-Z0-9_]*\\b"
347387
}
348388
]
349389
}

crates/typed_ast/src/lib.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,13 +123,13 @@ pub use typed_ast::{
123123
TypedBinary, TypedUnary, TypedCall, TypedFieldAccess, TypedIndex,
124124
TypedFor, TypedMatch, TypedMatchExpr, TypedMatchArm, ParameterKind,
125125
// Lambda types
126-
TypedLambda, TypedLambdaBody,
126+
TypedLambda, TypedLambdaBody, TypedLambdaParam,
127127
// Method types
128128
TypedMethodCall, TypedMethod, TypedMethodParam,
129129
// Range type
130130
TypedRange,
131-
// List comprehension and slice types
132-
TypedListComprehension, TypedSlice,
131+
// List comprehension, slice, and import modifier types
132+
TypedListComprehension, TypedSlice, TypedImportModifier,
133133
// Struct type
134134
TypedStructLiteral, TypedFieldInit,
135135
// Pattern types

crates/typed_ast/src/type_checker.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1209,6 +1209,10 @@ impl TypeChecker {
12091209
Ok(self.inference.fresh_type_var())
12101210
}
12111211
TypedExpression::MethodCall(method_call) => self.check_method_call(method_call),
1212+
TypedExpression::ImportModifier(import) => {
1213+
// The type is resolved to the target_type (e.g., Image, AudioBuffer)
1214+
Ok(Type::Unresolved(import.target_type))
1215+
}
12121216
}
12131217
}
12141218

crates/typed_ast/src/typed_ast.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -522,6 +522,8 @@ pub enum TypedExpression {
522522
ListComprehension(TypedListComprehension),
523523
/// Slice expression: arr[start:end:step]
524524
Slice(TypedSlice),
525+
/// Import modifier expression: import loader("path") as Type
526+
ImportModifier(TypedImportModifier),
525527
}
526528

527529
impl Default for TypedExpression {
@@ -915,6 +917,24 @@ pub struct TypedSlice {
915917
pub step: Option<Box<TypedNode<TypedExpression>>>,
916918
}
917919

920+
/// Import modifier expression: import loader("path") as Type
921+
///
922+
/// Loads an asset file using a specified loader function and returns it
923+
/// as an opaque type. The loader (e.g., `asset`, `image`, `audio`, `model`)
924+
/// determines how the file is loaded, and the target type specifies the
925+
/// expected return type.
926+
///
927+
/// Example: `import asset("image.jpg") as Image`
928+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
929+
pub struct TypedImportModifier {
930+
/// The loader function name (e.g., "asset", "image", "audio")
931+
pub loader: InternedString,
932+
/// Path to the asset file
933+
pub path: InternedString,
934+
/// Expected return type name
935+
pub target_type: InternedString,
936+
}
937+
918938
/// Method call with enhanced argument support
919939
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
920940
pub struct TypedMethodCall {

crates/zyn_peg/src/runtime2/interpreter.rs

Lines changed: 157 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ use zyntax_typed_ast::{
1919
TypedIf, TypedWhile, TypedFor, TypedUnary, TypedFieldAccess, TypedIndex,
2020
TypedRange, TypedStructLiteral, TypedFieldInit, TypedPattern,
2121
TypedParameter, TypedVariant, TypedVariantFields, TypedTypeAlias, ParameterKind,
22-
TypedInterface,
22+
TypedInterface, TypedExtern, TypedExternStruct, TypedTypeParam,
2323
TypedAnnotation, TypedAnnotationArg, TypedAnnotationValue,
24+
TypedLambda, TypedLambdaBody, TypedLambdaParam, TypedImportModifier,
2425
UnaryOp,
2526
typed_node, Span,
2627
type_registry::{Type, PrimitiveType, Mutability, Visibility, CallingConvention, NullabilityKind, ConstValue},
@@ -277,6 +278,10 @@ impl<'g> GrammarInterpreter<'g> {
277278
["SuffixField"] | ["SuffixMethod"] | ["SuffixCall"] | ["SuffixIndex"] | ["SuffixSlice"] => {
278279
self.construct_suffix(type_path, fields, state)
279280
}
281+
// Lambda parameter
282+
["TypedLambdaParam"] => {
283+
self.construct_lambda_param(fields, state, span)
284+
}
280285
_ => Err(format!("unknown type path: {}", type_path)),
281286
}
282287
}
@@ -609,6 +614,29 @@ impl<'g> GrammarInterpreter<'g> {
609614
step: step.map(Box::new),
610615
})
611616
}
617+
"Lambda" => {
618+
// Lambda expression: def(x): x * 2
619+
let params = self.get_field_as_lambda_param_list("params", fields, state)?;
620+
let body = self.get_field_as_expr("body", fields, state)?;
621+
622+
TypedExpression::Lambda(TypedLambda {
623+
params,
624+
body: TypedLambdaBody::Expression(Box::new(body)),
625+
captures: vec![],
626+
})
627+
}
628+
"ImportModifier" => {
629+
// Import modifier expression: import asset("image.jpg") as Image
630+
let loader = self.get_field_as_interned("loader", fields, state)?;
631+
let path = self.get_field_as_interned("path", fields, state)?;
632+
let target_type = self.get_field_as_interned("target_type", fields, state)?;
633+
634+
TypedExpression::ImportModifier(TypedImportModifier {
635+
loader,
636+
path,
637+
target_type,
638+
})
639+
}
612640
_ => return Err(format!("unknown TypedExpression variant: {}", variant)),
613641
};
614642

@@ -864,6 +892,16 @@ impl<'g> GrammarInterpreter<'g> {
864892
span,
865893
})
866894
}
895+
"ExternStruct" => {
896+
let name = self.get_field_as_interned("name", fields, state)?;
897+
let type_params = self.get_field_as_type_param_list("type_params", fields, state)?;
898+
899+
TypedDeclaration::Extern(TypedExtern::Struct(TypedExternStruct {
900+
name,
901+
runtime_prefix: name, // Use name as default runtime prefix
902+
type_params,
903+
}))
904+
}
867905
_ => return Err(format!("unknown TypedDeclaration variant: {}", variant)),
868906
};
869907

@@ -1288,6 +1326,22 @@ impl<'g> GrammarInterpreter<'g> {
12881326
}))
12891327
}
12901328

1329+
/// Construct a TypedLambdaParam (lambda parameter)
1330+
fn construct_lambda_param<'a>(
1331+
&self,
1332+
fields: &[(String, ExprIR)],
1333+
state: &mut ParserState<'a>,
1334+
_span: Span,
1335+
) -> Result<ParsedValue, String> {
1336+
let name = self.get_field_as_interned("name", fields, state)?;
1337+
let ty = self.get_field_optional("ty", fields, state)?;
1338+
1339+
Ok(ParsedValue::LambdaParam(TypedLambdaParam {
1340+
name,
1341+
ty,
1342+
}))
1343+
}
1344+
12911345
/// Get a field value as a Type
12921346
fn get_field_as_type<'a>(
12931347
&self,
@@ -2873,6 +2927,108 @@ impl<'g> GrammarInterpreter<'g> {
28732927
}
28742928
}
28752929

2930+
/// Get a field as a list of TypedTypeParam
2931+
fn get_field_as_type_param_list<'a>(
2932+
&self,
2933+
name: &str,
2934+
fields: &[(String, ExprIR)],
2935+
state: &mut ParserState<'a>,
2936+
) -> Result<Vec<TypedTypeParam>, String> {
2937+
match self.get_field(name, fields) {
2938+
Some(expr) => {
2939+
let val = self.eval_expr(expr, state)?;
2940+
match val {
2941+
ParsedValue::List(items) => {
2942+
let mut result = Vec::new();
2943+
for item in items {
2944+
let interned = self.parsed_value_to_interned(item, state)?;
2945+
result.push(TypedTypeParam {
2946+
name: interned,
2947+
bounds: vec![],
2948+
default: None,
2949+
span: Span::default(),
2950+
});
2951+
}
2952+
Ok(result)
2953+
}
2954+
ParsedValue::None => Ok(vec![]),
2955+
ParsedValue::Optional(None) => Ok(vec![]),
2956+
ParsedValue::Interned(i) => Ok(vec![TypedTypeParam {
2957+
name: i,
2958+
bounds: vec![],
2959+
default: None,
2960+
span: Span::default(),
2961+
}]),
2962+
ParsedValue::Text(s) => Ok(vec![TypedTypeParam {
2963+
name: state.intern(&s),
2964+
bounds: vec![],
2965+
default: None,
2966+
span: Span::default(),
2967+
}]),
2968+
_ => Err(format!("field '{}' is not a type param list", name)),
2969+
}
2970+
}
2971+
None => Ok(vec![]),
2972+
}
2973+
}
2974+
2975+
/// Get a field as a list of lambda parameters
2976+
fn get_field_as_lambda_param_list<'a>(
2977+
&self,
2978+
name: &str,
2979+
fields: &[(String, ExprIR)],
2980+
state: &mut ParserState<'a>,
2981+
) -> Result<Vec<TypedLambdaParam>, String> {
2982+
match self.get_field(name, fields) {
2983+
Some(expr) => {
2984+
let val = self.eval_expr(expr, state)?;
2985+
match val {
2986+
ParsedValue::List(items) => {
2987+
let mut result = Vec::new();
2988+
for item in items {
2989+
let param = self.parsed_value_to_lambda_param(item, state)?;
2990+
result.push(param);
2991+
}
2992+
Ok(result)
2993+
}
2994+
ParsedValue::None => Ok(vec![]),
2995+
ParsedValue::Optional(None) => Ok(vec![]),
2996+
ParsedValue::LambdaParam(p) => Ok(vec![p]),
2997+
ParsedValue::Interned(i) => Ok(vec![TypedLambdaParam {
2998+
name: i,
2999+
ty: None,
3000+
}]),
3001+
ParsedValue::Text(s) => Ok(vec![TypedLambdaParam {
3002+
name: state.intern(&s),
3003+
ty: None,
3004+
}]),
3005+
_ => Err(format!("field '{}' is not a lambda param list: {:?}", name, val)),
3006+
}
3007+
}
3008+
None => Ok(vec![]),
3009+
}
3010+
}
3011+
3012+
/// Convert ParsedValue to TypedLambdaParam
3013+
fn parsed_value_to_lambda_param<'a>(
3014+
&self,
3015+
val: ParsedValue,
3016+
state: &mut ParserState<'a>,
3017+
) -> Result<TypedLambdaParam, String> {
3018+
match val {
3019+
ParsedValue::LambdaParam(p) => Ok(p),
3020+
ParsedValue::Interned(i) => Ok(TypedLambdaParam {
3021+
name: i,
3022+
ty: None,
3023+
}),
3024+
ParsedValue::Text(s) => Ok(TypedLambdaParam {
3025+
name: state.intern(&s),
3026+
ty: None,
3027+
}),
3028+
_ => Err(format!("cannot convert value to lambda param: {:?}", val)),
3029+
}
3030+
}
3031+
28763032
/// Convert ParsedValue to TypedEffectOp
28773033
fn parsed_value_to_effect_op(&self, val: ParsedValue) -> Result<zyntax_typed_ast::TypedEffectOp, String> {
28783034
match val {

crates/zyn_peg/src/runtime2/state.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,8 @@ pub enum ParsedValue {
165165
EffectOp(zyntax_typed_ast::TypedEffectOp),
166166
/// A TypedEffectHandlerImpl (handler operation implementation)
167167
EffectHandlerImpl(zyntax_typed_ast::TypedEffectHandlerImpl),
168+
/// A lambda parameter
169+
LambdaParam(zyntax_typed_ast::TypedLambdaParam),
168170
}
169171

170172
/// Handle to an AST node (opaque, managed by builder)

0 commit comments

Comments
 (0)