diff --git a/src/fmt/layout.rs b/src/fmt/layout.rs index c0c6db01..39247e9f 100644 --- a/src/fmt/layout.rs +++ b/src/fmt/layout.rs @@ -469,6 +469,9 @@ impl<'a, 'src> Ctx<'a, 'src> { NodeKind::Block { name, params, sep, body } => { self.block_node(name, params, sep, &body, at) } + NodeKind::Type { .. } | NodeKind::Enum { .. } | NodeKind::Union { .. } => { + todo!("type-decl layout") + } } } diff --git a/src/fmt/print.rs b/src/fmt/print.rs index 215a9a31..b4050375 100644 --- a/src/fmt/print.rs +++ b/src/fmt/print.rs @@ -336,6 +336,26 @@ impl<'a, 'src> Printer<'a, 'src> { self.node(inner); } + // --- type declarations --- + NodeKind::Type { params, sep, body } => { + self.keyword(node_loc, "type"); + self.node(params); + self.tok(&sep); + self.exprs(&body); + } + NodeKind::Enum { params, sep, body } => { + self.keyword(node_loc, "enum"); + self.node(params); + self.tok(&sep); + self.exprs(&body); + } + NodeKind::Union { params, sep, body } => { + self.keyword(node_loc, "union"); + self.node(params); + self.tok(&sep); + self.exprs(&body); + } + // --- custom blocks --- NodeKind::Block { name, params, sep, body } => { self.node(name); diff --git a/src/passes/ast/fmt.rs b/src/passes/ast/fmt.rs index e2d4023f..dbb5c9fb 100644 --- a/src/passes/ast/fmt.rs +++ b/src/passes/ast/fmt.rs @@ -428,6 +428,9 @@ fn fmt_node(ast: &Ast<'_>, id: AstId, out: &mut MappedWriter, depth: usize) { out.push('\''); } } + NodeKind::Type { params, sep, body } => fmt_type_decl(ast, "type", params, &sep, &body.items, out, depth), + NodeKind::Enum { params, sep, body } => fmt_type_decl(ast, "enum", params, &sep, &body.items, out, depth), + NodeKind::Union { params, sep, body } => fmt_type_decl(ast, "union", params, &sep, &body.items, out, depth), NodeKind::Block { name, params, sep, body } => { fmt_node(ast, name, out, depth); out.push(' '); @@ -572,6 +575,28 @@ fn fmt_fn(ast: &Ast<'_>, params: AstId, sep: &Token, body: &[AstId], out: &mut M fmt_fn_with_inline(ast, params, sep, body, out, depth, true); } +// Format a `type` / `enum` / `union` declaration: keyword, optional generic +// params, then `:` body (or the unit `_` form). +fn fmt_type_decl(ast: &Ast<'_>, kw: &str, params: AstId, sep: &Token, body: &[AstId], out: &mut MappedWriter, depth: usize) { + out.push_str(kw); + // Unit form `type _`: keyword then the `_` sep, no body. + if sep.src == "_" { + out.push(' '); + out.mark(sep.loc); + out.push('_'); + return; + } + // Generic params (Patterns), only when non-empty. + let has_params = matches!(&ast.nodes.get(params).kind, NodeKind::Patterns(e) if !e.items.is_empty()); + if has_params { + out.push(' '); + fmt_node(ast, params, out, depth); + } + out.mark(sep.loc); + out.push(':'); + fmt_body(ast, body, out, depth, true); +} + fn fmt_fn_with_inline(ast: &Ast<'_>, params: AstId, sep: &Token, body: &[AstId], out: &mut MappedWriter, depth: usize, allow_apply_inline: bool) { let inline = body.len() == 1 && if allow_apply_inline { is_inline_expr(ast, body[0]) diff --git a/src/passes/ast/mod.rs b/src/passes/ast/mod.rs index 0a05e2d3..dead73f9 100644 --- a/src/passes/ast/mod.rs +++ b/src/passes/ast/mod.rs @@ -388,6 +388,23 @@ pub enum NodeKind<'src> { // Try — unwrap Ok or propagate Err from enclosing function Try(AstId), + // --- type declarations --- + + // Type — product type (record/tuple). params (Patterns, generic params; empty + // if none) + sep (':' for a block, '_' for the unit form `type _`) + body + // (fields: Arm ':' for named/record, bare exprs for positional/tuple; empty + // for unit). + Type { params: AstId, sep: Token<'src>, body: Exprs<'src> }, + + // Enum — closed sum that mints constructors. params (Patterns generic params) + // + sep (':') + body (constructor members: bare Ident for nullary, Apply for + // payload-carrying). + Enum { params: AstId, sep: Token<'src>, body: Exprs<'src> }, + + // Union — open union of existing types. params (Patterns generic params) + + // sep (':') + body (member type expressions). + Union { params: AstId, sep: Token<'src>, body: Exprs<'src> }, + // --- custom blocks --- // Block — name (Ident) + params (Patterns) + sep (:) + body @@ -479,6 +496,12 @@ pub fn walk<'src, 'a>( walk(ast, *lhs, f); for &stmt_id in body.items.iter() { walk(ast, stmt_id, f); } } + NodeKind::Type { params, body, .. } + | NodeKind::Enum { params, body, .. } + | NodeKind::Union { params, body, .. } => { + walk(ast, *params, f); + for &stmt_id in body.items.iter() { walk(ast, stmt_id, f); } + } NodeKind::Block { name, params, body, .. } => { walk(ast, *name, f); walk(ast, *params, f); @@ -687,6 +710,28 @@ fn print_node(ast: &Ast, id: AstId, out: &mut crate::sourcemap::MappedWriter, de print_node(ast, stmt_id, out, depth + 1); } } + NodeKind::Type { params, sep, body } + | NodeKind::Enum { params, sep, body } + | NodeKind::Union { params, sep, body } => { + let label = match node.kind { + NodeKind::Type { .. } => "Type", + NodeKind::Enum { .. } => "Enum", + _ => "Union", + }; + out.push_str(label); + out.push_str(" '"); out.push_str(sep.src); out.push('\''); + // Unit form `type _` has empty params + body and prints as just `Type '_'`. + if sep.src == "_" && body.items.is_empty() { + return; + } + out.push(','); + out.push('\n'); + print_node(ast, *params, out, depth + 1); + for &stmt_id in body.items.iter() { + out.push('\n'); + print_node(ast, stmt_id, out, depth + 1); + } + } NodeKind::Block { name, params, sep, body } => { out.push_str("Block '"); out.push_str(sep.src); out.push_str("',"); out.push('\n'); diff --git a/src/passes/ast/parser.rs b/src/passes/ast/parser.rs index e70ec47d..bc7a4953 100644 --- a/src/passes/ast/parser.rs +++ b/src/passes/ast/parser.rs @@ -80,6 +80,9 @@ impl<'src> Parser<'src> { let mut block_names = HashMap::new(); block_names.insert("fn", BlockMode::Ast); block_names.insert("match", BlockMode::Ast); + block_names.insert("type", BlockMode::Ast); + block_names.insert("enum", BlockMode::Ast); + block_names.insert("union", BlockMode::Ast); let mut p = Parser { lexer, src, @@ -417,6 +420,7 @@ impl<'src> Parser<'src> { if name == "fn" { return self.parse_fn(loc); } if name == "match" { return self.parse_match_expr(loc); } + if Self::is_type_keyword(name) { return self.parse_type_decl(loc, name); } if self.block_names.contains_key(name) { return self.parse_block(loc, name); } // Tagged template string: ident immediately adjacent to StrStart → raw template @@ -473,6 +477,7 @@ impl<'src> Parser<'src> { if let NodeKind::Ident(name) = self.get(head).kind { if name == "fn" { return self.parse_fn(self.loc_of(head)); } if name == "match" { return self.parse_match_expr(self.loc_of(head)); } + if Self::is_type_keyword(name) { return self.parse_type_decl(self.loc_of(head), name); } if self.block_names.contains_key(name) && name != "fn" && name != "match" { return self.parse_block(self.loc_of(head), name); } @@ -793,6 +798,10 @@ impl<'src> Parser<'src> { self.bump(); return self.parse_match_expr(name_tok.loc); } + if Self::is_type_keyword(name_tok.src) { + self.bump(); + return self.parse_type_decl(name_tok.loc, name_tok.src); + } if self.block_names.contains_key(name_tok.src) { self.bump(); return self.parse_block(name_tok.loc, name_tok.src); @@ -1568,6 +1577,90 @@ impl<'src> Parser<'src> { // --- fn --- + // type / enum / union — reserved keywords introducing a type declaration. + fn is_type_keyword(s: &str) -> bool { + matches!(s, "type" | "enum" | "union") + } + + // Parse a `type` / `enum` / `union` declaration. The keyword is already + // consumed; `kw` is its source ("type"/"enum"/"union"). + // + // type _ -> unit: Type with sep '_', empty params + body + // type T: -> generic params before the colon + // type: a, b -> inline positional (tuple) body + // type: -> block body (record arms / tuple exprs / members) + fn parse_type_decl(&mut self, kw_loc: Loc, kw: &'src str) -> ParseResult { + // Unit form `type _`: a bare wildcard with no following colon. + if self.at(TokenKind::Ident) && self.peek().src == "_" { + let underscore = *self.peek(); + // Only the unit form when `_` is NOT a generic param (`type _:` would be + // a colon-block with a `_` param — not currently meaningful, treat the + // bare `type _` as unit). + if self.peek_next().kind != TokenKind::Colon { + self.bump(); // consume `_` + let params = self.node(NodeKind::Patterns(Exprs::empty()), underscore.loc); + let loc = Loc { start: kw_loc.start, end: underscore.loc.end }; + let body = Exprs::empty(); + return Ok(self.make_type_node(kw, params, underscore, body, loc)); + } + } + + // Generic params: everything between the keyword and the `:`. + let (params, _) = self.parse_params()?; + + // Body after the colon: block (arms/exprs/spreads) or inline comma-separated exprs. + let sep = self.expect(TokenKind::Colon)?; + let body = if self.at(TokenKind::BlockStart) { + self.bump(); + self.parse_block_items(|p| p.parse_type_body_item())? + } else { + // Inline body: comma-separated expressions (positional tuple form). + self.parse_inline_type_items()? + }; + + let end = body.items.last().map(|&id| self.loc_of(id).end).unwrap_or(self.loc_of(params).end); + let loc = Loc { start: kw_loc.start, end }; + Ok(self.make_type_node(kw, params, sep, body, loc)) + } + + fn make_type_node( + &mut self, + kw: &'src str, + params: AstId, + sep: Token<'src>, + body: Exprs<'src>, + loc: Loc, + ) -> AstId { + let kind = match kw { + "type" => NodeKind::Type { params, sep, body }, + "enum" => NodeKind::Enum { params, sep, body }, + _ => NodeKind::Union { params, sep, body }, + }; + self.node(kind, loc) + } + + // A single line of a `type`/`enum`/`union` block body: a `..Foo` spread + // (type-level extension), a `key: T` arm (record field), or a bare type + // expression (tuple positional / enum constructor / union member). + fn parse_type_body_item(&mut self) -> ParseResult { + if Self::is_spread_op(self.peek()) { + return self.parse_spread(); + } + self.parse_expr_or_arm() + } + + // Inline (non-block) type body: comma-separated expressions, e.g. `type: u8, i8`. + fn parse_inline_type_items(&mut self) -> Result, ParseError> { + let mut items: Vec = vec![]; + let mut seps: Vec> = vec![]; + items.push(self.parse_expr()?); + while self.at(TokenKind::Comma) { + seps.push(self.bump()); + items.push(self.parse_expr()?); + } + Ok(Exprs { items: items.into_boxed_slice(), seps }) + } + fn parse_fn(&mut self, fn_loc: Loc) -> ParseResult { // "fn" already consumed let is_fn_match = self.at(TokenKind::Ident) && self.peek().src == "match"; @@ -2100,4 +2193,5 @@ mod tests { test_macros::include_fink_tests!("src/passes/ast/test_module.fnk"); test_macros::include_fink_tests!("src/passes/ast/test_member_apply.fnk"); test_macros::include_fink_tests!("src/passes/ast/test_errors.fnk"); + test_macros::include_fink_tests!("src/passes/ast/test_types.fnk"); } diff --git a/src/passes/ast/test_types.fnk b/src/passes/ast/test_types.fnk new file mode 100644 index 00000000..a3f0b7cd --- /dev/null +++ b/src/passes/ast/test_types.fnk @@ -0,0 +1,158 @@ +{test, expect, equals, ast} = import '!fake' +{ƒink} = import '@fink/parse/blocks.fnk' + + +--- +type: record (named fields) +--- + +test 'record type', fn: + expect ast ƒink: + type: + shrub: u8 + | equals ƒink: + Module + Type ':', + Patterns + Arm ':', + Ident 'shrub' + Ident 'u8' + + +test 'named record type binding', fn: + expect ast ƒink: + Foo = type: + shrub: u8 + ni: i8 + | equals ƒink: + Module + Bind '=', + Ident 'Foo' + Type ':', + Patterns + Arm ':', + Ident 'shrub' + Ident 'u8' + Arm ':', + Ident 'ni' + Ident 'i8' + + +--- +type: tuple (positional fields) +--- + +test 'tuple type', fn: + expect ast ƒink: + type: u8, i8 + | equals ƒink: + Module + Type ':', + Patterns + Ident 'u8' + Ident 'i8' + + +--- +type: unit +--- + +test 'unit type', fn: + expect ast ƒink: + type _ + | equals ƒink: + Module + Type '_' + + +--- +type: generic params +--- + +test 'generic type', fn: + expect ast ƒink: + type T: + ham: T + | equals ƒink: + Module + Type ':', + Patterns + Ident 'T' + Arm ':', + Ident 'ham' + Ident 'T' + + +--- +enum: closed sum +--- + +test 'enum type', fn: + expect ast ƒink: + enum T: + Some T + None + | equals ƒink: + Module + Enum ':', + Patterns + Ident 'T' + Apply + Ident 'Some' + Ident 'T' + Ident 'None' + + +--- +type: extension via ..Foo (type-level spread) +--- + +test 'record type extends', fn: + expect ast ƒink: + Bar = type: + ..Foo + ni: u8 + | equals ƒink: + Module + Bind '=', + Ident 'Bar' + Type ':', + Patterns + Spread '..', + Ident 'Foo' + Arm ':', + Ident 'ni' + Ident 'u8' + + +test 'tuple type extends with prepend', fn: + expect ast ƒink: + Nu = type: + u8 + ..Ni + | equals ƒink: + Module + Bind '=', + Ident 'Nu' + Type ':', + Patterns + Ident 'u8' + Spread '..', + Ident 'Ni' + + +--- +union: open union +--- + +test 'union block', fn: + expect ast ƒink: + union: + SignedInt + UnsignedInt + | equals ƒink: + Module + Union ':', + Patterns + Ident 'SignedInt' + Ident 'UnsignedInt' diff --git a/src/passes/ast/transform.rs b/src/passes/ast/transform.rs index 5b289a0c..c70c787c 100644 --- a/src/passes/ast/transform.rs +++ b/src/passes/ast/transform.rs @@ -77,6 +77,21 @@ pub trait Transform<'src> { NodeKind::Match { subjects, sep, arms } => self.transform_match(builder, src, subjects, sep, arms, loc), NodeKind::Arm { lhs, sep, body } => self.transform_arm(builder, src, lhs, sep, body, loc), NodeKind::Block { name, params, sep, body } => self.transform_block(builder, src, name, params, sep, body, loc), + NodeKind::Type { params, sep, body } => { + let params = self.transform(builder, src, params)?; + let body = self.transform_exprs(builder, src, &body)?; + Ok(builder.append(NodeKind::Type { params, sep, body }, loc)) + } + NodeKind::Enum { params, sep, body } => { + let params = self.transform(builder, src, params)?; + let body = self.transform_exprs(builder, src, &body)?; + Ok(builder.append(NodeKind::Enum { params, sep, body }, loc)) + } + NodeKind::Union { params, sep, body } => { + let params = self.transform(builder, src, params)?; + let body = self.transform_exprs(builder, src, &body)?; + Ok(builder.append(NodeKind::Union { params, sep, body }, loc)) + } } } diff --git a/src/passes/ast_desugar/mod.rs b/src/passes/ast_desugar/mod.rs index 7535b92f..90f3799d 100644 --- a/src/passes/ast_desugar/mod.rs +++ b/src/passes/ast_desugar/mod.rs @@ -446,6 +446,11 @@ fn has_partial(ast: &Ast<'_>, id: AstId) -> bool { has_partial(ast, *name) || has_partial(ast, *params) || body.items.iter().any(|&id| has_partial(ast, id)) } + NodeKind::Type { params, body, .. } + | NodeKind::Enum { params, body, .. } + | NodeKind::Union { params, body, .. } => { + has_partial(ast, *params) || body.items.iter().any(|&id| has_partial(ast, id)) + } NodeKind::Try(inner) => has_partial(ast, *inner), } } @@ -891,6 +896,12 @@ fn has_partial_builder(builder: &AstBuilder<'_>, id: AstId) -> bool { has_partial_builder(builder, *name) || has_partial_builder(builder, *params) || body.items.iter().any(|&id| has_partial_builder(builder, id)) } + NodeKind::Type { params, body, .. } + | NodeKind::Enum { params, body, .. } + | NodeKind::Union { params, body, .. } => { + has_partial_builder(builder, *params) + || body.items.iter().any(|&id| has_partial_builder(builder, id)) + } NodeKind::Try(inner) => has_partial_builder(builder, *inner), } } diff --git a/src/passes/cps/transform.rs b/src/passes/cps/transform.rs index dd3eb6c2..95934980 100644 --- a/src/passes/cps/transform.rs +++ b/src/passes/cps/transform.rs @@ -357,6 +357,10 @@ fn lower(g: &mut Gen, id: AstId) -> Lower { NodeKind::Patterns(_) => panic!("Patterns node lowered via fn/match"), NodeKind::Arm { .. } => panic!("Arm node lowered via lower_match"), NodeKind::Token(_) => panic!("Token node should not reach CPS transform"), + + // ---- type declarations: parsed, but not yet lowered ---- + NodeKind::Type { .. } | NodeKind::Enum { .. } | NodeKind::Union { .. } => + panic!("type/enum/union declaration lowering not yet implemented"), } } diff --git a/src/passes/scopes/mod.rs b/src/passes/scopes/mod.rs index 3c7db90e..b8ab3c39 100644 --- a/src/passes/scopes/mod.rs +++ b/src/passes/scopes/mod.rs @@ -56,6 +56,9 @@ pub enum ScopeKind { Fn, /// Match arm — pattern bindings visible in arm body. Arm, + /// Type-declaration body — generic params bind, body resolves type + /// references. The keyword ("type"/"enum"/"union") is carried for display. + Type(&'static str), } // --------------------------------------------------------------------------- @@ -612,6 +615,54 @@ fn walk_stmts<'src>(ast: &Ast<'src>, stmts: &[AstId], scope: ScopeId, ctx: &mut } } +/// Walk one member/field line of a `type` / `enum` / `union` body. +/// +/// `is_enum` selects the constructor-minting semantics: in an `enum`, the +/// leading name of a member is a *binding* (a minted constructor), and only +/// the payload is resolved as type references. In `type`/`union`, names that +/// are field labels are declarations (not resolved); everything else is a +/// type reference. +fn walk_type_member<'src>(ast: &Ast<'src>, id: AstId, scope: ScopeId, ctx: &mut Ctx<'src>, is_enum: bool) { + let kind = ast.nodes.get(id).kind.clone(); + match kind { + // `..Foo` -- references the extended type. + NodeKind::Spread { inner: Some(inner_id), .. } => walk_node(ast, inner_id, scope, ctx), + NodeKind::Spread { inner: None, .. } => {} + + // Record field `name: T` -- `name` is a field label (a declaration, not a + // reference); resolve only the type expression(s) in the body. + NodeKind::Arm { lhs, body, .. } => { + // A computed key (Group/expr) still needs resolution; a plain Ident + // field name does not (mirrors LitRec key handling). + if !matches!(ast.nodes.get(lhs).kind, NodeKind::Ident(_)) { + walk_node(ast, lhs, scope, ctx); + } + for &stmt_id in body.items.iter() { + walk_node(ast, stmt_id, scope, ctx); + } + } + + // Enum constructor with a payload: `Some T`. The constructor name (func) + // is minted (a binding); the payload args are type references. + NodeKind::Apply { func, args } if is_enum => { + register_pattern_binds(ast, func, scope, ctx); + for &arg_id in args.items.iter() { + walk_node(ast, arg_id, scope, ctx); + } + } + + // A bare name member. + NodeKind::Ident(_) | NodeKind::SynthIdent(_) if is_enum => { + // Nullary enum constructor `None` -- mint it. + register_pattern_binds(ast, id, scope, ctx); + } + + // type tuple positional / union member / enum payload-less non-ident: + // a type reference (or sub-expression to resolve). + _ => walk_node(ast, id, scope, ctx), + } +} + fn walk_node<'src>(ast: &Ast<'src>, id: AstId, scope: ScopeId, ctx: &mut Ctx<'src>) { // Clone the kind up front so we can freely call methods on ctx that would // otherwise conflict with a borrow of `ast`. NodeKind clone is cheap — @@ -810,6 +861,38 @@ fn walk_node<'src>(ast: &Ast<'src>, id: AstId, scope: ScopeId, ctx: &mut Ctx<'sr walk_stmts(ast, &body_items, scope, ctx); } + // Type declarations. Generic params bind in a child scope so the body's + // type expressions resolve against them. Body handling differs by keyword: + // + // type: fields. A record field `name: T` (Arm) declares `name` (not a + // reference) and references its type `T`. A tuple positional `T` + // is a type reference. `..Foo` references `Foo`. + // union: members are references to existing types. + // enum: members MINT constructors -- the constructor name is a binding, + // its payload types are references. + NodeKind::Type { params, body, .. } + | NodeKind::Enum { params, body, .. } + | NodeKind::Union { params, body, .. } => { + let kw = match ast.nodes.get(id).kind { + NodeKind::Type { .. } => "type", + NodeKind::Enum { .. } => "enum", + _ => "union", + }; + let is_enum = kw == "enum"; + let type_scope = ctx.push_scope(ScopeKind::Type(kw), Some(scope), id); + if let NodeKind::Patterns(pat_items) = &ast.nodes.get(params).kind { + let param_ids: Vec = pat_items.items.to_vec(); + for param_id in param_ids { + register_pattern_binds(ast, param_id, type_scope, ctx); + } + } + let body_items: Vec = body.items.to_vec(); + for item_id in body_items { + walk_type_member(ast, item_id, type_scope, ctx, is_enum); + } + ctx.pop_scope_binds(type_scope); + } + NodeKind::StrTempl { children, .. } | NodeKind::StrRawTempl { children, .. } => { for &child_id in children.iter() { walk_node(ast, child_id, scope, ctx); } } @@ -865,6 +948,7 @@ fn format_scope(scope_id: ScopeId, result: &ScopeResult, out: &mut String, inden ScopeKind::Module => "module".to_string(), ScopeKind::Fn => "fn".to_string(), ScopeKind::Arm => "arm".to_string(), + ScopeKind::Type(kw) => kw.to_string(), }; write_indent(out, indent); @@ -940,4 +1024,5 @@ mod tests { } test_macros::include_fink_tests!("src/passes/scopes/test_scope.fnk"); + test_macros::include_fink_tests!("src/passes/scopes/test_scope_types.fnk"); } diff --git a/src/passes/scopes/test_scope_types.fnk b/src/passes/scopes/test_scope_types.fnk new file mode 100644 index 00000000..6c47f83d --- /dev/null +++ b/src/passes/scopes/test_scope_types.fnk @@ -0,0 +1,49 @@ +{test, skip, expect, equals, scope} = import '!fake' +{ƒink} = import '@fink/parse/blocks.fnk' + + +# union members are references to existing types (unresolved here in isolation). +test 'union members are references', fn: + expect scope ƒink: + Int = union: + SignedInt + UnsignedInt + | equals ƒink: + scope 10, 'module', + + scope 4, 'union', + unresolved 'SignedInt' + unresolved 'UnsignedInt' + bind 0, 'Int' + + +# record field names are declarations, not references; only the field type +# resolves (unresolved here in isolation). +test 'record field names are not references', fn: + expect scope ƒink: + Foo = type: + shrub: u8 + | equals ƒink: + scope 11, 'module', + + scope 5, 'type', + unresolved 'u8' + bind 0, 'Foo' + + +# enum members MINT constructors: the constructor name binds, the payload type +# resolves against the generic param. +test 'enum members mint constructors', fn: + expect scope ƒink: + Option = enum T: + Some T + None + | equals ƒink: + scope 13, 'module', + + scope 7, 'enum', + bind 1, 'T' + bind 3, 'Some' + ref 'T', 1 + bind 6, 'None' + bind 0, 'Option'