From 3132d0cf6f70bd0ed61323e22a86e12bca8f2a9d Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Sat, 6 Dec 2025 23:01:48 -0300 Subject: [PATCH] refactor: Enhance type emitter --- crates/plotnik-lib/src/infer/emit/rust.rs | 6 +- .../plotnik-lib/src/infer/emit/rust_tests.rs | 60 +++--- .../plotnik-lib/src/infer/emit/typescript.rs | 6 +- .../src/infer/emit/typescript_tests.rs | 104 +++++----- crates/plotnik-lib/src/infer/types.rs | 140 ++++++++++++- crates/plotnik-lib/src/infer/types_tests.rs | 165 ++++++++++++++- crates/plotnik-lib/src/infer/tyton.rs | 47 +++-- crates/plotnik-lib/src/infer/tyton_tests.rs | 188 +++++++++++++----- 8 files changed, 553 insertions(+), 163 deletions(-) diff --git a/crates/plotnik-lib/src/infer/emit/rust.rs b/crates/plotnik-lib/src/infer/emit/rust.rs index 6a9c8a8b..86b0c65c 100644 --- a/crates/plotnik-lib/src/infer/emit/rust.rs +++ b/crates/plotnik-lib/src/infer/emit/rust.rs @@ -70,7 +70,7 @@ fn emit_type_def( let name = key.to_pascal_case(); match value { - TypeValue::Node | TypeValue::String | TypeValue::Unit => String::new(), + TypeValue::Node | TypeValue::String | TypeValue::Unit | TypeValue::Invalid => String::new(), TypeValue::Struct(fields) => { let mut out = emit_derives(config); @@ -134,7 +134,7 @@ pub(crate) fn emit_type_ref( let base = match table.get(key) { Some(TypeValue::Node) => "Node".to_string(), Some(TypeValue::String) => "String".to_string(), - Some(TypeValue::Unit) => "()".to_string(), + Some(TypeValue::Unit) | Some(TypeValue::Invalid) => "()".to_string(), Some(TypeValue::Optional(inner)) => { let inner_str = emit_type_ref(inner, table, config); format!("Option<{}>", inner_str) @@ -225,7 +225,7 @@ fn visit<'src>( pub(crate) fn dependencies<'src>(value: &TypeValue<'src>) -> Vec> { match value { - TypeValue::Node | TypeValue::String | TypeValue::Unit => vec![], + TypeValue::Node | TypeValue::String | TypeValue::Unit | TypeValue::Invalid => vec![], TypeValue::Struct(fields) => fields.values().cloned().collect(), diff --git a/crates/plotnik-lib/src/infer/emit/rust_tests.rs b/crates/plotnik-lib/src/infer/emit/rust_tests.rs index 89b4822f..192d76c1 100644 --- a/crates/plotnik-lib/src/infer/emit/rust_tests.rs +++ b/crates/plotnik-lib/src/infer/emit/rust_tests.rs @@ -24,7 +24,7 @@ fn emit_cyclic(input: &str, cyclic_types: &[&str]) -> String { #[test] fn emit_struct_single_field() { - let input = "Foo = { Node @value }"; + let input = "Foo = { #Node @value }"; insta::assert_snapshot!(emit(input), @r" #[derive(Debug, Clone)] pub struct Foo { @@ -35,7 +35,7 @@ fn emit_struct_single_field() { #[test] fn emit_struct_multiple_fields() { - let input = "Func = { string @name Node @body Node @params }"; + let input = "Func = { #string @name #Node @body #Node @params }"; insta::assert_snapshot!(emit(input), @r" #[derive(Debug, Clone)] pub struct Func { @@ -69,8 +69,8 @@ fn emit_struct_with_unit_field() { #[test] fn emit_struct_nested_refs() { let input = indoc! {r#" - Inner = { Node @value } - Outer = { Inner @inner string @label } + Inner = { #Node @value } + Outer = { Inner @inner #string @label } "#}; insta::assert_snapshot!(emit(input), @r" #[derive(Debug, Clone)] @@ -91,8 +91,8 @@ fn emit_struct_nested_refs() { #[test] fn emit_tagged_union_simple() { let input = indoc! {r#" - AssignStmt = { Node @target Node @value } - CallStmt = { Node @func } + AssignStmt = { #Node @target #Node @value } + CallStmt = { #Node @func } Stmt = [ Assign: AssignStmt Call: CallStmt ] "#}; insta::assert_snapshot!(emit(input), @r" @@ -123,7 +123,7 @@ fn emit_tagged_union_simple() { #[test] fn emit_tagged_union_with_empty_variant() { let input = indoc! {r#" - ValueVariant = { Node @value } + ValueVariant = { #Node @value } Expr = [ Some: ValueVariant None: () ] "#}; insta::assert_snapshot!(emit(input), @r" @@ -157,7 +157,7 @@ fn emit_tagged_union_all_empty() { #[test] fn emit_tagged_union_with_builtins() { - let input = "Value = [ Text: string Code: Node Empty: () ]"; + let input = "Value = [ Text: #string Code: #Node Empty: () ]"; insta::assert_snapshot!(emit(input), @r" #[derive(Debug, Clone)] pub enum Value { @@ -172,26 +172,26 @@ fn emit_tagged_union_with_builtins() { #[test] fn emit_optional() { - let input = "MaybeNode = Node?"; + let input = "MaybeNode = #Node?"; insta::assert_snapshot!(emit(input), @"pub type MaybeNode = Option;"); } #[test] fn emit_list() { - let input = "Nodes = Node*"; + let input = "Nodes = #Node*"; insta::assert_snapshot!(emit(input), @"pub type Nodes = Vec;"); } #[test] fn emit_non_empty_list() { - let input = "Nodes = Node+"; + let input = "Nodes = #Node+"; insta::assert_snapshot!(emit(input), @"pub type Nodes = Vec;"); } #[test] fn emit_optional_named() { let input = indoc! {r#" - Stmt = { Node @value } + Stmt = { #Node @value } MaybeStmt = Stmt? "#}; insta::assert_snapshot!(emit(input), @r" @@ -207,7 +207,7 @@ fn emit_optional_named() { #[test] fn emit_list_named() { let input = indoc! {r#" - Stmt = { Node @value } + Stmt = { #Node @value } Stmts = Stmt* "#}; insta::assert_snapshot!(emit(input), @r" @@ -223,7 +223,7 @@ fn emit_list_named() { #[test] fn emit_nested_wrappers() { let input = indoc! {r#" - Item = { Node @value } + Item = { #Node @value } Items = Item* MaybeItems = Items? "#}; @@ -244,7 +244,7 @@ fn emit_nested_wrappers() { #[test] fn emit_cyclic_box() { let input = indoc! {r#" - TreeNode = { Node @value TreeNode @left TreeNode @right } + TreeNode = { #Node @value TreeNode @left TreeNode @right } "#}; insta::assert_snapshot!(emit_cyclic(input, &["TreeNode"]), @r" #[derive(Debug, Clone)] @@ -258,7 +258,7 @@ fn emit_cyclic_box() { #[test] fn emit_cyclic_rc() { - let input = "TreeNode = { Node @value TreeNode @child }"; + let input = "TreeNode = { #Node @value TreeNode @child }"; let config = RustEmitConfig { indirection: Indirection::Rc, ..Default::default() @@ -276,7 +276,7 @@ fn emit_cyclic_rc() { #[test] fn emit_cyclic_arc() { - let input = "TreeNode = { Node @value TreeNode @child }"; + let input = "TreeNode = { #Node @value TreeNode @child }"; let config = RustEmitConfig { indirection: Indirection::Arc, ..Default::default() @@ -296,7 +296,7 @@ fn emit_cyclic_arc() { #[test] fn emit_no_derives() { - let input = "Foo = { Node @value }"; + let input = "Foo = { #Node @value }"; let config = RustEmitConfig { derive_debug: false, derive_clone: false, @@ -312,7 +312,7 @@ fn emit_no_derives() { #[test] fn emit_debug_only() { - let input = "Foo = { Node @value }"; + let input = "Foo = { #Node @value }"; let config = RustEmitConfig { derive_debug: true, derive_clone: false, @@ -329,7 +329,7 @@ fn emit_debug_only() { #[test] fn emit_all_derives() { - let input = "Foo = { Node @value }"; + let input = "Foo = { #Node @value }"; let config = RustEmitConfig { derive_debug: true, derive_clone: true, @@ -349,11 +349,11 @@ fn emit_all_derives() { #[test] fn emit_complex_program() { let input = indoc! {r#" - FuncInfo = { string @name Node @body } - Param = { string @name string @type_annotation } + FuncInfo = { #string @name #Node @body } + Param = { #string @name #string @type_annotation } Params = Param* FuncDecl = { FuncInfo @info Params @params } - ExprStmt = { Node @expr } + ExprStmt = { #Node @expr } Stmt = [ Func: FuncDecl Expr: ExprStmt ] Program = { Stmt @statements } "#}; @@ -420,8 +420,8 @@ fn emit_synthetic_keys() { #[test] fn emit_mixed_wrappers_and_structs() { let input = indoc! {r#" - Leaf = { string @text } - Branch = { Node @left Node @right } + Leaf = { #string @text } + Branch = { #Node @left #Node @right } Tree = [ Leaf: Leaf Branch: Branch ] Forest = Tree* MaybeForest = Forest? @@ -460,7 +460,7 @@ fn emit_mixed_wrappers_and_structs() { #[test] fn emit_single_variant_union() { let input = indoc! {r#" - OnlyVariant = { Node @value } + OnlyVariant = { #Node @value } Single = [ Only: OnlyVariant ] "#}; insta::assert_snapshot!(emit(input), @r" @@ -481,7 +481,7 @@ fn emit_single_variant_union() { #[test] fn emit_deeply_nested() { let input = indoc! {r#" - A = { Node @val } + A = { #Node @val } B = { A @a } C = { B @b } D = { C @c } @@ -512,7 +512,7 @@ fn emit_deeply_nested() { #[test] fn emit_list_of_optionals() { let input = indoc! {r#" - Item = { Node @value } + Item = { #Node @value } MaybeItem = Item? Items = MaybeItem* "#}; @@ -531,8 +531,8 @@ fn emit_list_of_optionals() { #[test] fn emit_builtin_value_with_named_key() { let input = indoc! {r#" - AliasNode = Node - AliasString = string + AliasNode = #Node + AliasString = #string AliasUnit = () "#}; insta::assert_snapshot!(emit(input), @""); diff --git a/crates/plotnik-lib/src/infer/emit/typescript.rs b/crates/plotnik-lib/src/infer/emit/typescript.rs index 3e9591ff..d07f2225 100644 --- a/crates/plotnik-lib/src/infer/emit/typescript.rs +++ b/crates/plotnik-lib/src/infer/emit/typescript.rs @@ -91,7 +91,7 @@ fn emit_type_def( }; match value { - TypeValue::Node | TypeValue::String | TypeValue::Unit => String::new(), + TypeValue::Node | TypeValue::String | TypeValue::Unit | TypeValue::Invalid => String::new(), TypeValue::Struct(fields) => { if config.use_type_alias { @@ -166,7 +166,7 @@ pub(crate) fn emit_field_type( match table.get(key) { Some(TypeValue::Node) => (config.node_type_name.clone(), false), Some(TypeValue::String) => ("string".to_string(), false), - Some(TypeValue::Unit) => ("{}".to_string(), false), + Some(TypeValue::Unit) | Some(TypeValue::Invalid) => ("{}".to_string(), false), Some(TypeValue::Optional(inner)) => { let (inner_str, _) = emit_field_type(inner, table, config); @@ -279,7 +279,7 @@ fn visit<'src>( pub(crate) fn dependencies<'src>(value: &TypeValue<'src>) -> Vec> { match value { - TypeValue::Node | TypeValue::String | TypeValue::Unit => vec![], + TypeValue::Node | TypeValue::String | TypeValue::Unit | TypeValue::Invalid => vec![], TypeValue::Struct(fields) => fields.values().cloned().collect(), TypeValue::TaggedUnion(variants) => variants.values().cloned().collect(), TypeValue::Optional(inner) | TypeValue::List(inner) | TypeValue::NonEmptyList(inner) => { diff --git a/crates/plotnik-lib/src/infer/emit/typescript_tests.rs b/crates/plotnik-lib/src/infer/emit/typescript_tests.rs index 73ee2a15..13fda721 100644 --- a/crates/plotnik-lib/src/infer/emit/typescript_tests.rs +++ b/crates/plotnik-lib/src/infer/emit/typescript_tests.rs @@ -16,7 +16,7 @@ fn emit_with_config(input: &str, config: &TypeScriptEmitConfig) -> String { #[test] fn emit_interface_single_field() { - let input = "Foo = { Node @value }"; + let input = "Foo = { #Node @value }"; insta::assert_snapshot!(emit(input), @r" interface Foo { value: SyntaxNode; @@ -26,7 +26,7 @@ fn emit_interface_single_field() { #[test] fn emit_interface_multiple_fields() { - let input = "Func = { string @name Node @body Node @params }"; + let input = "Func = { #string @name #Node @body #Node @params }"; insta::assert_snapshot!(emit(input), @r" interface Func { name: string; @@ -55,8 +55,8 @@ fn emit_interface_with_unit_field() { #[test] fn emit_interface_nested_refs() { let input = indoc! {r#" - Inner = { Node @value } - Outer = { Inner @inner string @label } + Inner = { #Node @value } + Outer = { Inner @inner #string @label } "#}; insta::assert_snapshot!(emit(input), @r" interface Inner { @@ -75,8 +75,8 @@ fn emit_interface_nested_refs() { #[test] fn emit_tagged_union_simple() { let input = indoc! {r#" - AssignStmt = { Node @target Node @value } - CallStmt = { Node @func } + AssignStmt = { #Node @target #Node @value } + CallStmt = { #Node @func } Stmt = [ Assign: AssignStmt Call: CallStmt ] "#}; insta::assert_snapshot!(emit(input), @r#" @@ -98,7 +98,7 @@ fn emit_tagged_union_simple() { #[test] fn emit_tagged_union_with_empty_variant() { let input = indoc! {r#" - ValueVariant = { Node @value } + ValueVariant = { #Node @value } Expr = [ Some: ValueVariant None: () ] "#}; insta::assert_snapshot!(emit(input), @r#" @@ -125,7 +125,7 @@ fn emit_tagged_union_all_empty() { #[test] fn emit_tagged_union_with_builtins() { - let input = "Value = [ Text: string Code: Node Empty: () ]"; + let input = "Value = [ Text: #string Code: #Node Empty: () ]"; insta::assert_snapshot!(emit(input), @r#" type Value = | { tag: "Text" } @@ -138,13 +138,13 @@ fn emit_tagged_union_with_builtins() { #[test] fn emit_optional_null() { - let input = "MaybeNode = Node?"; + let input = "MaybeNode = #Node?"; insta::assert_snapshot!(emit(input), @"type MaybeNode = SyntaxNode | null;"); } #[test] fn emit_optional_undefined() { - let input = "MaybeNode = Node?"; + let input = "MaybeNode = #Node?"; let config = TypeScriptEmitConfig { optional_style: OptionalStyle::Undefined, ..Default::default() @@ -155,7 +155,7 @@ fn emit_optional_undefined() { #[test] fn emit_optional_question_mark() { let input = indoc! {r#" - MaybeNode = Node? + MaybeNode = #Node? Foo = { MaybeNode @maybe } "#}; let config = TypeScriptEmitConfig { @@ -173,20 +173,20 @@ fn emit_optional_question_mark() { #[test] fn emit_list() { - let input = "Nodes = Node*"; + let input = "Nodes = #Node*"; insta::assert_snapshot!(emit(input), @"type Nodes = SyntaxNode[];"); } #[test] fn emit_non_empty_list() { - let input = "Nodes = Node+"; + let input = "Nodes = #Node+"; insta::assert_snapshot!(emit(input), @"type Nodes = [SyntaxNode, ...SyntaxNode[]];"); } #[test] fn emit_optional_named() { let input = indoc! {r#" - Stmt = { Node @value } + Stmt = { #Node @value } MaybeStmt = Stmt? "#}; insta::assert_snapshot!(emit(input), @r" @@ -201,7 +201,7 @@ fn emit_optional_named() { #[test] fn emit_list_named() { let input = indoc! {r#" - Stmt = { Node @value } + Stmt = { #Node @value } Stmts = Stmt* "#}; insta::assert_snapshot!(emit(input), @r" @@ -216,7 +216,7 @@ fn emit_list_named() { #[test] fn emit_nested_wrappers() { let input = indoc! {r#" - Item = { Node @value } + Item = { #Node @value } Items = Item* MaybeItems = Items? "#}; @@ -234,7 +234,7 @@ fn emit_nested_wrappers() { #[test] fn emit_list_of_optionals() { let input = indoc! {r#" - Item = { Node @value } + Item = { #Node @value } MaybeItem = Item? Items = MaybeItem* "#}; @@ -253,7 +253,7 @@ fn emit_list_of_optionals() { #[test] fn emit_with_export() { - let input = "Foo = { Node @value }"; + let input = "Foo = { #Node @value }"; let config = TypeScriptEmitConfig { export: true, ..Default::default() @@ -267,7 +267,7 @@ fn emit_with_export() { #[test] fn emit_readonly_fields() { - let input = "Foo = { Node @value string @name }"; + let input = "Foo = { #Node @value #string @name }"; let config = TypeScriptEmitConfig { readonly: true, ..Default::default() @@ -282,7 +282,7 @@ fn emit_readonly_fields() { #[test] fn emit_custom_node_type() { - let input = "Foo = { Node @value }"; + let input = "Foo = { #Node @value }"; let config = TypeScriptEmitConfig { node_type_name: "TSNode".to_string(), ..Default::default() @@ -296,7 +296,7 @@ fn emit_custom_node_type() { #[test] fn emit_type_alias_instead_of_interface() { - let input = "Foo = { Node @value string @name }"; + let input = "Foo = { #Node @value #string @name }"; let config = TypeScriptEmitConfig { use_type_alias: true, ..Default::default() @@ -317,8 +317,8 @@ fn emit_type_alias_empty() { #[test] fn emit_type_alias_nested() { let input = indoc! {r#" - Inner = { Node @value } - Outer = { Inner @inner string @label } + Inner = { #Node @value } + Outer = { Inner @inner #string @label } "#}; let config = TypeScriptEmitConfig { use_type_alias: true, @@ -364,11 +364,11 @@ fn emit_inline_synthetic() { #[test] fn emit_complex_program() { let input = indoc! {r#" - FuncInfo = { string @name Node @body } - Param = { string @name string @type_annotation } + FuncInfo = { #string @name #Node @body } + Param = { #string @name #string @type_annotation } Params = Param* FuncDecl = { FuncInfo @info Params @params } - ExprStmt = { Node @expr } + ExprStmt = { #Node @expr } Stmt = [ Func: FuncDecl Expr: ExprStmt ] Program = { Stmt @statements } "#}; @@ -407,8 +407,8 @@ fn emit_complex_program() { #[test] fn emit_mixed_wrappers_and_structs() { let input = indoc! {r#" - Leaf = { string @text } - Branch = { Node @left Node @right } + Leaf = { #string @text } + Branch = { #Node @left #Node @right } Tree = [ Leaf: Leaf Branch: Branch ] Forest = Tree* MaybeForest = Forest? @@ -436,8 +436,8 @@ fn emit_mixed_wrappers_and_structs() { #[test] fn emit_all_config_options() { let input = indoc! {r#" - MaybeNode = Node? - Item = { Node @value MaybeNode @maybe } + MaybeNode = #Node? + Item = { #Node @value MaybeNode @maybe } Items = Item* "#}; let config = TypeScriptEmitConfig { @@ -465,7 +465,7 @@ fn emit_all_config_options() { #[test] fn emit_single_variant_union() { let input = indoc! {r#" - OnlyVariant = { Node @value } + OnlyVariant = { #Node @value } Single = [ Only: OnlyVariant ] "#}; insta::assert_snapshot!(emit(input), @r#" @@ -481,7 +481,7 @@ fn emit_single_variant_union() { #[test] fn emit_deeply_nested() { let input = indoc! {r#" - A = { Node @val } + A = { #Node @val } B = { A @a } C = { B @b } D = { C @c } @@ -508,8 +508,8 @@ fn emit_deeply_nested() { #[test] fn emit_union_in_list() { let input = indoc! {r#" - A = { Node @a } - B = { Node @b } + A = { #Node @a } + B = { #Node @b } Choice = [ A: A B: B ] Choices = Choice* "#}; @@ -533,8 +533,8 @@ fn emit_union_in_list() { #[test] fn emit_optional_in_struct_null_style() { let input = indoc! {r#" - MaybeNode = Node? - Container = { MaybeNode @item string @name } + MaybeNode = #Node? + Container = { MaybeNode @item #string @name } "#}; insta::assert_snapshot!(emit(input), @r" type MaybeNode = SyntaxNode | null; @@ -549,8 +549,8 @@ fn emit_optional_in_struct_null_style() { #[test] fn emit_optional_in_struct_undefined_style() { let input = indoc! {r#" - MaybeNode = Node? - Container = { MaybeNode @item string @name } + MaybeNode = #Node? + Container = { MaybeNode @item #string @name } "#}; let config = TypeScriptEmitConfig { optional_style: OptionalStyle::Undefined, @@ -569,9 +569,9 @@ fn emit_optional_in_struct_undefined_style() { #[test] fn emit_tagged_union_with_optional_field_question_mark() { let input = indoc! {r#" - MaybeNode = Node? + MaybeNode = #Node? VariantA = { MaybeNode @value } - VariantB = { Node @item } + VariantB = { #Node @item } Choice = [ A: VariantA B: VariantB ] "#}; let config = TypeScriptEmitConfig { @@ -598,10 +598,10 @@ fn emit_tagged_union_with_optional_field_question_mark() { #[test] fn emit_struct_with_union_field() { let input = indoc! {r#" - A = { Node @a } - B = { Node @b } + A = { #Node @a } + B = { #Node @b } Choice = [ A: A B: B ] - Container = { Choice @choice string @name } + Container = { Choice @choice #string @name } "#}; insta::assert_snapshot!(emit(input), @r#" interface A { @@ -627,7 +627,7 @@ fn emit_struct_with_union_field() { fn emit_struct_with_forward_ref() { let input = indoc! {r#" Container = { Later @item } - Later = { Node @value } + Later = { #Node @value } "#}; insta::assert_snapshot!(emit(input), @r" interface Later { @@ -642,7 +642,7 @@ fn emit_struct_with_forward_ref() { #[test] fn emit_synthetic_type_no_inline() { - let input = " = { Node @value }"; + let input = " = { #Node @value }"; let config = TypeScriptEmitConfig { inline_synthetic: false, ..Default::default() @@ -656,7 +656,7 @@ fn emit_synthetic_type_no_inline() { #[test] fn emit_synthetic_type_with_inline() { - let input = " = { Node @value }"; + let input = " = { #Node @value }"; let config = TypeScriptEmitConfig { inline_synthetic: true, ..Default::default() @@ -667,8 +667,8 @@ fn emit_synthetic_type_with_inline() { #[test] fn emit_field_referencing_tagged_union() { let input = indoc! {r#" - VarA = { Node @x } - VarB = { Node @y } + VarA = { #Node @x } + VarB = { #Node @y } Choice = [ A: VarA B: VarB ] Container = { Choice @choice } "#}; @@ -714,8 +714,8 @@ fn emit_empty_interface_no_type_alias() { #[test] fn emit_inline_synthetic_struct_with_optional_field() { let input = indoc! {r#" - MaybeNode = Node? - = { Node @value MaybeNode @maybe } + MaybeNode = #Node? + = { #Node @value MaybeNode @maybe } Container = { @inner } "#}; let config = TypeScriptEmitConfig { @@ -735,8 +735,8 @@ fn emit_inline_synthetic_struct_with_optional_field() { #[test] fn emit_builtin_value_with_named_key() { let input = indoc! {r#" - AliasNode = Node - AliasString = string + AliasNode = #Node + AliasString = #string AliasUnit = () "#}; insta::assert_snapshot!(emit(input), @""); diff --git a/crates/plotnik-lib/src/infer/types.rs b/crates/plotnik-lib/src/infer/types.rs index 83ba2508..9942fe59 100644 --- a/crates/plotnik-lib/src/infer/types.rs +++ b/crates/plotnik-lib/src/infer/types.rs @@ -1,7 +1,45 @@ //! Type representation for inferred query output types. //! +//! # Overview +//! //! The type system is flat: all types live in a `TypeTable` keyed by `TypeKey`. //! Wrapper types (Optional, List, NonEmptyList) reference inner types by key. +//! +//! # Design Decisions +//! +//! ## Alternation Handling +//! +//! Alternations (`[A: ... B: ...]` or `[... ...]`) produce different type structures: +//! +//! - **Tagged alternations** (`[A: expr B: expr]`): Become `TaggedUnion` with named variants. +//! Each branch gets its own struct type, discriminated by the tag name. +//! +//! - **Untagged/mixed alternations** (`[expr expr]`): Branches are "merged" into a single +//! struct where fields are combined. The merge rules: +//! 1. Field present in all branches with same type → field has that type +//! 2. Field present in some branches only → field becomes Optional +//! 3. Field present in all branches but with different types → field gets Invalid type +//! +//! ## Invalid Type +//! +//! The `Invalid` type represents a type conflict that couldn't be resolved (e.g., field +//! has `Node` in one branch and `String` in another). It is emitted the same as `Unit` +//! in code generators—this keeps output valid while signaling the user made a questionable +//! query. Diagnostics should warn about Invalid types during inference. +//! +//! ## Type Keys vs Type Values +//! +//! - `TypeKey`: Identity/reference to a type. Used in field types, wrapper inner types. +//! - `TypeValue`: The actual type definition. Stored in the table. +//! +//! Built-in types (Node, String, Unit, Invalid) have both a key and value variant for +//! consistency—the key is what you reference, the value is what gets stored. +//! +//! ## Synthetic Keys +//! +//! For nested captures like `(function @fn { (param @p) @params })`, we need unique type +//! names. Synthetic keys use path segments: `["fn", "params"]` → `FnParams`. This avoids +//! name collisions while keeping names readable. use indexmap::IndexMap; @@ -14,6 +52,9 @@ pub enum TypeKey<'src> { String, /// Unit type for empty captures (built-in) Unit, + /// Invalid type for unresolvable conflicts (built-in) + /// Emitted same as Unit in code generators. + Invalid, /// User-provided type name via `:: TypeName` Named(&'src str), /// Path-based synthetic name: ["Foo", "bar"] → FooBar @@ -27,10 +68,19 @@ impl TypeKey<'_> { TypeKey::Node => "Node".to_string(), TypeKey::String => "String".to_string(), TypeKey::Unit => "Unit".to_string(), + TypeKey::Invalid => "Unit".to_string(), // Invalid emits as Unit TypeKey::Named(name) => (*name).to_string(), TypeKey::Synthetic(segments) => segments.iter().map(|s| to_pascal(s)).collect(), } } + + /// Returns true if this is a built-in primitive type. + pub fn is_builtin(&self) -> bool { + matches!( + self, + TypeKey::Node | TypeKey::String | TypeKey::Unit | TypeKey::Invalid + ) + } } /// Convert snake_case or lowercase to PascalCase. @@ -55,6 +105,9 @@ pub enum TypeValue<'src> { String, /// Unit type (empty struct) Unit, + /// Invalid type (conflicting types in untagged union) + /// Emitted same as Unit. Presence indicates a diagnostic should be emitted. + Invalid, /// Struct with named fields Struct(IndexMap<&'src str, TypeKey<'src>>), /// Tagged union: variant name → variant type (must resolve to Struct or Unit) @@ -67,11 +120,22 @@ pub enum TypeValue<'src> { NonEmptyList(TypeKey<'src>), } +/// Result of merging a single field across branches. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MergedField<'src> { + /// Field has same type in all branches where present + Same(TypeKey<'src>), + /// Field has same type but missing in some branches → needs Optional wrapper + Optional(TypeKey<'src>), + /// Field has conflicting types across branches → Invalid + Conflict, +} + /// Collection of all inferred types for a query. #[derive(Debug, Clone)] pub struct TypeTable<'src> { /// All type definitions, keyed by their identity. - /// Pre-populated with built-in types (Node, String, Unit). + /// Pre-populated with built-in types (Node, String, Unit, Invalid). pub types: IndexMap, TypeValue<'src>>, /// Types that contain cyclic references (need Box in Rust). pub cyclic: Vec>, @@ -84,6 +148,7 @@ impl<'src> TypeTable<'src> { types.insert(TypeKey::Node, TypeValue::Node); types.insert(TypeKey::String, TypeValue::String); types.insert(TypeKey::Unit, TypeValue::Unit); + types.insert(TypeKey::Invalid, TypeValue::Invalid); Self { types, cyclic: Vec::new(), @@ -117,6 +182,79 @@ impl<'src> TypeTable<'src> { pub fn iter(&self) -> impl Iterator, &TypeValue<'src>)> { self.types.iter() } + + /// Merge fields from multiple struct branches (for untagged unions). + /// + /// Given a list of field maps (one per branch), produces a merged field map where: + /// - Fields present in all branches with the same type keep that type + /// - Fields present in only some branches become Optional + /// - Fields with conflicting types across branches become Invalid + /// + /// # Example + /// + /// Branch 1: `{ name: String, value: Node }` + /// Branch 2: `{ name: String, extra: Node }` + /// + /// Merged: `{ name: String, value: Optional, extra: Optional }` + /// + /// # Type Conflict Example + /// + /// Branch 1: `{ x: String }` + /// Branch 2: `{ x: Node }` + /// + /// Merged: `{ x: Invalid }` (with diagnostic warning) + pub fn merge_fields( + branches: &[IndexMap<&'src str, TypeKey<'src>>], + ) -> IndexMap<&'src str, MergedField<'src>> { + if branches.is_empty() { + return IndexMap::new(); + } + + // Collect all field names across all branches + let mut all_fields: IndexMap<&'src str, ()> = IndexMap::new(); + for branch in branches { + for field_name in branch.keys() { + all_fields.entry(*field_name).or_insert(()); + } + } + + let mut result = IndexMap::new(); + let branch_count = branches.len(); + + for field_name in all_fields.keys() { + // Collect (type, count) for this field across branches + let mut type_occurrences: Vec<&TypeKey<'src>> = Vec::new(); + for branch in branches { + if let Some(ty) = branch.get(field_name) { + type_occurrences.push(ty); + } + } + + let present_count = type_occurrences.len(); + if present_count == 0 { + continue; + } + + // Check if all occurrences have the same type + let first_type = type_occurrences[0]; + let all_same_type = type_occurrences.iter().all(|t| *t == first_type); + + let merged = if !all_same_type { + // Type conflict + MergedField::Conflict + } else if present_count == branch_count { + // Present in all branches with same type + MergedField::Same(first_type.clone()) + } else { + // Present in some branches only + MergedField::Optional(first_type.clone()) + }; + + result.insert(*field_name, merged); + } + + result + } } impl Default for TypeTable<'_> { diff --git a/crates/plotnik-lib/src/infer/types_tests.rs b/crates/plotnik-lib/src/infer/types_tests.rs index 04d48be5..32299deb 100644 --- a/crates/plotnik-lib/src/infer/types_tests.rs +++ b/crates/plotnik-lib/src/infer/types_tests.rs @@ -1,4 +1,4 @@ -use super::types::{TypeKey, TypeTable, TypeValue, to_pascal}; +use super::types::{MergedField, TypeKey, TypeTable, TypeValue, to_pascal}; use indexmap::IndexMap; #[test] @@ -6,6 +6,7 @@ fn type_key_to_pascal_case_builtins() { assert_eq!(TypeKey::Node.to_pascal_case(), "Node"); assert_eq!(TypeKey::String.to_pascal_case(), "String"); assert_eq!(TypeKey::Unit.to_pascal_case(), "Unit"); + assert_eq!(TypeKey::Invalid.to_pascal_case(), "Unit"); // Invalid emits as Unit } #[test] @@ -48,6 +49,7 @@ fn type_table_new_has_builtins() { assert_eq!(table.get(&TypeKey::Node), Some(&TypeValue::Node)); assert_eq!(table.get(&TypeKey::String), Some(&TypeValue::String)); assert_eq!(table.get(&TypeKey::Unit), Some(&TypeValue::Unit)); + assert_eq!(table.get(&TypeKey::Invalid), Some(&TypeValue::Invalid)); } #[test] @@ -81,13 +83,14 @@ fn type_table_iter_preserves_order() { table.insert(TypeKey::Named("C"), TypeValue::Unit); let keys: Vec<_> = table.iter().map(|(k, _)| k.clone()).collect(); - // Builtins first, then inserted order + // Builtins first (Node, String, Unit, Invalid), then inserted order assert_eq!(keys[0], TypeKey::Node); assert_eq!(keys[1], TypeKey::String); assert_eq!(keys[2], TypeKey::Unit); - assert_eq!(keys[3], TypeKey::Named("A")); - assert_eq!(keys[4], TypeKey::Named("B")); - assert_eq!(keys[5], TypeKey::Named("C")); + assert_eq!(keys[3], TypeKey::Invalid); + assert_eq!(keys[4], TypeKey::Named("A")); + assert_eq!(keys[5], TypeKey::Named("B")); + assert_eq!(keys[6], TypeKey::Named("C")); } #[test] @@ -220,3 +223,155 @@ fn type_key_hash_consistency() { assert!(set.contains(&TypeKey::Synthetic(vec!["a", "b"]))); assert!(!set.contains(&TypeKey::String)); } + +#[test] +fn type_key_is_builtin() { + assert!(TypeKey::Node.is_builtin()); + assert!(TypeKey::String.is_builtin()); + assert!(TypeKey::Unit.is_builtin()); + assert!(TypeKey::Invalid.is_builtin()); + assert!(!TypeKey::Named("Foo").is_builtin()); + assert!(!TypeKey::Synthetic(vec!["a"]).is_builtin()); +} + +#[test] +fn type_value_invalid() { + assert_eq!(TypeValue::Invalid, TypeValue::Invalid); + assert_ne!(TypeValue::Invalid, TypeValue::Unit); +} + +#[test] +fn merge_fields_empty_branches() { + let branches: Vec> = vec![]; + + let merged = TypeTable::merge_fields(&branches); + + assert!(merged.is_empty()); +} + +#[test] +fn merge_fields_single_branch() { + let mut branch = IndexMap::new(); + branch.insert("name", TypeKey::String); + branch.insert("value", TypeKey::Node); + + let merged = TypeTable::merge_fields(&[branch]); + + assert_eq!(merged.len(), 2); + assert_eq!(merged["name"], MergedField::Same(TypeKey::String)); + assert_eq!(merged["value"], MergedField::Same(TypeKey::Node)); +} + +#[test] +fn merge_fields_identical_branches() { + let mut branch1 = IndexMap::new(); + branch1.insert("name", TypeKey::String); + + let mut branch2 = IndexMap::new(); + branch2.insert("name", TypeKey::String); + + let merged = TypeTable::merge_fields(&[branch1, branch2]); + + assert_eq!(merged.len(), 1); + assert_eq!(merged["name"], MergedField::Same(TypeKey::String)); +} + +#[test] +fn merge_fields_missing_in_some_branches() { + let mut branch1 = IndexMap::new(); + branch1.insert("name", TypeKey::String); + branch1.insert("value", TypeKey::Node); + + let mut branch2 = IndexMap::new(); + branch2.insert("name", TypeKey::String); + // value missing + + let merged = TypeTable::merge_fields(&[branch1, branch2]); + + assert_eq!(merged.len(), 2); + assert_eq!(merged["name"], MergedField::Same(TypeKey::String)); + assert_eq!(merged["value"], MergedField::Optional(TypeKey::Node)); +} + +#[test] +fn merge_fields_disjoint_branches() { + let mut branch1 = IndexMap::new(); + branch1.insert("a", TypeKey::String); + + let mut branch2 = IndexMap::new(); + branch2.insert("b", TypeKey::Node); + + let merged = TypeTable::merge_fields(&[branch1, branch2]); + + assert_eq!(merged.len(), 2); + assert_eq!(merged["a"], MergedField::Optional(TypeKey::String)); + assert_eq!(merged["b"], MergedField::Optional(TypeKey::Node)); +} + +#[test] +fn merge_fields_type_conflict() { + let mut branch1 = IndexMap::new(); + branch1.insert("x", TypeKey::String); + + let mut branch2 = IndexMap::new(); + branch2.insert("x", TypeKey::Node); + + let merged = TypeTable::merge_fields(&[branch1, branch2]); + + assert_eq!(merged.len(), 1); + assert_eq!(merged["x"], MergedField::Conflict); +} + +#[test] +fn merge_fields_partial_conflict() { + // Three branches: x is String in branch 1 and 2, Node in branch 3 + let mut branch1 = IndexMap::new(); + branch1.insert("x", TypeKey::String); + + let mut branch2 = IndexMap::new(); + branch2.insert("x", TypeKey::String); + + let mut branch3 = IndexMap::new(); + branch3.insert("x", TypeKey::Node); + + let merged = TypeTable::merge_fields(&[branch1, branch2, branch3]); + + assert_eq!(merged["x"], MergedField::Conflict); +} + +#[test] +fn merge_fields_complex_scenario() { + // Branch 1: { name: String, value: Node } + // Branch 2: { name: String, extra: Node } + // Result: { name: String, value: Optional, extra: Optional } + let mut branch1 = IndexMap::new(); + branch1.insert("name", TypeKey::String); + branch1.insert("value", TypeKey::Node); + + let mut branch2 = IndexMap::new(); + branch2.insert("name", TypeKey::String); + branch2.insert("extra", TypeKey::Node); + + let merged = TypeTable::merge_fields(&[branch1, branch2]); + + assert_eq!(merged.len(), 3); + assert_eq!(merged["name"], MergedField::Same(TypeKey::String)); + assert_eq!(merged["value"], MergedField::Optional(TypeKey::Node)); + assert_eq!(merged["extra"], MergedField::Optional(TypeKey::Node)); +} + +#[test] +fn merge_fields_preserves_order() { + let mut branch1 = IndexMap::new(); + branch1.insert("z", TypeKey::String); + branch1.insert("a", TypeKey::String); + + let mut branch2 = IndexMap::new(); + branch2.insert("m", TypeKey::String); + + let merged = TypeTable::merge_fields(&[branch1, branch2]); + + let keys: Vec<_> = merged.keys().collect(); + // Order follows first occurrence across branches + assert_eq!(keys, vec![&"z", &"a", &"m"]); +} diff --git a/crates/plotnik-lib/src/infer/tyton.rs b/crates/plotnik-lib/src/infer/tyton.rs index 7bfa1f42..d5ed0242 100644 --- a/crates/plotnik-lib/src/infer/tyton.rs +++ b/crates/plotnik-lib/src/infer/tyton.rs @@ -10,18 +10,19 @@ //! //! ```text //! // ✗ Invalid: inline optional -//! Foo = { Node? @maybe } +//! Foo = { #Node? @maybe } //! //! // ✓ Valid: separate definition + reference -//! MaybeNode = Node? +//! MaybeNode = #Node? //! Foo = { MaybeNode @maybe } //! ``` //! //! # Syntax //! //! Keys: -//! - `Node` — built-in node type -//! - `string` — built-in string type +//! - `#Node` — built-in node type +//! - `#string` — built-in string type +//! - `#Invalid` — built-in invalid type //! - `()` — built-in unit type //! - `PascalName` — named type //! - `` — synthetic key from path segments @@ -32,19 +33,19 @@ //! - `Key?` — optional wrapper //! - `Key*` — list wrapper //! - `Key+` — non-empty list wrapper -//! - `Node` / `string` / `()` — bare builtin alias +//! - `#Node` / `#string` / `()` — bare builtin alias //! //! Definitions: //! - `Name = { ... }` — define a struct //! - `Name = [ ... ]` — define a tagged union //! - `Name = Other?` — define an optional //! - ` = { ... }` — define with synthetic key -//! - `AliasNode = Node` — alias to builtin +//! - `AliasNode = #Node` — alias to builtin //! //! # Example //! //! ```text -//! FuncInfo = { string @name Node @body } +//! FuncInfo = { #string @name #Node @body } //! Stmt = [ Assign: AssignStmt Call: CallStmt ] //! Stmts = Stmt* //! ``` @@ -59,13 +60,16 @@ use super::{TypeKey, TypeTable, TypeValue}; #[derive(Logos, Debug, Clone, PartialEq)] #[logos(skip r"[ \t\n\r]+")] enum Token<'src> { - // Built-in type keywords - #[token("Node")] + // Built-in type keywords (prefixed with #) + #[token("#Node")] Node, - #[token("string")] + #[token("#string")] String, + #[token("#Invalid")] + Invalid, + #[token("()")] Unit, @@ -203,6 +207,10 @@ impl<'src> Parser<'src> { self.advance(); Ok(TypeKey::String) } + Some(Token::Invalid) => { + self.advance(); + Ok(TypeKey::Invalid) + } Some(Token::Unit) => { self.advance(); Ok(TypeKey::Unit) @@ -273,6 +281,10 @@ impl<'src> Parser<'src> { self.advance(); self.parse_wrapper_or_bare(TypeKey::String, TypeValue::String) } + Some(Token::Invalid) => { + self.advance(); + self.parse_wrapper_or_bare(TypeKey::Invalid, TypeValue::Invalid) + } Some(Token::Unit) => { self.advance(); self.parse_wrapper_or_bare(TypeKey::Unit, TypeValue::Unit) @@ -451,13 +463,17 @@ pub fn emit(table: &TypeTable<'_>) -> String { } fn is_builtin(key: &TypeKey<'_>) -> bool { - matches!(key, TypeKey::Node | TypeKey::String | TypeKey::Unit) + matches!( + key, + TypeKey::Node | TypeKey::String | TypeKey::Unit | TypeKey::Invalid + ) } fn emit_key(out: &mut String, key: &TypeKey<'_>) { match key { - TypeKey::Node => out.push_str("Node"), - TypeKey::String => out.push_str("string"), + TypeKey::Node => out.push_str("#Node"), + TypeKey::String => out.push_str("#string"), + TypeKey::Invalid => out.push_str("#Invalid"), TypeKey::Unit => out.push_str("()"), TypeKey::Named(name) => out.push_str(name), TypeKey::Synthetic(segments) => { @@ -475,8 +491,9 @@ fn emit_key(out: &mut String, key: &TypeKey<'_>) { fn emit_value(out: &mut String, value: &TypeValue<'_>) { match value { - TypeValue::Node => out.push_str("Node"), - TypeValue::String => out.push_str("string"), + TypeValue::Node => out.push_str("#Node"), + TypeValue::String => out.push_str("#string"), + TypeValue::Invalid => out.push_str("#Invalid"), TypeValue::Unit => out.push_str("()"), TypeValue::Struct(fields) => { out.push_str("{ "); diff --git a/crates/plotnik-lib/src/infer/tyton_tests.rs b/crates/plotnik-lib/src/infer/tyton_tests.rs index 3b796a29..c948f295 100644 --- a/crates/plotnik-lib/src/infer/tyton_tests.rs +++ b/crates/plotnik-lib/src/infer/tyton_tests.rs @@ -20,27 +20,30 @@ fn parse_empty() { Node = Node String = String Unit = Unit + Invalid = Invalid "); } #[test] fn parse_struct_simple() { - let input = "Foo = { Node @name }"; + let input = "Foo = { #Node @name }"; insta::assert_snapshot!(dump_table(input), @r#" Node = Node String = String Unit = Unit + Invalid = Invalid Named("Foo") = Struct({"name": Node}) "#); } #[test] fn parse_struct_multiple_fields() { - let input = "Func = { string @name Node @body Node @params }"; + let input = "Func = { #string @name #Node @body #Node @params }"; insta::assert_snapshot!(dump_table(input), @r#" Node = Node String = String Unit = Unit + Invalid = Invalid Named("Func") = Struct({"name": String, "body": Node, "params": Node}) "#); } @@ -52,6 +55,7 @@ fn parse_struct_empty() { Node = Node String = String Unit = Unit + Invalid = Invalid Named("Empty") = Struct({}) "#); } @@ -63,6 +67,7 @@ fn parse_struct_with_unit() { Node = Node String = String Unit = Unit + Invalid = Invalid Named("Wrapper") = Struct({"unit": Unit}) "#); } @@ -74,6 +79,7 @@ fn parse_tagged_union() { Node = Node String = String Unit = Unit + Invalid = Invalid Named("Stmt") = TaggedUnion({"Assign": Named("AssignStmt"), "Call": Named("CallStmt")}) "#); } @@ -85,50 +91,55 @@ fn parse_tagged_union_single() { Node = Node String = String Unit = Unit + Invalid = Invalid Named("Single") = TaggedUnion({"Only": Named("OnlyVariant")}) "#); } #[test] fn parse_tagged_union_with_builtins() { - let input = "Mixed = [ Text: string Code: Node Empty: () ]"; + let input = "Mixed = [ Text: #string Code: #Node Empty: () ]"; insta::assert_snapshot!(dump_table(input), @r#" Node = Node String = String Unit = Unit + Invalid = Invalid Named("Mixed") = TaggedUnion({"Text": String, "Code": Node, "Empty": Unit}) "#); } #[test] fn parse_optional() { - let input = "MaybeNode = Node?"; + let input = "MaybeNode = #Node?"; insta::assert_snapshot!(dump_table(input), @r#" Node = Node String = String Unit = Unit + Invalid = Invalid Named("MaybeNode") = Optional(Node) "#); } #[test] fn parse_list() { - let input = "Nodes = Node*"; + let input = "Nodes = #Node*"; insta::assert_snapshot!(dump_table(input), @r#" Node = Node String = String Unit = Unit + Invalid = Invalid Named("Nodes") = List(Node) "#); } #[test] fn parse_non_empty_list() { - let input = "Nodes = Node+"; + let input = "Nodes = #Node+"; insta::assert_snapshot!(dump_table(input), @r#" Node = Node String = String Unit = Unit + Invalid = Invalid Named("Nodes") = NonEmptyList(Node) "#); } @@ -140,6 +151,7 @@ fn parse_optional_named() { Node = Node String = String Unit = Unit + Invalid = Invalid Named("MaybeStmt") = Optional(Named("Stmt")) "#); } @@ -151,6 +163,7 @@ fn parse_list_named() { Node = Node String = String Unit = Unit + Invalid = Invalid Named("Stmts") = List(Named("Stmt")) "#); } @@ -162,6 +175,7 @@ fn parse_synthetic_key_simple() { Node = Node String = String Unit = Unit + Invalid = Invalid Named("Wrapper") = Optional(Synthetic(["Foo", "bar"])) "#); } @@ -173,6 +187,7 @@ fn parse_synthetic_key_multiple_segments() { Node = Node String = String Unit = Unit + Invalid = Invalid Named("Wrapper") = List(Synthetic(["Foo", "bar", "baz"])) "#); } @@ -184,6 +199,7 @@ fn parse_struct_with_synthetic() { Node = Node String = String Unit = Unit + Invalid = Invalid Named("Container") = Struct({"inner": Synthetic(["Inner", "field"])}) "#); } @@ -195,6 +211,7 @@ fn parse_union_with_synthetic() { Node = Node String = String Unit = Unit + Invalid = Invalid Named("Choice") = TaggedUnion({"First": Synthetic(["Choice", "first"]), "Second": Synthetic(["Choice", "second"])}) "#); } @@ -202,8 +219,8 @@ fn parse_union_with_synthetic() { #[test] fn parse_multiple_definitions() { let input = indoc! {r#" - AssignStmt = { Node @target Node @value } - CallStmt = { Node @func Node @args } + AssignStmt = { #Node @target #Node @value } + CallStmt = { #Node @func #Node @args } Stmt = [ Assign: AssignStmt Call: CallStmt ] Stmts = Stmt* "#}; @@ -211,6 +228,7 @@ fn parse_multiple_definitions() { Node = Node String = String Unit = Unit + Invalid = Invalid Named("AssignStmt") = Struct({"target": Node, "value": Node}) Named("CallStmt") = Struct({"func": Node, "args": Node}) Named("Stmt") = TaggedUnion({"Assign": Named("AssignStmt"), "Call": Named("CallStmt")}) @@ -221,11 +239,11 @@ fn parse_multiple_definitions() { #[test] fn parse_complex_example() { let input = indoc! {r#" - FuncInfo = { string @name Node @body } - Param = { string @name string @type_annotation } + FuncInfo = { #string @name #Node @body } + Param = { #string @name #string @type_annotation } Params = Param* FuncDecl = { FuncInfo @info Params @params } - Stmt = [ Func: FuncDecl Expr: Node ] + Stmt = [ Func: FuncDecl Expr: #Node ] MaybeStmt = Stmt? Program = { Stmt @statements } "#}; @@ -233,6 +251,7 @@ fn parse_complex_example() { Node = Node String = String Unit = Unit + Invalid = Invalid Named("FuncInfo") = Struct({"name": String, "body": Node}) Named("Param") = Struct({"name": String, "type_annotation": String}) Named("Params") = List(Named("Param")) @@ -246,15 +265,16 @@ fn parse_complex_example() { #[test] fn parse_all_builtins() { let input = indoc! {r#" - AllBuiltins = { Node @node string @str () @unit } - OptNode = Node? - ListStr = string* + AllBuiltins = { #Node @node #string @str () @unit } + OptNode = #Node? + ListStr = #string* NonEmptyUnit = ()+ "#}; insta::assert_snapshot!(dump_table(input), @r#" Node = Node String = String Unit = Unit + Invalid = Invalid Named("AllBuiltins") = Struct({"node": Node, "str": String, "unit": Unit}) Named("OptNode") = Optional(Node) Named("ListStr") = List(String) @@ -262,16 +282,40 @@ fn parse_all_builtins() { "#); } +#[test] +fn parse_invalid_builtin() { + let input = "HasInvalid = { #Invalid @bad }"; + insta::assert_snapshot!(dump_table(input), @r#" + Node = Node + String = String + Unit = Unit + Invalid = Invalid + Named("HasInvalid") = Struct({"bad": Invalid}) + "#); +} + +#[test] +fn parse_invalid_wrapper() { + let input = "MaybeInvalid = #Invalid?"; + insta::assert_snapshot!(dump_table(input), @r#" + Node = Node + String = String + Unit = Unit + Invalid = Invalid + Named("MaybeInvalid") = Optional(Invalid) + "#); +} + #[test] fn error_missing_eq() { - let input = "Foo { Node @x }"; + let input = "Foo { #Node @x }"; insta::assert_snapshot!(dump_table(input), @"ERROR: expected Eq, got LBrace at 4..5"); } #[test] fn error_missing_at() { - let input = "Foo = { Node name }"; - insta::assert_snapshot!(dump_table(input), @r#"ERROR: expected At, got LowerIdent("name") at 13..17"#); + let input = "Foo = { #Node name }"; + insta::assert_snapshot!(dump_table(input), @r#"ERROR: expected At, got LowerIdent("name") at 14..18"#); } #[test] @@ -288,8 +332,8 @@ fn error_empty_synthetic() { #[test] fn error_unclosed_brace() { - let input = "Foo = { Node @x"; - insta::assert_snapshot!(dump_table(input), @"ERROR: expected type key at 15..15"); + let input = "Foo = { #Node @x"; + insta::assert_snapshot!(dump_table(input), @"ERROR: expected type key at 16..16"); } #[test] @@ -300,34 +344,36 @@ fn error_unclosed_bracket() { #[test] fn error_lowercase_type_name() { - let input = "foo = { Node @x }"; + let input = "foo = { #Node @x }"; insta::assert_snapshot!(dump_table(input), @"ERROR: expected type name (uppercase) or synthetic key at 0..3"); } #[test] fn error_uppercase_field_name() { - let input = "Foo = { Node @Name }"; - insta::assert_snapshot!(dump_table(input), @"ERROR: expected field name (lowercase) at 14..18"); + let input = "Foo = { #Node @Name }"; + insta::assert_snapshot!(dump_table(input), @"ERROR: expected field name (lowercase) at 15..19"); } #[test] fn parse_bare_builtin_alias_node() { - let input = "AliasNode = Node"; + let input = "AliasNode = #Node"; insta::assert_snapshot!(dump_table(input), @r#" Node = Node String = String Unit = Unit + Invalid = Invalid Named("AliasNode") = Node "#); } #[test] fn parse_bare_builtin_alias_string() { - let input = "AliasString = string"; + let input = "AliasString = #string"; insta::assert_snapshot!(dump_table(input), @r#" Node = Node String = String Unit = Unit + Invalid = Invalid Named("AliasString") = String "#); } @@ -339,53 +385,69 @@ fn parse_bare_builtin_alias_unit() { Node = Node String = String Unit = Unit + Invalid = Invalid Named("AliasUnit") = Unit "#); } +#[test] +fn parse_bare_builtin_alias_invalid() { + let input = "AliasInvalid = #Invalid"; + insta::assert_snapshot!(dump_table(input), @r#" + Node = Node + String = String + Unit = Unit + Invalid = Invalid + Named("AliasInvalid") = Invalid + "#); +} + #[test] fn parse_synthetic_definition_struct() { - let input = " = { Node @value }"; + let input = " = { #Node @value }"; insta::assert_snapshot!(dump_table(input), @r#" Node = Node String = String Unit = Unit + Invalid = Invalid Synthetic(["Foo", "bar"]) = Struct({"value": Node}) "#); } #[test] fn parse_synthetic_definition_union() { - let input = " = [ A: Node B: string ]"; + let input = " = [ A: #Node B: #string ]"; insta::assert_snapshot!(dump_table(input), @r#" Node = Node String = String Unit = Unit + Invalid = Invalid Synthetic(["Choice", "first"]) = TaggedUnion({"A": Node, "B": String}) "#); } #[test] fn parse_synthetic_definition_wrapper() { - let input = " = Node?"; + let input = " = #Node?"; insta::assert_snapshot!(dump_table(input), @r#" Node = Node String = String Unit = Unit + Invalid = Invalid Synthetic(["Inner", "nested"]) = Optional(Node) "#); } #[test] fn error_invalid_char() { - let input = "Foo = { Node @x $ }"; - insta::assert_snapshot!(dump_table(input), @r#"ERROR: unexpected character: "$" at 16..17"#); + let input = "Foo = { #Node @x $ }"; + insta::assert_snapshot!(dump_table(input), @r#"ERROR: unexpected character: "$" at 17..18"#); } #[test] fn error_eof_in_struct() { - let input = "Foo = { Node @x"; - insta::assert_snapshot!(dump_table(input), @"ERROR: expected type key at 15..15"); + let input = "Foo = { #Node @x"; + insta::assert_snapshot!(dump_table(input), @"ERROR: expected type key at 16..16"); } #[test] @@ -406,6 +468,24 @@ fn error_invalid_type_value() { insta::assert_snapshot!(dump_table(input), @"ERROR: expected type value at 6..7"); } +#[test] +fn error_unprefixed_node() { + let input = "Foo = { Node @x }"; + insta::assert_snapshot!(dump_table(input), @r#" + Node = Node + String = String + Unit = Unit + Invalid = Invalid + Named("Foo") = Struct({"x": Named("Node")}) + "#); +} + +#[test] +fn error_unprefixed_string() { + let input = "Foo = string"; + insta::assert_snapshot!(dump_table(input), @"ERROR: expected type value at 6..12"); +} + // === emit tests === #[test] @@ -416,14 +496,14 @@ fn emit_empty() { #[test] fn emit_struct_simple() { - let table = parse("Foo = { Node @name }").unwrap(); - insta::assert_snapshot!(emit(&table), @"Foo = { Node @name }"); + let table = parse("Foo = { #Node @name }").unwrap(); + insta::assert_snapshot!(emit(&table), @"Foo = { #Node @name }"); } #[test] fn emit_struct_multiple_fields() { - let table = parse("Func = { string @name Node @body Node @params }").unwrap(); - insta::assert_snapshot!(emit(&table), @"Func = { string @name Node @body Node @params }"); + let table = parse("Func = { #string @name #Node @body #Node @params }").unwrap(); + insta::assert_snapshot!(emit(&table), @"Func = { #string @name #Node @body #Node @params }"); } #[test] @@ -440,26 +520,26 @@ fn emit_tagged_union() { #[test] fn emit_optional() { - let table = parse("MaybeNode = Node?").unwrap(); - insta::assert_snapshot!(emit(&table), @"MaybeNode = Node?"); + let table = parse("MaybeNode = #Node?").unwrap(); + insta::assert_snapshot!(emit(&table), @"MaybeNode = #Node?"); } #[test] fn emit_list() { - let table = parse("Nodes = Node*").unwrap(); - insta::assert_snapshot!(emit(&table), @"Nodes = Node*"); + let table = parse("Nodes = #Node*").unwrap(); + insta::assert_snapshot!(emit(&table), @"Nodes = #Node*"); } #[test] fn emit_non_empty_list() { - let table = parse("Nodes = Node+").unwrap(); - insta::assert_snapshot!(emit(&table), @"Nodes = Node+"); + let table = parse("Nodes = #Node+").unwrap(); + insta::assert_snapshot!(emit(&table), @"Nodes = #Node+"); } #[test] fn emit_synthetic_key() { - let table = parse(" = { Node @value }").unwrap(); - insta::assert_snapshot!(emit(&table), @" = { Node @value }"); + let table = parse(" = { #Node @value }").unwrap(); + insta::assert_snapshot!(emit(&table), @" = { #Node @value }"); } #[test] @@ -471,14 +551,14 @@ fn emit_synthetic_in_wrapper() { #[test] fn emit_bare_builtins() { let input = indoc! {r#" - AliasNode = Node - AliasString = string + AliasNode = #Node + AliasString = #string AliasUnit = () "#}; let table = parse(input).unwrap(); insta::assert_snapshot!(emit(&table), @r" - AliasNode = Node - AliasString = string + AliasNode = #Node + AliasString = #string AliasUnit = () "); } @@ -486,15 +566,15 @@ fn emit_bare_builtins() { #[test] fn emit_multiple_definitions() { let input = indoc! {r#" - AssignStmt = { Node @target Node @value } - CallStmt = { Node @func Node @args } + AssignStmt = { #Node @target #Node @value } + CallStmt = { #Node @func #Node @args } Stmt = [ Assign: AssignStmt Call: CallStmt ] Stmts = Stmt* "#}; let table = parse(input).unwrap(); insta::assert_snapshot!(emit(&table), @r" - AssignStmt = { Node @target Node @value } - CallStmt = { Node @func Node @args } + AssignStmt = { #Node @target #Node @value } + CallStmt = { #Node @func #Node @args } Stmt = [ Assign: AssignStmt Call: CallStmt ] Stmts = Stmt* "); @@ -503,11 +583,11 @@ fn emit_multiple_definitions() { #[test] fn emit_roundtrip() { let input = indoc! {r#" - FuncInfo = { string @name Node @body } - Param = { string @name string @type_annotation } + FuncInfo = { #string @name #Node @body } + Param = { #string @name #string @type_annotation } Params = Param* FuncDecl = { FuncInfo @info Params @params } - Stmt = [ Func: FuncDecl Expr: Node ] + Stmt = [ Func: FuncDecl Expr: #Node ] MaybeStmt = Stmt? "#};