diff --git a/crates/plotnik-lib/src/analyze/link_tests.rs b/crates/plotnik-lib/src/analyze/link_tests.rs index 7b98cce9..5e555aa4 100644 --- a/crates/plotnik-lib/src/analyze/link_tests.rs +++ b/crates/plotnik-lib/src/analyze/link_tests.rs @@ -1,9 +1,5 @@ use crate::Query; use indoc::indoc; -use plotnik_langs::Lang; -use std::sync::LazyLock; - -static LANG: LazyLock = LazyLock::new(|| plotnik_langs::javascript()); #[test] fn valid_query_with_field() { @@ -11,7 +7,7 @@ fn valid_query_with_field() { Q = (function_declaration name: (identifier) @name) @fn "#}; - Query::expect_valid_linking(input, &LANG); + Query::expect_valid_linking(input); } #[test] @@ -20,7 +16,7 @@ fn unknown_node_type_with_suggestion() { Q = (function_declaraton) @fn "#}; - let res = Query::expect_invalid_linking(input, &LANG); + let res = Query::expect_invalid_linking(input); insta::assert_snapshot!(res, @r" error: `function_declaraton` is not a valid node type @@ -38,7 +34,7 @@ fn unknown_node_type_no_suggestion() { Q = (xyzzy_foobar_baz) @fn "#}; - let res = Query::expect_invalid_linking(input, &LANG); + let res = Query::expect_invalid_linking(input); insta::assert_snapshot!(res, @r" error: `xyzzy_foobar_baz` is not a valid node type @@ -55,7 +51,7 @@ fn unknown_field_with_suggestion() { nme: (identifier) @name) @fn "#}; - let res = Query::expect_invalid_linking(input, &LANG); + let res = Query::expect_invalid_linking(input); insta::assert_snapshot!(res, @r" error: `nme` is not a valid field @@ -74,7 +70,7 @@ fn unknown_field_no_suggestion() { xyzzy: (identifier) @name) @fn "#}; - let res = Query::expect_invalid_linking(input, &LANG); + let res = Query::expect_invalid_linking(input); insta::assert_snapshot!(res, @r" error: `xyzzy` is not a valid field @@ -91,7 +87,7 @@ fn field_not_on_node_type() { condition: (identifier) @name) @fn "#}; - let res = Query::expect_invalid_linking(input, &LANG); + let res = Query::expect_invalid_linking(input); insta::assert_snapshot!(res, @r" error: field `condition` is not valid on this node type @@ -113,7 +109,7 @@ fn field_not_on_node_type_with_suggestion() { ) @fn "#}; - let res = Query::expect_invalid_linking(input, &LANG); + let res = Query::expect_invalid_linking(input); insta::assert_snapshot!(res, @r" error: field `parameter` is not valid on this node type @@ -134,7 +130,7 @@ fn negated_field_unknown() { Q = (function_declaration !nme) @fn "#}; - let res = Query::expect_invalid_linking(input, &LANG); + let res = Query::expect_invalid_linking(input); insta::assert_snapshot!(res, @r" error: `nme` is not a valid field @@ -152,7 +148,7 @@ fn negated_field_not_on_node_type() { Q = (function_declaration !condition) @fn "#}; - let res = Query::expect_invalid_linking(input, &LANG); + let res = Query::expect_invalid_linking(input); insta::assert_snapshot!(res, @r" error: field `condition` is not valid on this node type @@ -172,7 +168,7 @@ fn negated_field_not_on_node_type_with_suggestion() { Q = (function_declaration !parameter) @fn "#}; - let res = Query::expect_invalid_linking(input, &LANG); + let res = Query::expect_invalid_linking(input); insta::assert_snapshot!(res, @r" error: field `parameter` is not valid on this node type @@ -193,7 +189,7 @@ fn negated_field_valid() { Q = (function_declaration !name) @fn "#}; - Query::expect_valid_linking(input, &LANG); + Query::expect_valid_linking(input); } #[test] @@ -202,7 +198,7 @@ fn anonymous_node_unknown() { Q = (function_declaration "xyzzy_fake_token") @fn "#}; - let res = Query::expect_invalid_linking(input, &LANG); + let res = Query::expect_invalid_linking(input); insta::assert_snapshot!(res, @r#" error: `xyzzy_fake_token` is not a valid node type @@ -217,7 +213,7 @@ fn error_nodes_skip_validation() { let input = indoc! {r#" Q = (ERROR) @err "#}; - Query::expect_valid_linking(input, &LANG); + Query::expect_valid_linking(input); } #[test] @@ -225,7 +221,7 @@ fn missing_nodes_skip_validation() { let input = indoc! {r#" Q = (MISSING) @miss "#}; - Query::expect_valid_linking(input, &LANG); + Query::expect_valid_linking(input); } #[test] @@ -235,7 +231,7 @@ fn multiple_errors_in_query() { nme: (identifer) @name) @fn "#}; - let res = Query::expect_invalid_linking(input, &LANG); + let res = Query::expect_invalid_linking(input); insta::assert_snapshot!(res, @r" error: `function_declaraton` is not a valid node type @@ -269,7 +265,7 @@ fn nested_field_validation() { (return_statement) @ret) @body) @fn "#}; - Query::expect_valid_linking(input, &LANG); + Query::expect_valid_linking(input); } #[test] @@ -279,7 +275,7 @@ fn alternation_with_link_errors() { (class_declaraton)] @decl "#}; - let res = Query::expect_invalid_linking(input, &LANG); + let res = Query::expect_invalid_linking(input); insta::assert_snapshot!(res, @r" error: `function_declaraton` is not a valid node type @@ -305,7 +301,7 @@ fn quantified_expr_validation() { (function_declaration)+ @fns) @block "#}; - Query::expect_valid_linking(input, &LANG); + Query::expect_valid_linking(input); } #[test] @@ -314,7 +310,7 @@ fn wildcard_node_skips_validation() { Q = (_) @any "#}; - Query::expect_valid_linking(input, &LANG); + Query::expect_valid_linking(input); } #[test] @@ -325,7 +321,7 @@ fn def_reference_with_link() { Q = (program (Func)+ @funcs) "#}; - Query::expect_valid_linking(input, &LANG); + Query::expect_valid_linking(input); } #[test] @@ -335,7 +331,7 @@ fn field_on_node_without_fields() { name: (identifier) @inner) @id "#}; - let res = Query::expect_invalid_linking(input, &LANG); + let res = Query::expect_invalid_linking(input); insta::assert_snapshot!(res, @r" error: field `name` is not valid on this node type @@ -356,7 +352,7 @@ fn valid_child_via_supertype() { (function_declaration)) @block "#}; - Query::expect_valid_linking(input, &LANG); + Query::expect_valid_linking(input); } #[test] @@ -366,7 +362,7 @@ fn valid_child_via_nested_supertype() { (function_declaration)) @prog "#}; - Query::expect_valid_linking(input, &LANG); + Query::expect_valid_linking(input); } #[test] @@ -375,7 +371,7 @@ fn deeply_nested_sequences_valid() { Q = (statement_block {{{(function_declaration)}}}) @block "#}; - Query::expect_valid_linking(input, &LANG); + Query::expect_valid_linking(input); } #[test] @@ -384,7 +380,7 @@ fn deeply_nested_alternations_in_field_valid() { Q = (function_declaration name: [[[(identifier)]]]) @fn "#}; - Query::expect_valid_linking(input, &LANG); + Query::expect_valid_linking(input); } #[test] @@ -394,5 +390,5 @@ fn ref_followed_valid_case() { Q = (function_declaration name: (Foo)) "#}; - Query::expect_valid_linking(input, &LANG); + Query::expect_valid_linking(input); } diff --git a/crates/plotnik-lib/src/bytecode/dump_tests.rs b/crates/plotnik-lib/src/bytecode/dump_tests.rs index 705b5e18..4069196a 100644 --- a/crates/plotnik-lib/src/bytecode/dump_tests.rs +++ b/crates/plotnik-lib/src/bytecode/dump_tests.rs @@ -53,22 +53,60 @@ fn dump_multiple_entrypoints() { let res = Query::expect_valid_linked_bytecode(input); - // Verify key sections exist - assert!(res.contains("[header]")); - assert!(res.contains("[strings]")); - assert!(res.contains("[types.defs]")); - assert!(res.contains("[types.members]")); - assert!(res.contains("[types.names]")); - assert!(res.contains("[entry]")); - assert!(res.contains("[code]")); - - // Verify both entrypoints appear - assert!(res.contains("Expression")); - assert!(res.contains("Root")); - - // Verify code section has entrypoint labels - assert!(res.contains("Expression:")); - assert!(res.contains("Root:")); + insta::assert_snapshot!(res, @r#" + [header] + linked = true + + [strings] + S00 "Beauty will save the world" + S01 "name" + S02 "value" + S03 "Expression" + S04 "Root" + S05 "identifier" + S06 "number" + S07 "function_declaration" + + [types.defs] + T00 = void + T01 = Node + T02 = str + T03 = Struct(M0, 2) ; { name, value } + T04 = Struct(M2, 1) ; { name } + T05 = Optional(T01) ; Node? + + [types.members] + M0 = (S01, T05) ; name: T05 + M1 = (S02, T05) ; value: T05 + M2 = (S01, T01) ; name: Node + + [types.names] + N0 = (S03, T03) ; Expression + N1 = (S04, T04) ; Root + + [entry] + Expression = 01 :: T03 + Root = 04 :: T04 + + [code] + 00 ๐œ€ โ—ผ + + Expression: + 01 ๐œ€ 02 + 02 ๐œ€ 13, 17 + + Root: + 04 ๐œ€ 05 + 05 (function_declaration) 06 + 06 โ†“* name: (identifier) [Node Set(M2)]08 + 08 *โ†‘ยน 09 + 09 โ–ถ + 10 โ–ถ + 11 (identifier) [Node Set(M0)] 10 + 13 ๐œ€ [Null Set(M1)] 11 + 15 (number) [Node Set(M1)] 10 + 17 ๐œ€ [Null Set(M0)] 15 + "#); } #[test] @@ -81,9 +119,44 @@ fn dump_with_field_constraints() { let res = Query::expect_valid_linked_bytecode(input); - // Should have field references in code section - assert!(res.contains("left:")); - assert!(res.contains("right:")); + insta::assert_snapshot!(res, @r#" + [header] + linked = true + + [strings] + S00 "Beauty will save the world" + S01 "left" + S02 "right" + S03 "Test" + S04 "binary_expression" + + [types.defs] + T00 = void + T01 = Node + T02 = str + T03 = Struct(M0, 2) ; { left, right } + + [types.members] + M0 = (S01, T01) ; left: Node + M1 = (S02, T01) ; right: Node + + [types.names] + N0 = (S03, T03) ; Test + + [entry] + Test = 01 :: T03 + + [code] + 00 ๐œ€ โ—ผ + + Test: + 01 ๐œ€ 02 + 02 (binary_expression) 03 + 03 โ†“* left: _ [Node Set(M0)] 05 + 05 * right: _ [Node Set(M1)] 07 + 07 *โ†‘ยน 08 + 08 โ–ถ + "#); } #[test] @@ -92,8 +165,45 @@ fn dump_with_quantifier() { let res = Query::expect_valid_linked_bytecode(input); - // Should have array type - assert!(res.contains("Array") || res.contains("[]")); + insta::assert_snapshot!(res, @r#" + [header] + linked = true + + [strings] + S00 "Beauty will save the world" + S01 "items" + S02 "Test" + S03 "identifier" + + [types.defs] + T00 = void + T01 = Node + T02 = str + T03 = ArrayStar(T01) ; Node* + T04 = Struct(M0, 1) ; { items } + + [types.members] + M0 = (S01, T03) ; items: T03 + + [types.names] + N0 = (S02, T04) ; Test + + [entry] + Test = 01 :: T04 + + [code] + 00 ๐œ€ โ—ผ + + Test: + 01 ๐œ€ 02 + 02 ๐œ€ [Arr] 04 + 04 ๐œ€ 09, 07 + 06 โ–ถ + 07 ๐œ€ [EndArr Set(M0)] 06 + 09 (identifier) [Push] 13 + 11 (identifier) [Push] 13 + 13 ๐œ€ 11, 07 + "#); } #[test] @@ -102,8 +212,47 @@ fn dump_with_alternation() { let res = Query::expect_valid_linked_bytecode(input); - // Should have code section with branching - assert!(res.contains("[code]")); + insta::assert_snapshot!(res, @r#" + [header] + linked = true + + [strings] + S00 "Beauty will save the world" + S01 "id" + S02 "str" + S03 "Test" + S04 "identifier" + S05 "string" + + [types.defs] + T00 = void + T01 = Node + T02 = str + T03 = Struct(M0, 2) ; { id, str } + T04 = Optional(T01) ; Node? + + [types.members] + M0 = (S01, T04) ; id: T04 + M1 = (S02, T04) ; str: T04 + + [types.names] + N0 = (S03, T03) ; Test + + [entry] + Test = 01 :: T03 + + [code] + 00 ๐œ€ โ—ผ + + Test: + 01 ๐œ€ 02 + 02 ๐œ€ 07, 11 + 04 โ–ถ + 05 (identifier) [Node Set(M0)] 04 + 07 ๐œ€ [Null Set(M1)] 05 + 09 (string) [Node Set(M1)] 04 + 11 ๐œ€ [Null Set(M0)] 09 + "#); } #[test] @@ -596,17 +745,45 @@ fn regression_alternation_capture_node_effect() { let res = Query::expect_valid_bytecode(input); - // Both branches must have [Node Set(M0)], not just [Set(M0)] - assert!( - res.contains("[Node Set(M0)]"), - "Missing Node effect on alternation capture" - ); - // Should appear twice (once per branch) - assert_eq!( - res.matches("[Node Set(M0)]").count(), - 2, - "Node effect should appear on both alternation branches" - ); + insta::assert_snapshot!(res, @r#" + [header] + linked = false + + [strings] + S00 "Beauty will save the world" + S01 "x" + S02 "Q" + S03 "program" + S04 "identifier" + S05 "number" + + [types.defs] + T00 = void + T01 = Node + T02 = str + T03 = Struct(M0, 1) ; { x } + + [types.members] + M0 = (S01, T01) ; x: Node + + [types.names] + N0 = (S02, T03) ; Q + + [entry] + Q = 01 :: T03 + + [code] + 00 ๐œ€ โ—ผ + + Q: + 01 ๐œ€ 02 + 02 (program) 03 + 03 ๐œ€ 06, 08 + 05 โ–ถ + 06 โ†“* (identifier) [Node Set(M0)] 10 + 08 โ†“* (number) [Node Set(M0)] 10 + 10 *โ†‘ยน 05 + "#); } /// Regression test: optional first-child skip path needs Down navigation. @@ -620,17 +797,50 @@ fn regression_optional_first_child_skip_navigation() { let res = Query::expect_valid_bytecode(input); - // Should have TWO versions of (number): - // 1. With โ†“* (Down) for skip path - // 2. With * (Next) for after-match path - assert!( - res.contains("โ†“* (number)"), - "Skip path should have Down navigation for (number)" - ); - assert!( - res.contains("* (number)"), - "Match path should have Next navigation for (number)" - ); + insta::assert_snapshot!(res, @r#" + [header] + linked = false + + [strings] + S00 "Beauty will save the world" + S01 "id" + S02 "n" + S03 "Q" + S04 "program" + S05 "number" + S06 "identifier" + + [types.defs] + T00 = void + T01 = Node + T02 = str + T03 = Struct(M0, 2) ; { id, n } + T04 = Optional(T01) ; Node? + + [types.members] + M0 = (S01, T04) ; id: T04 + M1 = (S02, T01) ; n: Node + + [types.names] + N0 = (S03, T03) ; Q + + [entry] + Q = 01 :: T03 + + [code] + 00 ๐œ€ โ—ผ + + Q: + 01 ๐œ€ 02 + 02 (program) 03 + 03 ๐œ€ 12, 10 + 05 โ–ถ + 06 โ†“* (number) [Node Set(M1)] 14 + 08 * (number) [Node Set(M1)] 14 + 10 ๐œ€ [Null Set(M0)] 06 + 12 โ†“* (identifier) [Node Set(M0)] 08 + 14 *โ†‘ยน 05 + "#); } /// Regression test: optional skip path needs Null injection for captures. @@ -644,11 +854,45 @@ fn regression_optional_skip_null_injection() { let res = Query::expect_valid_bytecode(input); - // Skip path must have [Null Set(M0)] to set @dec to null - assert!( - res.contains("[Null Set(M0)]"), - "Skip path should inject Null for optional capture" - ); + insta::assert_snapshot!(res, @r#" + [header] + linked = false + + [strings] + S00 "Beauty will save the world" + S01 "dec" + S02 "Q" + S03 "function_declaration" + S04 "decorator" + + [types.defs] + T00 = void + T01 = Node + T02 = str + T03 = Struct(M0, 1) ; { dec } + T04 = Optional(T01) ; Node? + + [types.members] + M0 = (S01, T04) ; dec: T04 + + [types.names] + N0 = (S02, T03) ; Q + + [entry] + Q = 01 :: T03 + + [code] + 00 ๐œ€ โ—ผ + + Q: + 01 ๐œ€ 02 + 02 (function_declaration) 03 + 03 ๐œ€ 05, 09 + 05 โ†“* (decorator) [Node Set(M0)] 07 + 07 *โ†‘ยน 08 + 08 โ–ถ + 09 ๐œ€ [Null Set(M0)] 08 + "#); } /// Regression test: star first-child with array capture preserves Arr/EndArr. @@ -662,22 +906,54 @@ fn regression_star_first_child_array_capture() { let res = Query::expect_valid_bytecode(input); - // Must have Arr and EndArr for array semantics - assert!(res.contains("[Arr]"), "Array capture must have Arr effect"); - assert!( - res.contains("[EndArr Set(M0)]"), - "Array capture must have EndArr with Set" - ); - - // Must have TWO versions of (number) for skip/match paths - assert!( - res.contains("โ†“* (number)"), - "Skip path should have Down navigation" - ); - assert!( - res.contains("* (number)"), - "Match path should have Next navigation" - ); + insta::assert_snapshot!(res, @r#" + [header] + linked = false + + [strings] + S00 "Beauty will save the world" + S01 "ids" + S02 "n" + S03 "Q" + S04 "array" + S05 "number" + S06 "identifier" + + [types.defs] + T00 = void + T01 = Node + T02 = str + T03 = ArrayStar(T01) ; Node* + T04 = Struct(M0, 2) ; { ids, n } + + [types.members] + M0 = (S01, T03) ; ids: T03 + M1 = (S02, T01) ; n: Node + + [types.names] + N0 = (S03, T04) ; Q + + [entry] + Q = 01 :: T04 + + [code] + 00 ๐œ€ โ—ผ + + Q: + 01 ๐œ€ 02 + 02 (array) 03 + 03 ๐œ€ [Arr] 05 + 05 ๐œ€ 16, 14 + 07 โ–ถ + 08 โ†“* (number) [Node Set(M1)] 22 + 10 * (number) [Node Set(M1)] 22 + 12 ๐œ€ [EndArr Set(M0)] 10 + 14 ๐œ€ [EndArr Set(M0)] 08 + 16 โ†“* (identifier) [Push] 20 + 18 * (identifier) [Push] 20 + 20 ๐œ€ 18, 12 + 22 *โ†‘ยน 07 + "#); } /// Regression test: struct array with internal captures needs Obj/EndObj. @@ -691,18 +967,6 @@ fn regression_struct_array_internal_captures() { let res = Query::expect_valid_bytecode(input); - // Must have Obj/EndObj for struct boundaries - assert!(res.contains("[Obj]"), "Struct array must have Obj effect"); - assert!( - res.contains("[EndObj"), - "Struct array must have EndObj effect" - ); - - // Must have Set effects for internal captures - assert!(res.contains("Set(M0)"), "Must have Set effect for @a"); - assert!(res.contains("Set(M1)"), "Must have Set effect for @b"); - - // Full snapshot to verify correct structure insta::assert_snapshot!(res, @r#" [header] linked = false diff --git a/crates/plotnik-lib/src/query/query_tests.rs b/crates/plotnik-lib/src/query/query_tests.rs index bea5c1b6..12349dd9 100644 --- a/crates/plotnik-lib/src/query/query_tests.rs +++ b/crates/plotnik-lib/src/query/query_tests.rs @@ -10,6 +10,18 @@ fn javascript() -> Lang { from_name("javascript").expect("javascript lang") } +macro_rules! expect_invalid { + ($($name:literal: $content:literal),+ $(,)?) => {{ + let mut source_map = SourceMap::new(); + $(source_map.add_file($name, $content);)+ + let query = QueryBuilder::new(source_map).parse().unwrap().analyze(); + if query.is_valid() { + panic!("Expected invalid query, got valid"); + } + query.dump_diagnostics() + }}; +} + impl QueryAnalyzed { #[track_caller] fn parse_and_validate(src: &str) -> Self { @@ -61,8 +73,8 @@ impl QueryAnalyzed { } #[track_caller] - pub fn expect_valid_linking(src: &str, lang: &Lang) -> LinkedQuery { - let query = Self::parse_and_validate(src).link(lang); + pub fn expect_valid_linking(src: &str) -> LinkedQuery { + let query = Self::parse_and_validate(src).link(&javascript()); if !query.is_valid() { panic!( "Expected valid linking, got error:\n{}", @@ -73,8 +85,8 @@ impl QueryAnalyzed { } #[track_caller] - pub fn expect_invalid_linking(src: &str, lang: &Lang) -> String { - let query = Self::parse_and_validate(src).link(lang); + pub fn expect_invalid_linking(src: &str) -> String { + let query = Self::parse_and_validate(src).link(&javascript()); if query.is_valid() { panic!("Expected failed linking, got valid"); } @@ -169,15 +181,13 @@ impl QueryAnalyzed { #[test] fn invalid_three_way_mutual_recursion_across_files() { - let mut source_map = SourceMap::new(); - source_map.add_file("a.ptk", "A = (a (B))"); - source_map.add_file("b.ptk", "B = (b (C))"); - source_map.add_file("c.ptk", "C = (c (A))"); + let res = expect_invalid! { + "a.ptk": "A = (a (B))", + "b.ptk": "B = (b (C))", + "c.ptk": "C = (c (A))", + }; - let query = QueryBuilder::new(source_map).parse().unwrap().analyze(); - - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_diagnostics(), @r" + insta::assert_snapshot!(res, @r" error: infinite recursion: no escape path --> c.ptk:1:9 | @@ -203,14 +213,12 @@ fn invalid_three_way_mutual_recursion_across_files() { #[test] fn multifile_field_with_ref_to_seq_error() { - let mut source_map = SourceMap::new(); - source_map.add_file("defs.ptk", "X = {(a) (b)}"); - source_map.add_file("main.ptk", "Q = (call name: (X))"); - - let query = QueryBuilder::new(source_map).parse().unwrap().analyze(); + let res = expect_invalid! { + "defs.ptk": "X = {(a) (b)}", + "main.ptk": "Q = (call name: (X))", + }; - assert!(!query.is_valid()); - insta::assert_snapshot!(query.dump_diagnostics(), @r" + insta::assert_snapshot!(res, @r" error: field `name` cannot match a sequence --> main.ptk:1:17 |