diff --git a/mlf-codegen/src/lib.rs b/mlf-codegen/src/lib.rs index a3830e0..d83da55 100644 --- a/mlf-codegen/src/lib.rs +++ b/mlf-codegen/src/lib.rs @@ -423,15 +423,20 @@ fn generate_type_json(ty: &Type, usage_counts: &HashMap, workspac "items": generate_type_json(inner, usage_counts, workspace, current_namespace) }) } - Type::Union { types, .. } => { + Type::Union { types, closed, .. } => { let refs: Vec = types .iter() .map(|t| generate_type_json(t, usage_counts, workspace, current_namespace)) .collect(); - json!({ - "type": "union", - "refs": refs - }) + let mut union_obj = Map::new(); + union_obj.insert("type".to_string(), json!("union")); + union_obj.insert("refs".to_string(), json!(refs)); + // Only emit "closed" field if true (closed unions) + // Open unions omit the field (defaults to false per ATProto spec) + if *closed { + union_obj.insert("closed".to_string(), json!(true)); + } + Value::Object(union_obj) } Type::Object { fields, .. } => { let mut required = Vec::new(); diff --git a/mlf-lang/src/ast.rs b/mlf-lang/src/ast.rs index 4825560..8c7d53f 100644 --- a/mlf-lang/src/ast.rs +++ b/mlf-lang/src/ast.rs @@ -249,7 +249,7 @@ pub enum Type { /// Array type Array { inner: Box, span: Span }, /// Union type - Union { types: Vec, span: Span }, + Union { types: Vec, closed: bool, span: Span }, /// Object type (inline) Object { fields: Vec, span: Span }, /// Parenthesized type (for grouping, e.g., (A | B)[]) diff --git a/mlf-lang/src/lexer.rs b/mlf-lang/src/lexer.rs index d66ba4e..7a5b2c9 100644 --- a/mlf-lang/src/lexer.rs +++ b/mlf-lang/src/lexer.rs @@ -283,8 +283,8 @@ fn single_token(input: &str) -> IResult<&str, Option> { map(string_literal, Some), map(raw_identifier, Some), map(type_ident, Some), + map(symbol, Some), // Parse symbols before identifiers so _ is caught map(identifier, Some), - map(symbol, Some), )).parse(input) } diff --git a/mlf-lang/src/parser.rs b/mlf-lang/src/parser.rs index 4d8f0d1..3978911 100644 --- a/mlf-lang/src/parser.rs +++ b/mlf-lang/src/parser.rs @@ -411,7 +411,8 @@ impl Parser { } let span = Span::new(types[0].span().start, types.last().unwrap().span().end); - ReturnType::Type(Type::Union { types, span }) + // Return type unions are closed by default (no _ support in return types yet) + ReturnType::Type(Type::Union { types, closed: true, span }) } } else { ReturnType::Type(output) @@ -462,7 +463,8 @@ impl Parser { } let span = Span::new(types[0].span().start, types.last().unwrap().span().end); - ReturnType::Type(Type::Union { types, span }) + // Return type unions are closed by default (no _ support in return types yet) + ReturnType::Type(Type::Union { types, closed: true, span }) } } else { ReturnType::Type(output) @@ -623,16 +625,25 @@ impl Parser { if matches!(self.current().token, LexToken::Pipe) { let mut types = alloc::vec![base]; + let mut has_underscore = false; while matches!(self.current().token, LexToken::Pipe) { self.advance(); if matches!(self.current().token, LexToken::Error) { break; } - types.push(self.parse_base_type()?); + // Check if this is an underscore (open union marker) + if matches!(self.current().token, LexToken::Underscore) { + has_underscore = true; + self.advance(); + } else { + types.push(self.parse_base_type()?); + } } let span = Span::new(types[0].span().start, types.last().unwrap().span().end); - return Ok(Type::Union { types, span }); + // Unions are closed by default, open if _ is present + let closed = !has_underscore; + return Ok(Type::Union { types, closed, span }); } Ok(base) diff --git a/mlf-lang/src/workspace.rs b/mlf-lang/src/workspace.rs index 9d67016..a042318 100644 --- a/mlf-lang/src/workspace.rs +++ b/mlf-lang/src/workspace.rs @@ -251,7 +251,7 @@ impl Workspace { Type::Primitive { .. } | Type::Unknown { .. } => Ok(()), Type::Reference { .. } => Ok(()), Type::Array { inner, .. } => self.typecheck_type(namespace, inner), - Type::Union { types, span } => { + Type::Union { types, span, .. } => { let mut errors = ValidationErrors::new(); for ty in types {