From d86c9f2de31a3b80fbfbd9d4798e92a6cc305a2f Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Sat, 3 Jan 2026 09:15:22 -0300 Subject: [PATCH] feat: add runtime engine for query execution --- crates/plotnik-lib/src/compile/capture.rs | 15 + crates/plotnik-lib/src/compile/mod.rs | 50 +- crates/plotnik-lib/src/compile/quantifier.rs | 10 +- crates/plotnik-lib/src/compile/scope.rs | 9 +- ..._codegen_tests__alternations_captured.snap | 16 +- ...n_tests__alternations_captured_tagged.snap | 18 +- ...gen_tests__alternations_in_quantifier.snap | 54 +- ...ts__alternations_no_internal_captures.snap | 14 +- ...en_tests__alternations_null_injection.snap | 14 +- ...codegen_tests__alternations_unlabeled.snap | 14 +- ...__emit__codegen_tests__captures_basic.snap | 6 +- ...codegen_tests__captures_deeply_nested.snap | 16 +- ...mit__codegen_tests__captures_multiple.snap | 18 +- ...__codegen_tests__captures_nested_flat.snap | 14 +- ...sts__captures_optional_wrapper_struct.snap | 16 +- ..._codegen_tests__captures_struct_scope.snap | 10 +- ...egen_tests__captures_with_type_custom.snap | 6 +- ...egen_tests__captures_with_type_string.snap | 6 +- ...odegen_tests__captures_wrapper_struct.snap | 36 +- ...tests__comprehensive_multi_definition.snap | 44 +- ...__codegen_tests__definitions_multiple.snap | 22 +- ..._codegen_tests__definitions_reference.snap | 28 +- ...it__codegen_tests__definitions_single.snap | 6 +- ...it__codegen_tests__fields_alternation.snap | 20 +- ..._emit__codegen_tests__fields_multiple.snap | 18 +- ...__emit__codegen_tests__fields_negated.snap | 16 +- ...b__emit__codegen_tests__fields_single.snap | 16 +- ..._emit__codegen_tests__nodes_anonymous.snap | 16 +- ...lib__emit__codegen_tests__nodes_error.snap | 6 +- ...b__emit__codegen_tests__nodes_missing.snap | 6 +- ...lib__emit__codegen_tests__nodes_named.snap | 6 +- ...it__codegen_tests__nodes_wildcard_any.snap | 16 +- ...__codegen_tests__nodes_wildcard_named.snap | 16 +- ...__codegen_tests__optional_first_child.snap | 18 +- ...odegen_tests__optional_null_injection.snap | 14 +- ..._tests__quantifiers_first_child_array.snap | 26 +- ...__codegen_tests__quantifiers_optional.snap | 14 +- ...tests__quantifiers_optional_nongreedy.snap | 14 +- ...emit__codegen_tests__quantifiers_plus.snap | 14 +- ...gen_tests__quantifiers_plus_nongreedy.snap | 14 +- ..._tests__quantifiers_repeat_navigation.snap | 22 +- ...emit__codegen_tests__quantifiers_star.snap | 16 +- ...gen_tests__quantifiers_star_nongreedy.snap | 16 +- ...degen_tests__quantifiers_struct_array.snap | 34 +- ...sts__recursion_with_structured_result.snap | 34 +- ...odegen_tests__sequences_in_quantifier.snap | 26 +- ...odegen_tests__sequences_with_captures.snap | 18 +- crates/plotnik-lib/src/engine/checkpoint.rs | 94 ++++ crates/plotnik-lib/src/engine/cursor.rs | 178 +++++++ crates/plotnik-lib/src/engine/effect.rs | 91 ++++ crates/plotnik-lib/src/engine/engine_tests.rs | 369 +++++++++++++ crates/plotnik-lib/src/engine/error.rs | 31 ++ crates/plotnik-lib/src/engine/frame.rs | 108 ++++ crates/plotnik-lib/src/engine/materializer.rs | 159 ++++++ crates/plotnik-lib/src/engine/mod.rs | 24 + ...engine_tests__alternation_merge_ident.snap | 18 + ...__engine_tests__alternation_merge_num.snap | 18 + ...ngine_tests__alternation_tagged_ident.snap | 23 + ..._engine_tests__alternation_tagged_num.snap | 23 + ...ngine__engine_tests__anchor_adjacency.snap | 25 + ...ine__engine_tests__anchor_first_child.snap | 17 + ...ngine__engine_tests__capture_multiple.snap | 25 + ..._engine__engine_tests__capture_single.snap | 17 + ...gine_tests__capture_string_annotation.snap | 10 + ...e__engine_tests__field_negated_absent.snap | 17 + ...gine_tests__quantifier_nongreedy_star.snap | 10 + ...ine_tests__quantifier_optional_absent.snap | 18 + ...ne_tests__quantifier_optional_present.snap | 25 + ...engine__engine_tests__quantifier_plus.snap | 35 ++ ...engine__engine_tests__quantifier_star.snap | 35 ++ ...engine_tests__quantifier_struct_array.snap | 65 +++ ..._engine_tests__recursion_member_chain.snap | 49 ++ ..._engine_tests__recursion_nested_calls.snap | 29 ++ ...on_call_searches_for_field_constraint.snap | 41 ++ ...sion_recursive_captures_nest_properly.snap | 31 ++ ...egression_scalar_array_captures_nodes.snap | 45 ++ ...ssion_tagged_alternation_materializes.snap | 23 + ...e__engine_tests__search_skip_siblings.snap | 17 + crates/plotnik-lib/src/engine/trace.rs | 493 ++++++++++++++++++ crates/plotnik-lib/src/engine/value.rs | 422 +++++++++++++++ crates/plotnik-lib/src/engine/vm.rs | 352 +++++++++++++ crates/plotnik-lib/src/lib.rs | 1 + 82 files changed, 3447 insertions(+), 349 deletions(-) create mode 100644 crates/plotnik-lib/src/engine/checkpoint.rs create mode 100644 crates/plotnik-lib/src/engine/cursor.rs create mode 100644 crates/plotnik-lib/src/engine/effect.rs create mode 100644 crates/plotnik-lib/src/engine/engine_tests.rs create mode 100644 crates/plotnik-lib/src/engine/error.rs create mode 100644 crates/plotnik-lib/src/engine/frame.rs create mode 100644 crates/plotnik-lib/src/engine/materializer.rs create mode 100644 crates/plotnik-lib/src/engine/mod.rs create mode 100644 crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__alternation_merge_ident.snap create mode 100644 crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__alternation_merge_num.snap create mode 100644 crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__alternation_tagged_ident.snap create mode 100644 crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__alternation_tagged_num.snap create mode 100644 crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__anchor_adjacency.snap create mode 100644 crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__anchor_first_child.snap create mode 100644 crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__capture_multiple.snap create mode 100644 crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__capture_single.snap create mode 100644 crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__capture_string_annotation.snap create mode 100644 crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__field_negated_absent.snap create mode 100644 crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__quantifier_nongreedy_star.snap create mode 100644 crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__quantifier_optional_absent.snap create mode 100644 crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__quantifier_optional_present.snap create mode 100644 crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__quantifier_plus.snap create mode 100644 crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__quantifier_star.snap create mode 100644 crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__quantifier_struct_array.snap create mode 100644 crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__recursion_member_chain.snap create mode 100644 crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__recursion_nested_calls.snap create mode 100644 crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__regression_call_searches_for_field_constraint.snap create mode 100644 crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__regression_recursive_captures_nest_properly.snap create mode 100644 crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__regression_scalar_array_captures_nodes.snap create mode 100644 crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__regression_tagged_alternation_materializes.snap create mode 100644 crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__search_skip_siblings.snap create mode 100644 crates/plotnik-lib/src/engine/trace.rs create mode 100644 crates/plotnik-lib/src/engine/value.rs create mode 100644 crates/plotnik-lib/src/engine/vm.rs diff --git a/crates/plotnik-lib/src/compile/capture.rs b/crates/plotnik-lib/src/compile/capture.rs index 90e9ba72..1d5fc25a 100644 --- a/crates/plotnik-lib/src/compile/capture.rs +++ b/crates/plotnik-lib/src/compile/capture.rs @@ -76,6 +76,21 @@ impl Compiler<'_> { effects } + /// Check if a quantifier body needs Node effect before Push. + /// + /// For scalar array elements (simple named nodes, not structs/enums/refs), + /// we need [Node, Push] to capture the matched node value. + /// For structured elements, EndObj/EndEnum/Call already provides the value. + pub(super) fn quantifier_needs_node_for_push(&self, expr: &Expr) -> bool { + if let Expr::QuantifiedExpr(quant) = expr + && let Some(body) = quant.inner() + { + !inner_creates_scope(&body) && !self.is_ref_returning_structured(&body) + } else { + true + } + } + /// Check if expr is (or wraps) a ref returning a structured type. /// /// For such refs, we skip the Node effect in captures - the Call leaves diff --git a/crates/plotnik-lib/src/compile/mod.rs b/crates/plotnik-lib/src/compile/mod.rs index f5c4ab0d..09622910 100644 --- a/crates/plotnik-lib/src/compile/mod.rs +++ b/crates/plotnik-lib/src/compile/mod.rs @@ -24,13 +24,13 @@ mod sequences; use indexmap::IndexMap; use plotnik_core::{Interner, NodeFieldId, NodeTypeId, Symbol}; -use crate::bytecode::ir::{Instruction, Label, ReturnIR}; -use crate::bytecode::Nav; +use crate::bytecode::ir::{EffectIR, Instruction, Label, MatchIR, ReturnIR}; +use crate::bytecode::{EffectOpcode, Nav}; use crate::parser::ast::Expr; use crate::emit::StringTableBuilder; use crate::analyze::symbol_table::SymbolTable; -use crate::analyze::type_check::{DefId, TypeContext}; +use crate::analyze::type_check::{DefId, TypeContext, TypeShape}; pub use capture::CaptureEffects; use scope::StructScope; @@ -157,8 +157,48 @@ impl<'a> Compiler<'a> { self.instructions .push(Instruction::Return(ReturnIR { label: return_label })); - // Compile body with root scope, targeting return instruction - let body_entry = if let Some(type_id) = self.type_ctx.get_def_type(def_id) { + // Check if definition returns a struct type - if so, wrap in Obj/EndObj + // This ensures recursive calls properly scope their captures + let def_returns_struct = self + .type_ctx + .get_def_type(def_id) + .and_then(|tid| self.type_ctx.get_type(tid)) + .is_some_and(|shape| matches!(shape, TypeShape::Struct(_))); + + let body_entry = if def_returns_struct { + let type_id = self.type_ctx.get_def_type(def_id).expect("checked above"); + + // Emit EndObj → Return + let endobj_label = self.fresh_label(); + self.instructions.push(Instruction::Match(MatchIR { + label: endobj_label, + nav: Nav::Stay, + node_type: None, + node_field: None, + pre_effects: vec![], + neg_fields: vec![], + post_effects: vec![EffectIR::simple(EffectOpcode::EndObj, 0)], + successors: vec![return_label], + })); + + // Compile body with scope, targeting EndObj + let inner_entry = self.with_scope(type_id, |this| this.compile_expr(body, endobj_label)); + + // Emit Obj → inner_entry + let obj_label = self.fresh_label(); + self.instructions.push(Instruction::Match(MatchIR { + label: obj_label, + nav: Nav::Stay, + node_type: None, + node_field: None, + pre_effects: vec![EffectIR::simple(EffectOpcode::Obj, 0)], + neg_fields: vec![], + post_effects: vec![], + successors: vec![inner_entry], + })); + + obj_label + } else if let Some(type_id) = self.type_ctx.get_def_type(def_id) { self.with_scope(type_id, |this| this.compile_expr(body, return_label)) } else { self.compile_expr(body, return_label) diff --git a/crates/plotnik-lib/src/compile/quantifier.rs b/crates/plotnik-lib/src/compile/quantifier.rs index 3e014c9b..5c133645 100644 --- a/crates/plotnik-lib/src/compile/quantifier.rs +++ b/crates/plotnik-lib/src/compile/quantifier.rs @@ -236,9 +236,15 @@ impl Compiler<'_> { let match_endarr = self.emit_endarr_step(&capture_effects, &outer_capture.post, match_exit); let skip_endarr = self.emit_endarr_step(&capture_effects, &outer_capture.post, skip_exit); - // Compile inner star with Push effects and split exits let push_effects = CaptureEffects { - post: vec![EffectIR::simple(EffectOpcode::Push, 0)], + post: if self.quantifier_needs_node_for_push(inner) { + vec![ + EffectIR::simple(EffectOpcode::Node, 0), + EffectIR::simple(EffectOpcode::Push, 0), + ] + } else { + vec![EffectIR::simple(EffectOpcode::Push, 0)] + }, }; let inner_entry = self.compile_star_for_array_with_exits(inner, match_endarr, skip_endarr, nav_override, push_effects); diff --git a/crates/plotnik-lib/src/compile/scope.rs b/crates/plotnik-lib/src/compile/scope.rs index ec6bfada..111e2624 100644 --- a/crates/plotnik-lib/src/compile/scope.rs +++ b/crates/plotnik-lib/src/compile/scope.rs @@ -207,7 +207,14 @@ impl Compiler<'_> { })); let push_effects = CaptureEffects { - post: vec![EffectIR::simple(EffectOpcode::Push, 0)], + post: if self.quantifier_needs_node_for_push(inner) { + vec![ + EffectIR::simple(EffectOpcode::Node, 0), + EffectIR::simple(EffectOpcode::Push, 0), + ] + } else { + vec![EffectIR::simple(EffectOpcode::Push, 0)] + }, }; let inner_entry = if let Expr::QuantifiedExpr(quant) = inner { self.compile_quantified_for_array(quant, endarr_step, nav_override, push_effects) diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__alternations_captured.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__alternations_captured.snap index 64862249..70b5e6bb 100644 --- a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__alternations_captured.snap +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__alternations_captured.snap @@ -24,14 +24,16 @@ M0: S1 → T0 ; value: N0: S2 → T1 ; Test [entrypoints] -Test = 1 :: T1 +Test = 01 :: T1 [transitions] - 0 ε ◼ + 00 ε ◼ Test: - 1 ε 2 - 2 ε 5, 7 - 4 ▶ - 5 (identifier) [Node Set(M0)] 4 - 7 (number) [Node Set(M0)] 4 + 01 ε 02 + 02 ε [Obj] 04 + 04 ε 09, 11 + 06 ▶ + 07 ε [EndObj] 06 + 09 (identifier) [Node Set(M0)] 07 + 11 (number) [Node Set(M0)] 07 diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__alternations_captured_tagged.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__alternations_captured_tagged.snap index 9afe5cfb..efec52f8 100644 --- a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__alternations_captured_tagged.snap +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__alternations_captured_tagged.snap @@ -42,11 +42,13 @@ Test = 01 :: T4 Test: 01 ε 02 - 02 ε 09, 15 - 04 ▶ - 05 ε [EndEnum Set(M4)] 04 - 07 (identifier) [Node Set(M0)] 05 - 09 ε [Enum(M2)] 07 - 11 ε [EndEnum Set(M4)] 04 - 13 (number) [Node Set(M1)] 11 - 15 ε [Enum(M3)] 13 + 02 ε [Obj] 04 + 04 ε 13, 19 + 06 ▶ + 07 ε [EndObj] 06 + 09 ε [EndEnum Set(M4)] 07 + 11 (identifier) [Node Set(M0)] 09 + 13 ε [Enum(M2)] 11 + 15 ε [EndEnum Set(M4)] 07 + 17 (number) [Node Set(M1)] 15 + 19 ε [Enum(M3)] 17 diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__alternations_in_quantifier.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__alternations_in_quantifier.snap index 8773deaf..793fe976 100644 --- a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__alternations_in_quantifier.snap +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__alternations_in_quantifier.snap @@ -47,29 +47,31 @@ Test = 01 :: T6 Test: 01 ε 02 - 02 (object) 03 - 03 ε [Arr] 05 - 05 ε 29, 11 - 07 ε [EndArr Set(M5)] 09 - 09 △ 10 - 10 ▶ - 11 ε [EndArr Set(M5)] 10 - 13 ε [EndObj Push] 49 - 15 ε [EndEnum Set(M4)] 13 - 17 ▽ (pair) [Node Set(M0)] 15 - 19 ε [Enum(M2)] 17 - 21 ε [EndEnum Set(M4)] 13 - 23 ▽ (shorthand_property_identifier) [Node Set(M1)] 21 - 25 ε [Enum(M3)] 23 - 27 ε 19, 25 - 29 ε [Obj] 27 - 31 ε [EndObj Push] 49 - 33 ε [EndEnum Set(M4)] 31 - 35 ▷ (pair) [Node Set(M0)] 33 - 37 ε [Enum(M2)] 35 - 39 ε [EndEnum Set(M4)] 31 - 41 ▷ (shorthand_property_identifier) [Node Set(M1)] 39 - 43 ε [Enum(M3)] 41 - 45 ε 37, 43 - 47 ε [Obj] 45 - 49 ε 47, 07 + 02 ε [Obj] 04 + 04 (object) 05 + 05 ε [Arr] 07 + 07 ε 33, 15 + 09 ε [EndArr Set(M5)] 11 + 11 △ 13 + 12 ▶ + 13 ε [EndObj] 12 + 15 ε [EndArr Set(M5)] 13 + 17 ε [EndObj Push] 53 + 19 ε [EndEnum Set(M4)] 17 + 21 ▽ (pair) [Node Set(M0)] 19 + 23 ε [Enum(M2)] 21 + 25 ε [EndEnum Set(M4)] 17 + 27 ▽ (shorthand_property_identifier) [Node Set(M1)] 25 + 29 ε [Enum(M3)] 27 + 31 ε 23, 29 + 33 ε [Obj] 31 + 35 ε [EndObj Push] 53 + 37 ε [EndEnum Set(M4)] 35 + 39 ▷ (pair) [Node Set(M0)] 37 + 41 ε [Enum(M2)] 39 + 43 ε [EndEnum Set(M4)] 35 + 45 ▷ (shorthand_property_identifier) [Node Set(M1)] 43 + 47 ε [Enum(M3)] 45 + 49 ε 41, 47 + 51 ε [Obj] 49 + 53 ε 51, 09 diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__alternations_no_internal_captures.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__alternations_no_internal_captures.snap index f992b322..e2fe6126 100644 --- a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__alternations_no_internal_captures.snap +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__alternations_no_internal_captures.snap @@ -32,9 +32,11 @@ Test = 01 :: T1 Test: 01 ε 02 - 02 (program) 03 - 03 ε 06, 08 - 05 ▶ - 06 ▽ (identifier) [Node Set(M0)] 10 - 08 ▽ (number) [Node Set(M0)] 10 - 10 △ 05 + 02 ε [Obj] 04 + 04 (program) 05 + 05 ε 10, 12 + 07 ▶ + 08 ε [EndObj] 07 + 10 ▽ (identifier) [Node Set(M0)] 14 + 12 ▽ (number) [Node Set(M0)] 14 + 14 △ 08 diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__alternations_null_injection.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__alternations_null_injection.snap index 334afba6..b6c9b943 100644 --- a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__alternations_null_injection.snap +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__alternations_null_injection.snap @@ -38,9 +38,11 @@ Test = 01 :: T1 Test: 01 ε 02 - 02 ε 07, 11 - 04 ▶ - 05 (identifier) [Node Set(M0)] 04 - 07 ε [Null Set(M1)] 05 - 09 (number) [Node Set(M1)] 04 - 11 ε [Null Set(M0)] 09 + 02 ε [Obj] 04 + 04 ε 11, 15 + 06 ▶ + 07 ε [EndObj] 06 + 09 (identifier) [Node Set(M0)] 07 + 11 ε [Null Set(M1)] 09 + 13 (number) [Node Set(M1)] 07 + 15 ε [Null Set(M0)] 13 diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__alternations_unlabeled.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__alternations_unlabeled.snap index c169784e..6ea6d61e 100644 --- a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__alternations_unlabeled.snap +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__alternations_unlabeled.snap @@ -38,9 +38,11 @@ Test = 01 :: T1 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 + 02 ε [Obj] 04 + 04 ε 11, 15 + 06 ▶ + 07 ε [EndObj] 06 + 09 (identifier) [Node Set(M0)] 07 + 11 ε [Null Set(M1)] 09 + 13 (string) [Node Set(M1)] 07 + 15 ε [Null Set(M0)] 13 diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__captures_basic.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__captures_basic.snap index a5f81b99..c6780013 100644 --- a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__captures_basic.snap +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__captures_basic.snap @@ -30,5 +30,7 @@ Test = 1 :: T1 Test: 1 ε 2 - 2 (identifier) [Node Set(M0)] 4 - 4 ▶ + 2 ε [Obj] 4 + 4 (identifier) [Node Set(M0)] 6 + 6 ε [EndObj] 8 + 8 ▶ diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__captures_deeply_nested.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__captures_deeply_nested.snap index 20411b59..83e54e03 100644 --- a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__captures_deeply_nested.snap +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__captures_deeply_nested.snap @@ -44,11 +44,13 @@ Test = 01 :: T1 Test: 01 ε 02 - 02 (a) [Node Set(M0)] 04 - 04 ▽ (b) [Node Set(M1)] 06 - 06 ▽ (c) [Node Set(M2)] 08 - 08 ▽ (d) [Node Set(M3)] 10 - 10 △ 11 - 11 △ 12 + 02 ε [Obj] 04 + 04 (a) [Node Set(M0)] 06 + 06 ▽ (b) [Node Set(M1)] 08 + 08 ▽ (c) [Node Set(M2)] 10 + 10 ▽ (d) [Node Set(M3)] 12 12 △ 13 - 13 ▶ + 13 △ 14 + 14 △ 15 + 15 ε [EndObj] 17 + 17 ▶ diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__captures_multiple.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__captures_multiple.snap index 945d6bbf..447efca7 100644 --- a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__captures_multiple.snap +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__captures_multiple.snap @@ -31,15 +31,17 @@ M3: S2 → T0 ; b: N0: S3 → T1 ; Test [entrypoints] -Test = 1 :: T1 +Test = 01 :: T1 [transitions] - 0 ε ◼ + 00 ε ◼ Test: - 1 ε 2 - 2 (binary_expression) 3 - 3 ▽ (identifier) [Node Set(M0)] 5 - 5 ▷ (number) [Node Set(M1)] 7 - 7 △ 8 - 8 ▶ + 01 ε 02 + 02 ε [Obj] 04 + 04 (binary_expression) 05 + 05 ▽ (identifier) [Node Set(M0)] 07 + 07 ▷ (number) [Node Set(M1)] 09 + 09 △ 10 + 10 ε [EndObj] 12 + 12 ▶ diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__captures_nested_flat.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__captures_nested_flat.snap index 2b22cce4..fe2dc3d6 100644 --- a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__captures_nested_flat.snap +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__captures_nested_flat.snap @@ -38,9 +38,11 @@ Test = 01 :: T1 Test: 01 ε 02 - 02 (a) [Node Set(M0)] 04 - 04 ▽ (b) [Node Set(M1)] 06 - 06 ▽ (c) [Node Set(M2)] 08 - 08 △ 09 - 09 △ 10 - 10 ▶ + 02 ε [Obj] 04 + 04 (a) [Node Set(M0)] 06 + 06 ▽ (b) [Node Set(M1)] 08 + 08 ▽ (c) [Node Set(M2)] 10 + 10 △ 11 + 11 △ 12 + 12 ε [EndObj] 14 + 14 ▶ diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__captures_optional_wrapper_struct.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__captures_optional_wrapper_struct.snap index dfd49e3e..196062d5 100644 --- a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__captures_optional_wrapper_struct.snap +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__captures_optional_wrapper_struct.snap @@ -41,10 +41,12 @@ Test = 01 :: T3 Test: 01 ε 02 02 ε [Obj] 04 - 04 ε 13, 15 - 06 ▶ - 07 ε [EndObj Set(M2)] 06 - 09 ε [EndObj Set(M1)] 07 - 11 (identifier) [Node Set(M0)] 09 - 13 ε [Obj] 11 - 15 ε [Null Set(M1)] 07 + 04 ε [Obj] 06 + 06 ε 17, 19 + 08 ▶ + 09 ε [EndObj] 08 + 11 ε [EndObj Set(M2)] 09 + 13 ε [EndObj Set(M1)] 11 + 15 (identifier) [Node Set(M0)] 13 + 17 ε [Obj] 15 + 19 ε [Null Set(M1)] 11 diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__captures_struct_scope.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__captures_struct_scope.snap index cf7c1e2d..e3a0cff0 100644 --- a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__captures_struct_scope.snap +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__captures_struct_scope.snap @@ -39,7 +39,9 @@ Test = 01 :: T2 Test: 01 ε 02 02 ε [Obj] 04 - 04 (a) [Node Set(M0)] 06 - 06 ▷ (b) [Node Set(M1)] 08 - 08 ε [EndObj Set(M2)] 10 - 10 ▶ + 04 ε [Obj] 06 + 06 (a) [Node Set(M0)] 08 + 08 ▷ (b) [Node Set(M1)] 10 + 10 ε [EndObj Set(M2)] 12 + 12 ε [EndObj] 14 + 14 ▶ diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__captures_with_type_custom.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__captures_with_type_custom.snap index 4c78544a..9321ae3f 100644 --- a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__captures_with_type_custom.snap +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__captures_with_type_custom.snap @@ -33,5 +33,7 @@ Test = 1 :: T2 Test: 1 ε 2 - 2 (identifier) [Node Set(M0)] 4 - 4 ▶ + 2 ε [Obj] 4 + 4 (identifier) [Node Set(M0)] 6 + 6 ε [EndObj] 8 + 8 ▶ diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__captures_with_type_string.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__captures_with_type_string.snap index 88250cde..12a12210 100644 --- a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__captures_with_type_string.snap +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__captures_with_type_string.snap @@ -30,5 +30,7 @@ Test = 1 :: T1 Test: 1 ε 2 - 2 (identifier) [Text Set(M0)] 4 - 4 ▶ + 2 ε [Obj] 4 + 4 (identifier) [Text Set(M0)] 6 + 6 ε [EndObj] 8 + 8 ▶ diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__captures_wrapper_struct.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__captures_wrapper_struct.snap index 3605d754..ebf84e56 100644 --- a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__captures_wrapper_struct.snap +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__captures_wrapper_struct.snap @@ -44,20 +44,22 @@ Test = 01 :: T4 Test: 01 ε 02 - 02 ε [Arr] 04 - 04 ε 19, 07 - 06 ▶ - 07 ε [EndArr Set(M3)] 06 - 09 ε [EndObj Push] 33 - 11 ε [EndObj Set(M2)] 09 - 13 ▷ (number) [Node Set(M1)] 11 - 15 (identifier) [Node Set(M0)] 13 - 17 ε [Obj] 15 - 19 ε [Obj] 17 - 21 ε [EndObj Push] 33 - 23 ε [EndObj Set(M2)] 21 - 25 ▷ (number) [Node Set(M1)] 23 - 27 (identifier) [Node Set(M0)] 25 - 29 ε [Obj] 27 - 31 ε [Obj] 29 - 33 ε 31, 07 + 02 ε [Obj] 04 + 04 ε [Arr] 06 + 06 ε 23, 11 + 08 ▶ + 09 ε [EndObj] 08 + 11 ε [EndArr Set(M3)] 09 + 13 ε [EndObj Push] 37 + 15 ε [EndObj Set(M2)] 13 + 17 ▷ (number) [Node Set(M1)] 15 + 19 (identifier) [Node Set(M0)] 17 + 21 ε [Obj] 19 + 23 ε [Obj] 21 + 25 ε [EndObj Push] 37 + 27 ε [EndObj Set(M2)] 25 + 29 ▷ (number) [Node Set(M1)] 27 + 31 (identifier) [Node Set(M0)] 29 + 33 ε [Obj] 31 + 35 ε [Obj] 33 + 37 ε 35, 11 diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__comprehensive_multi_definition.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__comprehensive_multi_definition.snap index d4927fce..6d44d222 100644 --- a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__comprehensive_multi_definition.snap +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__comprehensive_multi_definition.snap @@ -57,8 +57,8 @@ N1: S07 → T05 ; Expression N2: S08 → T06 ; Assignment [entrypoints] -Assignment = 08 :: T06 -Expression = 05 :: T05 +Assignment = 12 :: T06 +Expression = 09 :: T05 Ident = 01 :: T02 [transitions] @@ -66,25 +66,29 @@ Ident = 01 :: T02 Ident: 01 ε 02 - 02 (identifier) [Text Set(M0)] 04 - 04 ▶ + 02 ε [Obj] 04 + 04 (identifier) [Text Set(M0)] 06 + 06 ε [EndObj] 08 + 08 ▶ Expression: - 05 ε 06 - 06 ε 22, 28 + 09 ε 10 + 10 ε 30, 36 Assignment: - 08 ε 09 - 09 (assignment_expression) 10 - 10 ▽ left: (identifier) [Node Set(M6)] 12 - 12 ▷ right: (Expression) 13 ⯇ - 13 ε [Set(M5)] 15 - 15 △ 16 - 16 ▶ - 17 ▶ - 18 ε [EndEnum] 17 - 20 (number) [Node Set(M1)] 18 - 22 ε [Enum(M3)] 20 - 24 ε [EndEnum] 17 - 26 (identifier) [Node Set(M2)] 24 - 28 ε [Enum(M4)] 26 + 12 ε 13 + 13 ε [Obj] 15 + 15 (assignment_expression) 16 + 16 ▽ left: (identifier) [Node Set(M6)] 18 + 18 ▷ right: (Expression) 19 ⯇ + 19 ε [Set(M5)] 21 + 21 △ 22 + 22 ε [EndObj] 24 + 24 ▶ + 25 ▶ + 26 ε [EndEnum] 25 + 28 (number) [Node Set(M1)] 26 + 30 ε [Enum(M3)] 28 + 32 ε [EndEnum] 25 + 34 (identifier) [Node Set(M2)] 32 + 36 ε [Enum(M4)] 34 diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__definitions_multiple.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__definitions_multiple.snap index 85053b79..2d3771ed 100644 --- a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__definitions_multiple.snap +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__definitions_multiple.snap @@ -30,18 +30,22 @@ N0: S3 → T1 ; Foo N1: S4 → T2 ; Bar [entrypoints] -Bar = 5 :: T2 -Foo = 1 :: T1 +Bar = 09 :: T2 +Foo = 01 :: T1 [transitions] - 0 ε ◼ + 00 ε ◼ Foo: - 1 ε 2 - 2 (identifier) [Node Set(M0)] 4 - 4 ▶ + 01 ε 02 + 02 ε [Obj] 04 + 04 (identifier) [Node Set(M0)] 06 + 06 ε [EndObj] 08 + 08 ▶ Bar: - 5 ε 6 - 6 (string) [Node Set(M1)] 8 - 8 ▶ + 09 ε 10 + 10 ε [Obj] 12 + 12 (string) [Node Set(M1)] 14 + 14 ε [EndObj] 16 + 16 ▶ diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__definitions_reference.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__definitions_reference.snap index 3d25f336..7c154695 100644 --- a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__definitions_reference.snap +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__definitions_reference.snap @@ -36,23 +36,27 @@ N1: S4 → T2 ; Root [entrypoints] Expression = 01 :: T1 -Root = 04 :: T2 +Root = 06 :: T2 [transitions] 00 ε ◼ Expression: 01 ε 02 - 02 ε 13, 17 + 02 ε [Obj] 04 + 04 ε 21, 25 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 + 06 ε 07 + 07 ε [Obj] 09 + 09 (function_declaration) 10 + 10 ▽ name: (identifier) [Node Set(M2)] 12 + 12 △ 13 + 13 ε [EndObj] 15 + 15 ▶ + 16 ▶ + 17 ε [EndObj] 16 + 19 (identifier) [Node Set(M0)] 17 + 21 ε [Null Set(M1)] 19 + 23 (number) [Node Set(M1)] 17 + 25 ε [Null Set(M0)] 23 diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__definitions_single.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__definitions_single.snap index 21ffa3ba..d451545a 100644 --- a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__definitions_single.snap +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__definitions_single.snap @@ -30,5 +30,7 @@ Foo = 1 :: T1 Foo: 1 ε 2 - 2 (identifier) [Node Set(M0)] 4 - 4 ▶ + 2 ε [Obj] 4 + 4 (identifier) [Node Set(M0)] 6 + 6 ε [EndObj] 8 + 8 ▶ diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__fields_alternation.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__fields_alternation.snap index 6fdf96db..58eca669 100644 --- a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__fields_alternation.snap +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__fields_alternation.snap @@ -40,12 +40,14 @@ Test = 01 :: T1 Test: 01 ε 02 - 02 (call_expression) 03 - 03 ▽ function: _ 04 - 04 ε 09, 13 - 06 ▶ - 07 (identifier) [Node Set(M0)] 15 - 09 ε [Null Set(M1)] 07 - 11 (number) [Node Set(M1)] 15 - 13 ε [Null Set(M0)] 11 - 15 △ 06 + 02 ε [Obj] 04 + 04 (call_expression) 05 + 05 ▽ function: _ 06 + 06 ε 13, 17 + 08 ▶ + 09 ε [EndObj] 08 + 11 (identifier) [Node Set(M0)] 19 + 13 ε [Null Set(M1)] 11 + 15 (number) [Node Set(M1)] 19 + 17 ε [Null Set(M0)] 15 + 19 △ 09 diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__fields_multiple.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__fields_multiple.snap index 4a9a6b79..da15248d 100644 --- a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__fields_multiple.snap +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__fields_multiple.snap @@ -31,15 +31,17 @@ M3: S2 → T0 ; right: N0: S3 → T1 ; Test [entrypoints] -Test = 1 :: T1 +Test = 01 :: T1 [transitions] - 0 ε ◼ + 00 ε ◼ Test: - 1 ε 2 - 2 (binary_expression) 3 - 3 ▽ left: _ [Node Set(M0)] 5 - 5 ▷ right: _ [Node Set(M1)] 7 - 7 △ 8 - 8 ▶ + 01 ε 02 + 02 ε [Obj] 04 + 04 (binary_expression) 05 + 05 ▽ left: _ [Node Set(M0)] 07 + 07 ▷ right: _ [Node Set(M1)] 09 + 09 △ 10 + 10 ε [EndObj] 12 + 12 ▶ diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__fields_negated.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__fields_negated.snap index 24ad5f8d..5442c91f 100644 --- a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__fields_negated.snap +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__fields_negated.snap @@ -25,14 +25,16 @@ M0: S1 → T0 ; name: N0: S2 → T1 ; Test [entrypoints] -Test = 1 :: T1 +Test = 01 :: T1 [transitions] - 0 ε ◼ + 00 ε ◼ Test: - 1 ε 2 - 2 -type_parameters (function_declaration) 4 - 4 ▽ name: (identifier) [Node Set(M0)] 6 - 6 △ 7 - 7 ▶ + 01 ε 02 + 02 ε [Obj] 04 + 04 -type_parameters (function_declaration) 06 + 06 ▽ name: (identifier) [Node Set(M0)] 08 + 08 △ 09 + 09 ε [EndObj] 11 + 11 ▶ diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__fields_single.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__fields_single.snap index fa501a97..17651b2b 100644 --- a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__fields_single.snap +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__fields_single.snap @@ -24,14 +24,16 @@ M0: S1 → T0 ; name: N0: S2 → T1 ; Test [entrypoints] -Test = 1 :: T1 +Test = 01 :: T1 [transitions] - 0 ε ◼ + 00 ε ◼ Test: - 1 ε 2 - 2 (function_declaration) 3 - 3 ▽ name: (identifier) [Node Set(M0)] 5 - 5 △ 6 - 6 ▶ + 01 ε 02 + 02 ε [Obj] 04 + 04 (function_declaration) 05 + 05 ▽ name: (identifier) [Node Set(M0)] 07 + 07 △ 08 + 08 ε [EndObj] 10 + 10 ▶ diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__nodes_anonymous.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__nodes_anonymous.snap index 2875156a..b66d09e7 100644 --- a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__nodes_anonymous.snap +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__nodes_anonymous.snap @@ -24,14 +24,16 @@ M0: S1 → T0 ; op: N0: S2 → T1 ; Test [entrypoints] -Test = 1 :: T1 +Test = 01 :: T1 [transitions] - 0 ε ◼ + 00 ε ◼ Test: - 1 ε 2 - 2 (binary_expression) 3 - 3 ▽ (+) [Node Set(M0)] 5 - 5 △ 6 - 6 ▶ + 01 ε 02 + 02 ε [Obj] 04 + 04 (binary_expression) 05 + 05 ▽ (+) [Node Set(M0)] 07 + 07 △ 08 + 08 ε [EndObj] 10 + 10 ▶ diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__nodes_error.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__nodes_error.snap index 30a2f155..682eb76f 100644 --- a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__nodes_error.snap +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__nodes_error.snap @@ -30,5 +30,7 @@ Test = 1 :: T1 Test: 1 ε 2 - 2 (ERROR) [Node Set(M0)] 4 - 4 ▶ + 2 ε [Obj] 4 + 4 (ERROR) [Node Set(M0)] 6 + 6 ε [EndObj] 8 + 8 ▶ diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__nodes_missing.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__nodes_missing.snap index 4d2ffcae..3d303390 100644 --- a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__nodes_missing.snap +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__nodes_missing.snap @@ -30,5 +30,7 @@ Test = 1 :: T1 Test: 1 ε 2 - 2 (MISSING) [Node Set(M0)] 4 - 4 ▶ + 2 ε [Obj] 4 + 4 (MISSING) [Node Set(M0)] 6 + 6 ε [EndObj] 8 + 8 ▶ diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__nodes_named.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__nodes_named.snap index 9947cdc7..480bebe4 100644 --- a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__nodes_named.snap +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__nodes_named.snap @@ -30,5 +30,7 @@ Test = 1 :: T1 Test: 1 ε 2 - 2 (identifier) [Node Set(M0)] 4 - 4 ▶ + 2 ε [Obj] 4 + 4 (identifier) [Node Set(M0)] 6 + 6 ε [EndObj] 8 + 8 ▶ diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__nodes_wildcard_any.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__nodes_wildcard_any.snap index 280d756b..9f24aa11 100644 --- a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__nodes_wildcard_any.snap +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__nodes_wildcard_any.snap @@ -23,14 +23,16 @@ M0: S1 → T0 ; key: N0: S2 → T1 ; Test [entrypoints] -Test = 1 :: T1 +Test = 01 :: T1 [transitions] - 0 ε ◼ + 00 ε ◼ Test: - 1 ε 2 - 2 (pair) 3 - 3 ▽ key: _ [Node Set(M0)] 5 - 5 △ 6 - 6 ▶ + 01 ε 02 + 02 ε [Obj] 04 + 04 (pair) 05 + 05 ▽ key: _ [Node Set(M0)] 07 + 07 △ 08 + 08 ε [EndObj] 10 + 10 ▶ diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__nodes_wildcard_named.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__nodes_wildcard_named.snap index ae4d8751..5a2f821c 100644 --- a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__nodes_wildcard_named.snap +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__nodes_wildcard_named.snap @@ -23,14 +23,16 @@ M0: S1 → T0 ; key: N0: S2 → T1 ; Test [entrypoints] -Test = 1 :: T1 +Test = 01 :: T1 [transitions] - 0 ε ◼ + 00 ε ◼ Test: - 1 ε 2 - 2 (pair) 3 - 3 ▽ key: _ [Node Set(M0)] 5 - 5 △ 6 - 6 ▶ + 01 ε 02 + 02 ε [Obj] 04 + 04 (pair) 05 + 05 ▽ key: _ [Node Set(M0)] 07 + 07 △ 08 + 08 ε [EndObj] 10 + 10 ▶ diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__optional_first_child.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__optional_first_child.snap index e5b3978a..ad63e996 100644 --- a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__optional_first_child.snap +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__optional_first_child.snap @@ -39,11 +39,13 @@ Test = 01 :: T1 Test: 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 + 02 ε [Obj] 04 + 04 (program) 05 + 05 ε 16, 14 + 07 ▶ + 08 ε [EndObj] 07 + 10 ▽ (number) [Node Set(M1)] 18 + 12 ▷ (number) [Node Set(M1)] 18 + 14 ε [Null Set(M0)] 10 + 16 ▽ (identifier) [Node Set(M0)] 12 + 18 △ 08 diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__optional_null_injection.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__optional_null_injection.snap index ce53efe3..04170092 100644 --- a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__optional_null_injection.snap +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__optional_null_injection.snap @@ -32,9 +32,11 @@ Test = 01 :: T1 Test: 01 ε 02 - 02 (function_declaration) 03 - 03 ε 05, 09 - 05 ▽ (decorator) [Node Set(M0)] 07 - 07 △ 08 - 08 ▶ - 09 ε [Null Set(M0)] 08 + 02 ε [Obj] 04 + 04 (function_declaration) 05 + 05 ε 07, 13 + 07 ▽ (decorator) [Node Set(M0)] 09 + 09 △ 11 + 10 ▶ + 11 ε [EndObj] 10 + 13 ε [Null Set(M0)] 11 diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__quantifiers_first_child_array.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__quantifiers_first_child_array.snap index 5adfe2c0..67f355ea 100644 --- a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__quantifiers_first_child_array.snap +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__quantifiers_first_child_array.snap @@ -39,15 +39,17 @@ Test = 01 :: T2 Test: 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 + 02 ε [Obj] 04 + 04 (array) 05 + 05 ε [Arr] 07 + 07 ε 20, 18 + 09 ▶ + 10 ε [EndObj] 09 + 12 ▽ (number) [Node Set(M1)] 26 + 14 ▷ (number) [Node Set(M1)] 26 + 16 ε [EndArr Set(M0)] 14 + 18 ε [EndArr Set(M0)] 12 + 20 ▽ (identifier) [Node Push] 24 + 22 ▷ (identifier) [Node Push] 24 + 24 ε 22, 16 + 26 △ 10 diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__quantifiers_optional.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__quantifiers_optional.snap index ce53efe3..04170092 100644 --- a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__quantifiers_optional.snap +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__quantifiers_optional.snap @@ -32,9 +32,11 @@ Test = 01 :: T1 Test: 01 ε 02 - 02 (function_declaration) 03 - 03 ε 05, 09 - 05 ▽ (decorator) [Node Set(M0)] 07 - 07 △ 08 - 08 ▶ - 09 ε [Null Set(M0)] 08 + 02 ε [Obj] 04 + 04 (function_declaration) 05 + 05 ε 07, 13 + 07 ▽ (decorator) [Node Set(M0)] 09 + 09 △ 11 + 10 ▶ + 11 ε [EndObj] 10 + 13 ε [Null Set(M0)] 11 diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__quantifiers_optional_nongreedy.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__quantifiers_optional_nongreedy.snap index 647977c3..4da8c5fd 100644 --- a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__quantifiers_optional_nongreedy.snap +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__quantifiers_optional_nongreedy.snap @@ -32,9 +32,11 @@ Test = 01 :: T1 Test: 01 ε 02 - 02 (function_declaration) 03 - 03 ε 09, 05 - 05 ▽ (decorator) [Node Set(M0)] 07 - 07 △ 08 - 08 ▶ - 09 ε [Null Set(M0)] 08 + 02 ε [Obj] 04 + 04 (function_declaration) 05 + 05 ε 13, 07 + 07 ▽ (decorator) [Node Set(M0)] 09 + 09 △ 11 + 10 ▶ + 11 ε [EndObj] 10 + 13 ε [Null Set(M0)] 11 diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__quantifiers_plus.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__quantifiers_plus.snap index 0d5976e6..a7a92118 100644 --- a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__quantifiers_plus.snap +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__quantifiers_plus.snap @@ -31,9 +31,11 @@ Test = 01 :: T2 Test: 01 ε 02 - 02 ε [Arr] 04 - 04 (identifier) [Push] 11 - 06 ▶ - 07 ε [EndArr Set(M0)] 06 - 09 (identifier) [Push] 11 - 11 ε 09, 07 + 02 ε [Obj] 04 + 04 ε [Arr] 06 + 06 (identifier) [Node Push] 15 + 08 ▶ + 09 ε [EndObj] 08 + 11 ε [EndArr Set(M0)] 09 + 13 (identifier) [Node Push] 15 + 15 ε 13, 11 diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__quantifiers_plus_nongreedy.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__quantifiers_plus_nongreedy.snap index 44f08a87..b7fa9ccc 100644 --- a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__quantifiers_plus_nongreedy.snap +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__quantifiers_plus_nongreedy.snap @@ -31,9 +31,11 @@ Test = 01 :: T2 Test: 01 ε 02 - 02 ε [Arr] 04 - 04 (identifier) [Push] 11 - 06 ▶ - 07 ε [EndArr Set(M0)] 06 - 09 (identifier) [Push] 11 - 11 ε 07, 09 + 02 ε [Obj] 04 + 04 ε [Arr] 06 + 06 (identifier) [Node Push] 15 + 08 ▶ + 09 ε [EndObj] 08 + 11 ε [EndArr Set(M0)] 09 + 13 (identifier) [Node Push] 15 + 15 ε 11, 13 diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__quantifiers_repeat_navigation.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__quantifiers_repeat_navigation.snap index 0ffdf3a9..0cffcc8f 100644 --- a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__quantifiers_repeat_navigation.snap +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__quantifiers_repeat_navigation.snap @@ -32,13 +32,15 @@ Test = 01 :: T2 Test: 01 ε 02 - 02 (function_declaration) 03 - 03 ε [Arr] 05 - 05 ε 13, 11 - 07 ε [EndArr Set(M0)] 09 - 09 △ 10 - 10 ▶ - 11 ε [EndArr Set(M0)] 10 - 13 ▽ (decorator) [Push] 17 - 15 ▷ (decorator) [Push] 17 - 17 ε 15, 07 + 02 ε [Obj] 04 + 04 (function_declaration) 05 + 05 ε [Arr] 07 + 07 ε 17, 15 + 09 ε [EndArr Set(M0)] 11 + 11 △ 13 + 12 ▶ + 13 ε [EndObj] 12 + 15 ε [EndArr Set(M0)] 13 + 17 ▽ (decorator) [Node Push] 21 + 19 ▷ (decorator) [Node Push] 21 + 21 ε 19, 09 diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__quantifiers_star.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__quantifiers_star.snap index 91caacf1..e3a99a1c 100644 --- a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__quantifiers_star.snap +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__quantifiers_star.snap @@ -31,10 +31,12 @@ Test = 01 :: T2 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 + 02 ε [Obj] 04 + 04 ε [Arr] 06 + 06 ε 13, 11 + 08 ▶ + 09 ε [EndObj] 08 + 11 ε [EndArr Set(M0)] 09 + 13 (identifier) [Node Push] 17 + 15 (identifier) [Node Push] 17 + 17 ε 15, 11 diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__quantifiers_star_nongreedy.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__quantifiers_star_nongreedy.snap index f62cc270..77dc3ea7 100644 --- a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__quantifiers_star_nongreedy.snap +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__quantifiers_star_nongreedy.snap @@ -31,10 +31,12 @@ Test = 01 :: T2 Test: 01 ε 02 - 02 ε [Arr] 04 - 04 ε 07, 09 - 06 ▶ - 07 ε [EndArr Set(M0)] 06 - 09 (identifier) [Push] 13 - 11 (identifier) [Push] 13 - 13 ε 07, 11 + 02 ε [Obj] 04 + 04 ε [Arr] 06 + 06 ε 11, 13 + 08 ▶ + 09 ε [EndObj] 08 + 11 ε [EndArr Set(M0)] 09 + 13 (identifier) [Node Push] 17 + 15 (identifier) [Node Push] 17 + 17 ε 11, 15 diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__quantifiers_struct_array.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__quantifiers_struct_array.snap index 8d33c656..2450fa3a 100644 --- a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__quantifiers_struct_array.snap +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__quantifiers_struct_array.snap @@ -42,19 +42,21 @@ Test = 01 :: T3 Test: 01 ε 02 - 02 (array) 03 - 03 ε [Arr] 05 - 05 ε 19, 11 - 07 ε [EndArr Set(M2)] 09 - 09 △ 10 - 10 ▶ - 11 ε [EndArr Set(M2)] 10 - 13 ε [EndObj Push] 29 - 15 ▷ (number) [Node Set(M1)] 13 - 17 ▽ (identifier) [Node Set(M0)] 15 - 19 ε [Obj] 17 - 21 ε [EndObj Push] 29 - 23 ▷ (number) [Node Set(M1)] 21 - 25 ▷ (identifier) [Node Set(M0)] 23 - 27 ε [Obj] 25 - 29 ε 27, 07 + 02 ε [Obj] 04 + 04 (array) 05 + 05 ε [Arr] 07 + 07 ε 23, 15 + 09 ε [EndArr Set(M2)] 11 + 11 △ 13 + 12 ▶ + 13 ε [EndObj] 12 + 15 ε [EndArr Set(M2)] 13 + 17 ε [EndObj Push] 33 + 19 ▷ (number) [Node Set(M1)] 17 + 21 ▽ (identifier) [Node Set(M0)] 19 + 23 ε [Obj] 21 + 25 ε [EndObj Push] 33 + 27 ▷ (number) [Node Set(M1)] 25 + 29 ▷ (identifier) [Node Set(M0)] 27 + 31 ε [Obj] 29 + 33 ε 31, 09 diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__recursion_with_structured_result.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__recursion_with_structured_result.snap index 0820da35..b392558c 100644 --- a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__recursion_with_structured_result.snap +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__recursion_with_structured_result.snap @@ -61,23 +61,25 @@ Test = 04 :: T05 Expr: 01 ε 02 - 02 ε 19, 27 + 02 ε 23, 31 Test: 04 ε 05 - 05 (program) 06 - 06 ▽ (Expr) 07 ⯇ - 07 ε [Set(M5)] 09 - 09 △ 10 - 10 ▶ - 11 ε [Set(M2)] 13 - 13 △ 21 + 05 ε [Obj] 07 + 07 (program) 08 + 08 ▽ (Expr) 09 ⯇ + 09 ε [Set(M5)] 11 + 11 △ 12 + 12 ε [EndObj] 14 14 ▶ - 15 ε [EndEnum] 14 - 17 (number) [Text Set(M0)] 15 - 19 ε [Enum(M3)] 17 - 21 ε [EndEnum] 14 - 23 ▷ arguments: (Expr) 11 ⯇ - 24 ▽ function: (identifier) [Node Set(M1)] 23 - 26 (call_expression) 24 - 27 ε [Enum(M4)] 26 + 15 ε [Set(M2)] 17 + 17 △ 25 + 18 ▶ + 19 ε [EndEnum] 18 + 21 (number) [Text Set(M0)] 19 + 23 ε [Enum(M3)] 21 + 25 ε [EndEnum] 18 + 27 ▷ arguments: (Expr) 15 ⯇ + 28 ▽ function: (identifier) [Node Set(M1)] 27 + 30 (call_expression) 28 + 31 ε [Enum(M4)] 30 diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__sequences_in_quantifier.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__sequences_in_quantifier.snap index a2fe5b7a..60f800aa 100644 --- a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__sequences_in_quantifier.snap +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__sequences_in_quantifier.snap @@ -33,15 +33,17 @@ Test = 01 :: T2 Test: 01 ε 02 - 02 (parent) 03 - 03 ε [Arr] 05 - 05 ε 15, 11 - 07 ε [EndArr Set(M0)] 09 - 09 △ 10 - 10 ▶ - 11 ε [EndArr Set(M0)] 10 - 13 ▷ (b) [Push] 19 - 15 ▽ (a) 13 - 16 ▷ (b) [Push] 19 - 18 ▷ (a) 16 - 19 ε 18, 07 + 02 ε [Obj] 04 + 04 (parent) 05 + 05 ε [Arr] 07 + 07 ε 19, 15 + 09 ε [EndArr Set(M0)] 11 + 11 △ 13 + 12 ▶ + 13 ε [EndObj] 12 + 15 ε [EndArr Set(M0)] 13 + 17 ▷ (b) [Push] 23 + 19 ▽ (a) 17 + 20 ▷ (b) [Push] 23 + 22 ▷ (a) 20 + 23 ε 22, 09 diff --git a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__sequences_with_captures.snap b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__sequences_with_captures.snap index ca04184b..9112549c 100644 --- a/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__sequences_with_captures.snap +++ b/crates/plotnik-lib/src/emit/snapshots/plotnik_lib__emit__codegen_tests__sequences_with_captures.snap @@ -29,15 +29,17 @@ M3: S2 → T0 ; b: N0: S3 → T1 ; Test [entrypoints] -Test = 1 :: T1 +Test = 01 :: T1 [transitions] - 0 ε ◼ + 00 ε ◼ Test: - 1 ε 2 - 2 (parent) 3 - 3 ▽ (a) [Node Set(M0)] 5 - 5 ▷ (b) [Node Set(M1)] 7 - 7 △ 8 - 8 ▶ + 01 ε 02 + 02 ε [Obj] 04 + 04 (parent) 05 + 05 ▽ (a) [Node Set(M0)] 07 + 07 ▷ (b) [Node Set(M1)] 09 + 09 △ 10 + 10 ε [EndObj] 12 + 12 ▶ diff --git a/crates/plotnik-lib/src/engine/checkpoint.rs b/crates/plotnik-lib/src/engine/checkpoint.rs new file mode 100644 index 00000000..1323d82f --- /dev/null +++ b/crates/plotnik-lib/src/engine/checkpoint.rs @@ -0,0 +1,94 @@ +//! Checkpoints for backtracking. +//! +//! When the VM encounters a branch (multiple successors), it saves +//! a checkpoint for each alternative. On failure, it restores the +//! most recent checkpoint and continues. + +/// Checkpoint for backtracking. +#[derive(Clone, Copy, Debug)] +pub struct Checkpoint { + /// Cursor position (tree-sitter descendant_index). + pub descendant_index: u32, + /// Effect stream length at checkpoint. + pub effect_watermark: usize, + /// Frame arena state at checkpoint. + pub frame_index: Option, + /// Resume point (raw step index). + pub ip: u16, +} + +/// Stack of checkpoints with O(1) max_frame_ref tracking. +/// +/// The `max_frame_ref` is maintained for frame arena pruning: +/// we track the highest frame index referenced by any checkpoint +/// so pruning knows which frames are safe to remove. +#[derive(Debug)] +pub struct CheckpointStack { + stack: Vec, + /// Highest frame index referenced by any checkpoint. + max_frame_ref: Option, +} + +impl CheckpointStack { + /// Create an empty checkpoint stack. + pub fn new() -> Self { + Self { + stack: Vec::new(), + max_frame_ref: None, + } + } + + /// Push a checkpoint. + pub fn push(&mut self, checkpoint: Checkpoint) { + // Update max_frame_ref (O(1)) + if let Some(frame_idx) = checkpoint.frame_index { + self.max_frame_ref = Some(match self.max_frame_ref { + Some(max) => max.max(frame_idx), + None => frame_idx, + }); + } + self.stack.push(checkpoint); + } + + /// Pop and return the most recent checkpoint. + pub fn pop(&mut self) -> Option { + let cp = self.stack.pop()?; + + // Recompute max_frame_ref only if we removed the max holder + // This is O(1) amortized: each checkpoint contributes to at most + // one recomputation over its lifetime. + if cp.frame_index == self.max_frame_ref && !self.stack.is_empty() { + self.max_frame_ref = self.stack.iter().filter_map(|c| c.frame_index).max(); + } else if self.stack.is_empty() { + self.max_frame_ref = None; + } + + Some(cp) + } + + /// Get the highest frame index referenced by any checkpoint. + #[inline] + pub fn max_frame_ref(&self) -> Option { + self.max_frame_ref + } + + /// Check if empty. + #[inline] + #[allow(dead_code)] + pub fn is_empty(&self) -> bool { + self.stack.is_empty() + } + + /// Get number of checkpoints. + #[inline] + #[allow(dead_code)] + pub fn len(&self) -> usize { + self.stack.len() + } +} + +impl Default for CheckpointStack { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/plotnik-lib/src/engine/cursor.rs b/crates/plotnik-lib/src/engine/cursor.rs new file mode 100644 index 00000000..dc803fe0 --- /dev/null +++ b/crates/plotnik-lib/src/engine/cursor.rs @@ -0,0 +1,178 @@ +//! TreeCursor wrapper with Plotnik navigation semantics. +//! +//! The wrapper handles the search loop and skip policies defined +//! in docs/tree-navigation.md. + +use std::num::NonZeroU16; + +use arborium_tree_sitter::{Node, TreeCursor}; + +use crate::bytecode::Nav; + +/// Skip policy for navigation. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SkipPolicy { + /// Skip any nodes until match. + Any, + /// Skip trivia only (fail if non-trivia must be skipped). + Trivia, + /// No skipping allowed (exact match required). + Exact, +} + +/// Exit constraint for Up navigation. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum UpMode { + /// No constraint - just ascend. + Any, + /// Must be at last non-trivia child before ascending. + SkipTrivia, + /// Must be at last child before ascending. + Exact, +} + +/// Wrapper around TreeCursor with Plotnik navigation semantics. +/// +/// Critical: The cursor is created at tree root and never reset. +/// The `descendant_index` is relative to this root, enabling O(1) +/// checkpoint saves and O(depth) restores. +pub struct CursorWrapper<'t> { + cursor: TreeCursor<'t>, + /// Trivia node type IDs (for skip policies). + trivia_types: Vec, +} + +impl<'t> CursorWrapper<'t> { + /// Create a wrapper around a tree cursor. + /// + /// `trivia_types` is the list of node type IDs considered trivia + /// (e.g., comments, whitespace). + pub fn new(cursor: TreeCursor<'t>, trivia_types: Vec) -> Self { + Self { + cursor, + trivia_types, + } + } + + /// Get the current node. + #[inline] + pub fn node(&self) -> Node<'t> { + self.cursor.node() + } + + /// Get the current cursor position for checkpointing. + #[inline] + pub fn descendant_index(&self) -> u32 { + self.cursor.descendant_index() as u32 + } + + /// Restore cursor to a checkpointed position. + #[inline] + pub fn goto_descendant(&mut self, index: u32) { + self.cursor.goto_descendant(index as usize); + } + + /// Get the field ID of the current node (if any). + #[inline] + pub fn field_id(&self) -> Option { + self.cursor.field_id() + } + + /// Check if a node type is trivia. + #[inline] + pub fn is_trivia(&self, node: &Node<'_>) -> bool { + // Anonymous nodes are typically trivia (punctuation, operators) + !node.is_named() || self.trivia_types.contains(&node.kind_id()) + } + + /// Navigate according to Nav command, preparing for match attempt. + /// + /// Returns the skip policy to use for the subsequent match attempt, + /// or None if navigation failed (no children/siblings). + pub fn navigate(&mut self, nav: Nav) -> Option { + match nav { + Nav::Stay => Some(SkipPolicy::Any), + Nav::Down => self.go_first_child().then_some(SkipPolicy::Any), + Nav::DownSkip => self.go_first_child().then_some(SkipPolicy::Trivia), + Nav::DownExact => self.go_first_child().then_some(SkipPolicy::Exact), + Nav::Next => self.go_next_sibling().then_some(SkipPolicy::Any), + Nav::NextSkip => self.go_next_sibling().then_some(SkipPolicy::Trivia), + Nav::NextExact => self.go_next_sibling().then_some(SkipPolicy::Exact), + Nav::Up(n) => self.go_up(n, UpMode::Any).then_some(SkipPolicy::Any), + Nav::UpSkipTrivia(n) => self.go_up(n, UpMode::SkipTrivia).then_some(SkipPolicy::Any), + Nav::UpExact(n) => self.go_up(n, UpMode::Exact).then_some(SkipPolicy::Any), + } + } + + /// Move to first child. + fn go_first_child(&mut self) -> bool { + self.cursor.goto_first_child() + } + + /// Move to next sibling. + fn go_next_sibling(&mut self) -> bool { + self.cursor.goto_next_sibling() + } + + /// Ascend n levels with exit constraint. + fn go_up(&mut self, levels: u8, mode: UpMode) -> bool { + // Check exit constraint before ascending + match mode { + UpMode::Any => {} + UpMode::Exact => { + // Must be at last child + if self.cursor.goto_next_sibling() { + // Oops, there was a next sibling - restore position + self.cursor.goto_previous_sibling(); + return false; + } + } + UpMode::SkipTrivia => { + // Must be at last non-trivia child + // Save position + let saved = self.cursor.descendant_index(); + + // Look for non-trivia siblings after us + while self.cursor.goto_next_sibling() { + if !self.is_trivia(&self.cursor.node()) { + // Found non-trivia sibling - fail + self.cursor.goto_descendant(saved); + return false; + } + } + // Restore position + self.cursor.goto_descendant(saved); + } + } + + // Ascend n levels + for _ in 0..levels { + if !self.cursor.goto_parent() { + return false; + } + } + true + } + + /// Continue searching for a match with the given skip policy. + /// + /// This is called when a match attempt fails. It advances to the next + /// sibling based on the skip policy and returns whether to retry. + /// + /// - `Exact`: Return false (no skipping allowed) + /// - `Trivia`: Skip trivia siblings only, fail if non-trivia + /// - `Any`: Skip any siblings + pub fn continue_search(&mut self, policy: SkipPolicy) -> bool { + match policy { + SkipPolicy::Exact => false, + SkipPolicy::Trivia => { + // Fail if current node is non-trivia (we'd have to skip it) + if !self.is_trivia(&self.cursor.node()) { + return false; + } + self.cursor.goto_next_sibling() + } + SkipPolicy::Any => self.cursor.goto_next_sibling(), + } + } +} diff --git a/crates/plotnik-lib/src/engine/effect.rs b/crates/plotnik-lib/src/engine/effect.rs new file mode 100644 index 00000000..e8040dfa --- /dev/null +++ b/crates/plotnik-lib/src/engine/effect.rs @@ -0,0 +1,91 @@ +//! Runtime effects for VM execution. +//! +//! Runtime effects carry actual node references, unlike bytecode EffectOp +//! which only stores opcode + payload. + +use arborium_tree_sitter::Node; + +/// Runtime effect produced by VM execution. +/// +/// Unlike bytecode `EffectOp`, runtime effects carry actual node references +/// for materialization. Lifetime `'t` denotes the parsed tree-sitter tree. +#[derive(Debug)] +pub enum RuntimeEffect<'t> { + /// Capture a node reference. + Node(Node<'t>), + /// Extract source text from a node. + Text(Node<'t>), + /// Begin array scope. + Arr, + /// Push current value to array. + Push, + /// End array scope. + EndArr, + /// Begin object scope. + Obj, + /// Set field at member index. + Set(u16), + /// End object scope. + EndObj, + /// Begin enum variant at variant index. + Enum(u16), + /// End enum variant. + EndEnum, + /// Clear current value. + Clear, + /// Null placeholder (for optional/alternation). + Null, +} + +/// Effect log with truncation support for backtracking. +#[derive(Debug)] +pub struct EffectLog<'t>(Vec>); + +impl<'t> EffectLog<'t> { + /// Create an empty effect log. + pub fn new() -> Self { + Self(Vec::new()) + } + + /// Push an effect to the log. + #[inline] + pub fn push(&mut self, effect: RuntimeEffect<'t>) { + self.0.push(effect); + } + + /// Get current length (used as watermark for backtracking). + #[inline] + pub fn len(&self) -> usize { + self.0.len() + } + + /// Check if empty. + #[inline] + #[allow(dead_code)] + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Truncate to watermark (for backtracking). + #[inline] + pub fn truncate(&mut self, watermark: usize) { + self.0.truncate(watermark); + } + + /// Get effects as slice. + pub fn as_slice(&self) -> &[RuntimeEffect<'t>] { + &self.0 + } + + /// Consume into vec. + #[allow(dead_code)] + pub fn into_vec(self) -> Vec> { + self.0 + } +} + +impl Default for EffectLog<'_> { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/plotnik-lib/src/engine/engine_tests.rs b/crates/plotnik-lib/src/engine/engine_tests.rs new file mode 100644 index 00000000..db5dbb32 --- /dev/null +++ b/crates/plotnik-lib/src/engine/engine_tests.rs @@ -0,0 +1,369 @@ +//! VM execution tests with snapshot-based verification. +//! +//! Tests are organized by feature and use file-based snapshots. +//! Each test captures query, source, and JSON output. + +use indoc::indoc; + +use crate::bytecode::Module; +use crate::emit::emit_linked; +use crate::QueryBuilder; + +use super::{FuelLimits, Materializer, ValueMaterializer, VM}; + +/// Execute a query against source code and return the JSON output. +fn execute(query: &str, source: &str) -> String { + execute_with_entry(query, source, None) +} + +/// Execute a query against source code with a specific entrypoint. +fn execute_with_entry(query: &str, source: &str, entry: Option<&str>) -> String { + let lang = plotnik_langs::javascript(); + + let query_obj = QueryBuilder::one_liner(query) + .parse() + .expect("parse failed") + .analyze() + .link(&lang); + + assert!(query_obj.is_valid(), "query should be valid"); + + let bytecode = emit_linked(&query_obj).expect("emit failed"); + let module = Module::from_bytes(bytecode).expect("decode failed"); + + let tree = lang.parse(source); + let trivia = build_trivia_types(&module); + let vm = VM::new(&tree, trivia, FuelLimits::default()); + + let entrypoint = resolve_entrypoint(&module, entry); + let effects = vm.execute(&module, &entrypoint).expect("execution failed"); + + let materializer = ValueMaterializer::new(source, module.types(), module.strings()); + let value = materializer.materialize(effects.as_slice(), entrypoint.result_type); + + serde_json::to_string_pretty(&value).expect("json serialization failed") +} + +/// Build list of trivia node type IDs from module metadata. +fn build_trivia_types(module: &Module) -> Vec { + let node_types = module.node_types(); + let strings = module.strings(); + let mut trivia = Vec::new(); + for i in 0..node_types.len() { + let t = node_types.get(i); + let name = strings.get(t.name); + if name == "comment" { + trivia.push(t.id); + } + } + trivia +} + +/// Resolve entrypoint by name or use the default. +fn resolve_entrypoint( + module: &Module, + name: Option<&str>, +) -> crate::bytecode::Entrypoint { + let entrypoints = module.entrypoints(); + let strings = module.strings(); + + if let Some(name) = name { + for i in 0..entrypoints.len() { + let e = entrypoints.get(i); + if strings.get(e.name) == name { + return e; + } + } + panic!("entrypoint not found: {}", name); + } + + // Default: first entrypoint + entrypoints.get(0) +} + +macro_rules! snap { + ($query:expr, $source:expr) => {{ + let query = $query.trim(); + let source = $source.trim(); + let output = execute(query, source); + insta::with_settings!({ + omit_expression => true + }, { + insta::assert_snapshot!(format!("{query}\n---\n{source}\n---\n{output}")); + }); + }}; + ($query:expr, $source:expr, entry: $entry:expr) => {{ + let query = $query.trim(); + let source = $source.trim(); + let output = execute_with_entry(query, source, Some($entry)); + insta::with_settings!({ + omit_expression => true + }, { + insta::assert_snapshot!(format!("{query}\n---\n{source}\n---\n{output}")); + }); + }}; +} + +// ============================================================================ +// 1. SIMPLE CAPTURES +// ============================================================================ + +#[test] +fn capture_single() { + snap!( + "Q = (program (lexical_declaration (variable_declarator name: (identifier) @name)))", + "let x = 1" + ); +} + +#[test] +fn capture_multiple() { + snap!( + "Q = (program (lexical_declaration (variable_declarator name: (identifier) @name value: (number) @value)))", + "let x = 42" + ); +} + +#[test] +fn capture_string_annotation() { + snap!( + "Q = (program (lexical_declaration (variable_declarator name: (identifier) @name :: string)))", + "let myVar = 1" + ); +} + +// ============================================================================ +// 2. QUANTIFIERS +// ============================================================================ + +#[test] +fn quantifier_star() { + snap!( + "Q = (program (expression_statement (array (number)* @nums)))", + "[1, 2, 3]" + ); +} + +#[test] +fn quantifier_plus() { + snap!( + "Q = (program (expression_statement (array (number)+ @nums)))", + "[1, 2, 3]" + ); +} + +#[test] +fn quantifier_optional_present() { + snap!( + "Q = (program (lexical_declaration (variable_declarator name: (identifier) @name value: (number)? @value)))", + "let x = 42" + ); +} + +#[test] +fn quantifier_optional_absent() { + snap!( + "Q = (program (lexical_declaration (variable_declarator name: (identifier) @name value: (number)? @value)))", + "let x" + ); +} + +#[test] +fn quantifier_nongreedy_star() { + snap!( + "Q = (program (lexical_declaration (variable_declarator)*? @decls))", + "let a, b" + ); +} + +#[test] +fn quantifier_struct_array() { + snap!( + "Q = (program (lexical_declaration {(variable_declarator name: (identifier) @name) @decl}* @decls))", + "let a, b, c" + ); +} + +// ============================================================================ +// 3. ALTERNATIONS +// ============================================================================ + +#[test] +fn alternation_tagged_num() { + snap!( + indoc! {r#" + Value = [Ident: (identifier) @name Num: (number) @val] + Q = (program (lexical_declaration (variable_declarator value: (Value) @value))) + "#}, + "let x = 42", + entry: "Q" + ); +} + +#[test] +fn alternation_tagged_ident() { + snap!( + indoc! {r#" + Value = [Ident: (identifier) @name Num: (number) @val] + Q = (program (lexical_declaration (variable_declarator value: (Value) @value))) + "#}, + "let x = y", + entry: "Q" + ); +} + +#[test] +fn alternation_merge_num() { + snap!( + "Q = (program (lexical_declaration (variable_declarator value: [(identifier) @ident (number) @num])))", + "let x = 42" + ); +} + +#[test] +fn alternation_merge_ident() { + snap!( + "Q = (program (lexical_declaration (variable_declarator value: [(identifier) @ident (number) @num])))", + "let x = y" + ); +} + +// ============================================================================ +// 4. RECURSION +// ============================================================================ + +#[test] +fn recursion_member_chain() { + snap!( + indoc! {r#" + Chain = [Base: (identifier) @name Access: (member_expression object: (Chain) @base property: (property_identifier) @prop)] + Q = (program (expression_statement (Chain) @chain)) + "#}, + "a.b.c", + entry: "Q" + ); +} + +#[test] +fn recursion_nested_calls() { + snap!( + indoc! {r#" + Main = (program (expression_statement (Call))) + Call = (call_expression function: (identifier) @name arguments: (arguments (Call)? @inner)) + "#}, + "foo(bar())", + entry: "Main" + ); +} + +// ============================================================================ +// 5. ANCHORS +// ============================================================================ + +#[test] +fn anchor_first_child() { + snap!( + "Q = (program (lexical_declaration . (variable_declarator) @first))", + "let a, b, c" + ); +} + +#[test] +fn anchor_adjacency() { + snap!( + "Q = (program (lexical_declaration {(variable_declarator) @first . (variable_declarator) @second}))", + "let a, b, c" + ); +} + +// ============================================================================ +// 6. FIELDS +// ============================================================================ + +#[test] +fn field_negated_absent() { + snap!( + "Q = (program (lexical_declaration (variable_declarator name: (identifier) @name !value)))", + "let x" + ); +} + +// ============================================================================ +// 7. SEARCH BEHAVIOR +// ============================================================================ + +#[test] +fn search_skip_siblings() { + snap!( + "Q = (program (statement_block (return_statement) @ret))", + "{ foo(); bar(); return 1; }" + ); +} + +// ============================================================================ +// 8. REGRESSION TESTS +// ============================================================================ + +/// BUG #1: Scalar node arrays produced null values. +/// The issue was that `(identifier)* @args` captured an array of nulls instead +/// of actual node values because [Node, Push] effects were missing. +#[test] +fn regression_scalar_array_captures_nodes() { + snap!( + indoc! {r#" + Q = (program (expression_statement (call_expression + function: (identifier) @fn + arguments: (arguments (identifier)* @args)))) + "#}, + "foo(a, b, c)" + ); +} + +/// BUG #2: Tagged alternations panicked with "type member index out of bounds". +/// The issue was that enum types from tagged alternations weren't being collected +/// for bytecode emission when inside named nodes that don't propagate TypeFlow::Scalar. +#[test] +fn regression_tagged_alternation_materializes() { + snap!( + indoc! {r#" + Q = (program (expression_statement [ + Ident: (identifier) @x + Num: (number) @y + ])) + "#}, + "42" + ); +} + +/// BUG #3: Recursive patterns produced invalid JSON with duplicate keys. +/// The issue was that Call/Return didn't create proper Obj/EndObj scopes, so +/// recursive calls would flatten their captures into the same scope as the caller. +#[test] +fn regression_recursive_captures_nest_properly() { + snap!( + indoc! {r#" + Main = (program (expression_statement (Call))) + Call = (call_expression + function: (identifier) @name + arguments: (arguments (Call)? @inner)) + "#}, + "foo(bar())", + entry: "Main" + ); +} + +/// BUG #4: Call instructions didn't search for field constraints. +/// When a Call instruction has navigation with a field constraint, it should +/// continue searching (according to skip policy) until finding a node with the +/// required field, not immediately backtrack on the first mismatch. +#[test] +fn regression_call_searches_for_field_constraint() { + snap!( + indoc! {r#" + Expr = [Lit: (number) @val Binary: (binary_expression left: (Expr) @left right: (Expr) @right)] + Q = (program (expression_statement (Expr) @expr)) + "#}, + "1 + 2", + entry: "Q" + ); +} diff --git a/crates/plotnik-lib/src/engine/error.rs b/crates/plotnik-lib/src/engine/error.rs new file mode 100644 index 00000000..7c992a95 --- /dev/null +++ b/crates/plotnik-lib/src/engine/error.rs @@ -0,0 +1,31 @@ +//! Runtime errors for VM execution. + +use crate::bytecode::ModuleError; + +/// Errors during VM execution. +#[derive(Debug, thiserror::Error)] +pub enum RuntimeError { + /// Internal signal for successful completion (not a real error). + #[error("accept")] + Accept, + + /// Internal signal that backtracking occurred (control returns to main loop). + #[error("backtracked")] + Backtracked, + + #[error("execution fuel exhausted after {0} steps")] + ExecFuelExhausted(u32), + + #[error("recursion limit exceeded (depth {0})")] + RecursionLimitExceeded(u32), + + #[error("no match found")] + NoMatch, + + #[error("invalid entrypoint: {0}")] + #[allow(dead_code)] + InvalidEntrypoint(String), + + #[error("module error: {0}")] + Module(#[from] ModuleError), +} diff --git a/crates/plotnik-lib/src/engine/frame.rs b/crates/plotnik-lib/src/engine/frame.rs new file mode 100644 index 00000000..a0632da0 --- /dev/null +++ b/crates/plotnik-lib/src/engine/frame.rs @@ -0,0 +1,108 @@ +//! Call frame arena for recursion support. +//! +//! Implements the cactus stack pattern: frames are append-only, +//! with a current pointer that can be restored for backtracking. + +/// Call frame for recursion support. +#[derive(Clone, Copy, Debug)] +pub struct Frame { + /// Where to jump on Return (raw step index). + pub return_addr: u16, + /// Parent frame index (for cactus stack). + pub parent: Option, +} + +/// Append-only arena for frames (cactus stack implementation). +/// +/// Frames are never deallocated during execution - "pop" just moves +/// the current pointer. This allows checkpoint restoration without +/// invalidating frames referenced by other checkpoints. +#[derive(Debug)] +pub struct FrameArena { + frames: Vec, + current: Option, +} + +impl FrameArena { + /// Create an empty frame arena. + pub fn new() -> Self { + Self { + frames: Vec::new(), + current: None, + } + } + + /// Push a new frame, returns its index. + pub fn push(&mut self, return_addr: u16) -> u32 { + let idx = self.frames.len() as u32; + self.frames.push(Frame { + return_addr, + parent: self.current, + }); + self.current = Some(idx); + idx + } + + /// Pop the current frame, returning its return address. + /// + /// Panics if the stack is empty. + pub fn pop(&mut self) -> u16 { + let current_idx = self.current.expect("pop on empty frame stack"); + let frame = self.frames[current_idx as usize]; + self.current = frame.parent; + frame.return_addr + } + + /// Restore frame state for backtracking. + #[inline] + pub fn restore(&mut self, frame_index: Option) { + self.current = frame_index; + } + + /// Get current frame index. + #[inline] + pub fn current(&self) -> Option { + self.current + } + + /// Check if frame stack is empty. + #[inline] + pub fn is_empty(&self) -> bool { + self.current.is_none() + } + + /// Get current call depth. + #[allow(dead_code)] + pub fn depth(&self) -> u32 { + let mut depth = 0; + let mut idx = self.current; + while let Some(i) = idx { + depth += 1; + idx = self.frames[i as usize].parent; + } + depth + } + + /// Prune frames above high-water mark. + /// + /// Frames are only pruned after Return, when we know no checkpoint + /// references them. The `max_referenced` is the highest frame index + /// still referenced by any active checkpoint. + pub fn prune(&mut self, max_referenced: Option) { + // Keep frames up to max(current, max_referenced) + let keep = match (self.current, max_referenced) { + (Some(a), Some(b)) => Some(a.max(b)), + (a, b) => a.or(b), + }; + + if let Some(high_water) = keep { + self.frames.truncate(high_water as usize + 1); + } + } +} + +impl Default for FrameArena { + fn default() -> Self { + Self::new() + } +} diff --git a/crates/plotnik-lib/src/engine/materializer.rs b/crates/plotnik-lib/src/engine/materializer.rs new file mode 100644 index 00000000..264c88ed --- /dev/null +++ b/crates/plotnik-lib/src/engine/materializer.rs @@ -0,0 +1,159 @@ +//! Materializer transforms effect logs into output values. + +use crate::bytecode::{QTypeId, StringsView, TypeKind, TypesView}; + +use super::effect::RuntimeEffect; +use super::value::{NodeHandle, Value}; + +/// Materializer transforms effect logs into output values. +pub trait Materializer<'t> { + type Output; + + fn materialize(&self, effects: &[RuntimeEffect<'t>], result_type: QTypeId) -> Self::Output; +} + +/// Materializer that produces Value with resolved strings. +pub struct ValueMaterializer<'ctx> { + source: &'ctx str, + types: TypesView<'ctx>, + strings: StringsView<'ctx>, +} + +impl<'ctx> ValueMaterializer<'ctx> { + pub fn new(source: &'ctx str, types: TypesView<'ctx>, strings: StringsView<'ctx>) -> Self { + Self { + source, + types, + strings, + } + } + + fn resolve_member_name(&self, idx: u16) -> String { + let member = self.types.get_member(idx as usize); + self.strings.get(member.name).to_owned() + } + + /// Create initial builder based on result type. + fn builder_for_type(&self, type_id: QTypeId) -> Builder { + let def = match self.types.get(type_id) { + Some(d) => d, + None => return Builder::Scalar(None), + }; + + match TypeKind::from_u8(def.kind) { + Some(TypeKind::Struct) => Builder::Object(vec![]), + Some(TypeKind::Enum) => Builder::Scalar(None), // Enum gets built when Enum effect comes + Some(TypeKind::ArrayZeroOrMore | TypeKind::ArrayOneOrMore) => Builder::Array(vec![]), + _ => Builder::Scalar(None), + } + } +} + +/// Value builder for stack-based materialization. +enum Builder { + Scalar(Option), + Array(Vec), + Object(Vec<(String, Value)>), + Tagged { tag: String, fields: Vec<(String, Value)> }, +} + +impl Builder { + fn build(self) -> Value { + match self { + Builder::Scalar(v) => v.unwrap_or(Value::Null), + Builder::Array(arr) => Value::Array(arr), + Builder::Object(fields) => Value::Object(fields), + Builder::Tagged { tag, fields } => Value::Tagged { + tag, + data: Box::new(Value::Object(fields)), + }, + } + } +} + +impl<'t> Materializer<'t> for ValueMaterializer<'_> { + type Output = Value; + + fn materialize(&self, effects: &[RuntimeEffect<'t>], result_type: QTypeId) -> Value { + // Stack of containers being built + let mut stack: Vec = vec![]; + + // Initialize with result type container + let result_builder = self.builder_for_type(result_type); + stack.push(result_builder); + + // Pending value from Node/Text/Null (consumed by Set/Push) + let mut pending: Option = None; + + for effect in effects { + match effect { + RuntimeEffect::Node(n) => { + pending = Some(Value::Node(NodeHandle::from_node(*n, self.source))); + } + RuntimeEffect::Text(n) => { + let text = n + .utf8_text(self.source.as_bytes()) + .expect("invalid UTF-8") + .to_owned(); + pending = Some(Value::String(text)); + } + RuntimeEffect::Null => { + pending = Some(Value::Null); + } + RuntimeEffect::Arr => { + stack.push(Builder::Array(vec![])); + } + RuntimeEffect::Push => { + // Take pending value (or completed container) and push to parent array + let val = pending.take().unwrap_or(Value::Null); + if let Some(Builder::Array(arr)) = stack.last_mut() { + arr.push(val); + } + } + RuntimeEffect::EndArr => { + if let Some(Builder::Array(arr)) = stack.pop() { + pending = Some(Value::Array(arr)); + } + } + RuntimeEffect::Obj => { + stack.push(Builder::Object(vec![])); + } + RuntimeEffect::Set(idx) => { + let field_name = self.resolve_member_name(*idx); + let val = pending.take().unwrap_or(Value::Null); + // Set works on both Object and Tagged (enum variant data) + match stack.last_mut() { + Some(Builder::Object(obj)) => obj.push((field_name, val)), + Some(Builder::Tagged { fields, .. }) => fields.push((field_name, val)), + _ => {} + } + } + RuntimeEffect::EndObj => { + if let Some(Builder::Object(fields)) = stack.pop() { + pending = Some(Value::Object(fields)); + } + } + RuntimeEffect::Enum(idx) => { + let tag = self.resolve_member_name(*idx); + stack.push(Builder::Tagged { tag, fields: vec![] }); + } + RuntimeEffect::EndEnum => { + if let Some(Builder::Tagged { tag, fields }) = stack.pop() { + pending = Some(Value::Tagged { + tag, + data: Box::new(Value::Object(fields)), + }); + } + } + RuntimeEffect::Clear => { + pending = None; + } + } + } + + // Result: pending value takes precedence, otherwise pop the result container + pending + .or_else(|| stack.pop().map(Builder::build)) + .unwrap_or(Value::Null) + } +} diff --git a/crates/plotnik-lib/src/engine/mod.rs b/crates/plotnik-lib/src/engine/mod.rs new file mode 100644 index 00000000..78f4247e --- /dev/null +++ b/crates/plotnik-lib/src/engine/mod.rs @@ -0,0 +1,24 @@ +//! Runtime engine for executing compiled Plotnik queries. +//! +//! The VM executes bytecode against tree-sitter syntax trees, +//! producing an effect log that can be materialized into output values. + +mod checkpoint; +mod cursor; +mod effect; +mod error; +mod frame; +mod materializer; +mod trace; +mod value; +mod vm; + +#[cfg(test)] +mod engine_tests; + +pub use effect::{EffectLog, RuntimeEffect}; +pub use error::RuntimeError; +pub use materializer::{Materializer, ValueMaterializer}; +pub use trace::{PrintTracer, Tracer, Verbosity}; +pub use value::{NodeHandle, Value}; +pub use vm::{FuelLimits, VM}; diff --git a/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__alternation_merge_ident.snap b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__alternation_merge_ident.snap new file mode 100644 index 00000000..ac0a0dee --- /dev/null +++ b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__alternation_merge_ident.snap @@ -0,0 +1,18 @@ +--- +source: crates/plotnik-lib/src/engine/engine_tests.rs +--- +Q = (program (lexical_declaration (variable_declarator value: [(identifier) @ident (number) @num]))) +--- +let x = y +--- +{ + "num": null, + "ident": { + "kind": "identifier", + "text": "y", + "span": [ + 8, + 9 + ] + } +} diff --git a/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__alternation_merge_num.snap b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__alternation_merge_num.snap new file mode 100644 index 00000000..61b28b2c --- /dev/null +++ b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__alternation_merge_num.snap @@ -0,0 +1,18 @@ +--- +source: crates/plotnik-lib/src/engine/engine_tests.rs +--- +Q = (program (lexical_declaration (variable_declarator value: [(identifier) @ident (number) @num]))) +--- +let x = 42 +--- +{ + "ident": null, + "num": { + "kind": "number", + "text": "42", + "span": [ + 8, + 10 + ] + } +} diff --git a/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__alternation_tagged_ident.snap b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__alternation_tagged_ident.snap new file mode 100644 index 00000000..2778c14a --- /dev/null +++ b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__alternation_tagged_ident.snap @@ -0,0 +1,23 @@ +--- +source: crates/plotnik-lib/src/engine/engine_tests.rs +--- +Value = [Ident: (identifier) @name Num: (number) @val] +Q = (program (lexical_declaration (variable_declarator value: (Value) @value))) +--- +let x = y +--- +{ + "value": { + "$tag": "Ident", + "$data": { + "name": { + "kind": "identifier", + "text": "y", + "span": [ + 8, + 9 + ] + } + } + } +} diff --git a/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__alternation_tagged_num.snap b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__alternation_tagged_num.snap new file mode 100644 index 00000000..6696aa5b --- /dev/null +++ b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__alternation_tagged_num.snap @@ -0,0 +1,23 @@ +--- +source: crates/plotnik-lib/src/engine/engine_tests.rs +--- +Value = [Ident: (identifier) @name Num: (number) @val] +Q = (program (lexical_declaration (variable_declarator value: (Value) @value))) +--- +let x = 42 +--- +{ + "value": { + "$tag": "Num", + "$data": { + "val": { + "kind": "number", + "text": "42", + "span": [ + 8, + 10 + ] + } + } + } +} diff --git a/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__anchor_adjacency.snap b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__anchor_adjacency.snap new file mode 100644 index 00000000..6fb66b1c --- /dev/null +++ b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__anchor_adjacency.snap @@ -0,0 +1,25 @@ +--- +source: crates/plotnik-lib/src/engine/engine_tests.rs +--- +Q = (program (lexical_declaration {(variable_declarator) @first . (variable_declarator) @second})) +--- +let a, b, c +--- +{ + "first": { + "kind": "variable_declarator", + "text": "a", + "span": [ + 4, + 5 + ] + }, + "second": { + "kind": "variable_declarator", + "text": "b", + "span": [ + 7, + 8 + ] + } +} diff --git a/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__anchor_first_child.snap b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__anchor_first_child.snap new file mode 100644 index 00000000..eb080723 --- /dev/null +++ b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__anchor_first_child.snap @@ -0,0 +1,17 @@ +--- +source: crates/plotnik-lib/src/engine/engine_tests.rs +--- +Q = (program (lexical_declaration . (variable_declarator) @first)) +--- +let a, b, c +--- +{ + "first": { + "kind": "variable_declarator", + "text": "a", + "span": [ + 4, + 5 + ] + } +} diff --git a/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__capture_multiple.snap b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__capture_multiple.snap new file mode 100644 index 00000000..79a45cd6 --- /dev/null +++ b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__capture_multiple.snap @@ -0,0 +1,25 @@ +--- +source: crates/plotnik-lib/src/engine/engine_tests.rs +--- +Q = (program (lexical_declaration (variable_declarator name: (identifier) @name value: (number) @value))) +--- +let x = 42 +--- +{ + "name": { + "kind": "identifier", + "text": "x", + "span": [ + 4, + 5 + ] + }, + "value": { + "kind": "number", + "text": "42", + "span": [ + 8, + 10 + ] + } +} diff --git a/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__capture_single.snap b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__capture_single.snap new file mode 100644 index 00000000..b43d25b2 --- /dev/null +++ b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__capture_single.snap @@ -0,0 +1,17 @@ +--- +source: crates/plotnik-lib/src/engine/engine_tests.rs +--- +Q = (program (lexical_declaration (variable_declarator name: (identifier) @name))) +--- +let x = 1 +--- +{ + "name": { + "kind": "identifier", + "text": "x", + "span": [ + 4, + 5 + ] + } +} diff --git a/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__capture_string_annotation.snap b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__capture_string_annotation.snap new file mode 100644 index 00000000..de18e12d --- /dev/null +++ b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__capture_string_annotation.snap @@ -0,0 +1,10 @@ +--- +source: crates/plotnik-lib/src/engine/engine_tests.rs +--- +Q = (program (lexical_declaration (variable_declarator name: (identifier) @name :: string))) +--- +let myVar = 1 +--- +{ + "name": "myVar" +} diff --git a/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__field_negated_absent.snap b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__field_negated_absent.snap new file mode 100644 index 00000000..1b88f09b --- /dev/null +++ b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__field_negated_absent.snap @@ -0,0 +1,17 @@ +--- +source: crates/plotnik-lib/src/engine/engine_tests.rs +--- +Q = (program (lexical_declaration (variable_declarator name: (identifier) @name !value))) +--- +let x +--- +{ + "name": { + "kind": "identifier", + "text": "x", + "span": [ + 4, + 5 + ] + } +} diff --git a/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__quantifier_nongreedy_star.snap b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__quantifier_nongreedy_star.snap new file mode 100644 index 00000000..0d80e1ea --- /dev/null +++ b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__quantifier_nongreedy_star.snap @@ -0,0 +1,10 @@ +--- +source: crates/plotnik-lib/src/engine/engine_tests.rs +--- +Q = (program (lexical_declaration (variable_declarator)*? @decls)) +--- +let a, b +--- +{ + "decls": [] +} diff --git a/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__quantifier_optional_absent.snap b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__quantifier_optional_absent.snap new file mode 100644 index 00000000..74c2afcc --- /dev/null +++ b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__quantifier_optional_absent.snap @@ -0,0 +1,18 @@ +--- +source: crates/plotnik-lib/src/engine/engine_tests.rs +--- +Q = (program (lexical_declaration (variable_declarator name: (identifier) @name value: (number)? @value))) +--- +let x +--- +{ + "name": { + "kind": "identifier", + "text": "x", + "span": [ + 4, + 5 + ] + }, + "value": null +} diff --git a/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__quantifier_optional_present.snap b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__quantifier_optional_present.snap new file mode 100644 index 00000000..5d519b97 --- /dev/null +++ b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__quantifier_optional_present.snap @@ -0,0 +1,25 @@ +--- +source: crates/plotnik-lib/src/engine/engine_tests.rs +--- +Q = (program (lexical_declaration (variable_declarator name: (identifier) @name value: (number)? @value))) +--- +let x = 42 +--- +{ + "name": { + "kind": "identifier", + "text": "x", + "span": [ + 4, + 5 + ] + }, + "value": { + "kind": "number", + "text": "42", + "span": [ + 8, + 10 + ] + } +} diff --git a/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__quantifier_plus.snap b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__quantifier_plus.snap new file mode 100644 index 00000000..278236af --- /dev/null +++ b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__quantifier_plus.snap @@ -0,0 +1,35 @@ +--- +source: crates/plotnik-lib/src/engine/engine_tests.rs +--- +Q = (program (expression_statement (array (number)+ @nums))) +--- +[1, 2, 3] +--- +{ + "nums": [ + { + "kind": "number", + "text": "1", + "span": [ + 1, + 2 + ] + }, + { + "kind": "number", + "text": "2", + "span": [ + 4, + 5 + ] + }, + { + "kind": "number", + "text": "3", + "span": [ + 7, + 8 + ] + } + ] +} diff --git a/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__quantifier_star.snap b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__quantifier_star.snap new file mode 100644 index 00000000..d212145b --- /dev/null +++ b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__quantifier_star.snap @@ -0,0 +1,35 @@ +--- +source: crates/plotnik-lib/src/engine/engine_tests.rs +--- +Q = (program (expression_statement (array (number)* @nums))) +--- +[1, 2, 3] +--- +{ + "nums": [ + { + "kind": "number", + "text": "1", + "span": [ + 1, + 2 + ] + }, + { + "kind": "number", + "text": "2", + "span": [ + 4, + 5 + ] + }, + { + "kind": "number", + "text": "3", + "span": [ + 7, + 8 + ] + } + ] +} diff --git a/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__quantifier_struct_array.snap b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__quantifier_struct_array.snap new file mode 100644 index 00000000..37609f1c --- /dev/null +++ b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__quantifier_struct_array.snap @@ -0,0 +1,65 @@ +--- +source: crates/plotnik-lib/src/engine/engine_tests.rs +--- +Q = (program (lexical_declaration {(variable_declarator name: (identifier) @name) @decl}* @decls)) +--- +let a, b, c +--- +{ + "decls": [ + { + "decl": { + "kind": "variable_declarator", + "text": "a", + "span": [ + 4, + 5 + ] + }, + "name": { + "kind": "identifier", + "text": "a", + "span": [ + 4, + 5 + ] + } + }, + { + "decl": { + "kind": "variable_declarator", + "text": "b", + "span": [ + 7, + 8 + ] + }, + "name": { + "kind": "identifier", + "text": "b", + "span": [ + 7, + 8 + ] + } + }, + { + "decl": { + "kind": "variable_declarator", + "text": "c", + "span": [ + 10, + 11 + ] + }, + "name": { + "kind": "identifier", + "text": "c", + "span": [ + 10, + 11 + ] + } + } + ] +} diff --git a/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__recursion_member_chain.snap b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__recursion_member_chain.snap new file mode 100644 index 00000000..eb0ff868 --- /dev/null +++ b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__recursion_member_chain.snap @@ -0,0 +1,49 @@ +--- +source: crates/plotnik-lib/src/engine/engine_tests.rs +--- +Chain = [Base: (identifier) @name Access: (member_expression object: (Chain) @base property: (property_identifier) @prop)] +Q = (program (expression_statement (Chain) @chain)) +--- +a.b.c +--- +{ + "chain": { + "$tag": "Access", + "$data": { + "base": { + "$tag": "Access", + "$data": { + "base": { + "$tag": "Base", + "$data": { + "name": { + "kind": "identifier", + "text": "a", + "span": [ + 0, + 1 + ] + } + } + }, + "prop": { + "kind": "property_identifier", + "text": "b", + "span": [ + 2, + 3 + ] + } + } + }, + "prop": { + "kind": "property_identifier", + "text": "c", + "span": [ + 4, + 5 + ] + } + } + } +} diff --git a/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__recursion_nested_calls.snap b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__recursion_nested_calls.snap new file mode 100644 index 00000000..c473a232 --- /dev/null +++ b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__recursion_nested_calls.snap @@ -0,0 +1,29 @@ +--- +source: crates/plotnik-lib/src/engine/engine_tests.rs +--- +Main = (program (expression_statement (Call))) +Call = (call_expression function: (identifier) @name arguments: (arguments (Call)? @inner)) +--- +foo(bar()) +--- +{ + "name": { + "kind": "identifier", + "text": "foo", + "span": [ + 0, + 3 + ] + }, + "inner": { + "name": { + "kind": "identifier", + "text": "bar", + "span": [ + 4, + 7 + ] + }, + "inner": null + } +} diff --git a/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__regression_call_searches_for_field_constraint.snap b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__regression_call_searches_for_field_constraint.snap new file mode 100644 index 00000000..88cd4d3a --- /dev/null +++ b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__regression_call_searches_for_field_constraint.snap @@ -0,0 +1,41 @@ +--- +source: crates/plotnik-lib/src/engine/engine_tests.rs +--- +Expr = [Lit: (number) @val Binary: (binary_expression left: (Expr) @left right: (Expr) @right)] +Q = (program (expression_statement (Expr) @expr)) +--- +1 + 2 +--- +{ + "expr": { + "$tag": "Binary", + "$data": { + "left": { + "$tag": "Lit", + "$data": { + "val": { + "kind": "number", + "text": "1", + "span": [ + 0, + 1 + ] + } + } + }, + "right": { + "$tag": "Lit", + "$data": { + "val": { + "kind": "number", + "text": "2", + "span": [ + 4, + 5 + ] + } + } + } + } + } +} diff --git a/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__regression_recursive_captures_nest_properly.snap b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__regression_recursive_captures_nest_properly.snap new file mode 100644 index 00000000..1ecd8ec1 --- /dev/null +++ b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__regression_recursive_captures_nest_properly.snap @@ -0,0 +1,31 @@ +--- +source: crates/plotnik-lib/src/engine/engine_tests.rs +--- +Main = (program (expression_statement (Call))) +Call = (call_expression + function: (identifier) @name + arguments: (arguments (Call)? @inner)) +--- +foo(bar()) +--- +{ + "name": { + "kind": "identifier", + "text": "foo", + "span": [ + 0, + 3 + ] + }, + "inner": { + "name": { + "kind": "identifier", + "text": "bar", + "span": [ + 4, + 7 + ] + }, + "inner": null + } +} diff --git a/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__regression_scalar_array_captures_nodes.snap b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__regression_scalar_array_captures_nodes.snap new file mode 100644 index 00000000..9703605b --- /dev/null +++ b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__regression_scalar_array_captures_nodes.snap @@ -0,0 +1,45 @@ +--- +source: crates/plotnik-lib/src/engine/engine_tests.rs +--- +Q = (program (expression_statement (call_expression + function: (identifier) @fn + arguments: (arguments (identifier)* @args)))) +--- +foo(a, b, c) +--- +{ + "fn": { + "kind": "identifier", + "text": "foo", + "span": [ + 0, + 3 + ] + }, + "args": [ + { + "kind": "identifier", + "text": "a", + "span": [ + 4, + 5 + ] + }, + { + "kind": "identifier", + "text": "b", + "span": [ + 7, + 8 + ] + }, + { + "kind": "identifier", + "text": "c", + "span": [ + 10, + 11 + ] + } + ] +} diff --git a/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__regression_tagged_alternation_materializes.snap b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__regression_tagged_alternation_materializes.snap new file mode 100644 index 00000000..51532f47 --- /dev/null +++ b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__regression_tagged_alternation_materializes.snap @@ -0,0 +1,23 @@ +--- +source: crates/plotnik-lib/src/engine/engine_tests.rs +--- +Q = (program (expression_statement [ + Ident: (identifier) @x + Num: (number) @y +])) +--- +42 +--- +{ + "$tag": "Num", + "$data": { + "y": { + "kind": "number", + "text": "42", + "span": [ + 0, + 2 + ] + } + } +} diff --git a/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__search_skip_siblings.snap b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__search_skip_siblings.snap new file mode 100644 index 00000000..48cd1da8 --- /dev/null +++ b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__search_skip_siblings.snap @@ -0,0 +1,17 @@ +--- +source: crates/plotnik-lib/src/engine/engine_tests.rs +--- +Q = (program (statement_block (return_statement) @ret)) +--- +{ foo(); bar(); return 1; } +--- +{ + "ret": { + "kind": "return_statement", + "text": "return 1;", + "span": [ + 16, + 25 + ] + } +} diff --git a/crates/plotnik-lib/src/engine/trace.rs b/crates/plotnik-lib/src/engine/trace.rs new file mode 100644 index 00000000..eebe0246 --- /dev/null +++ b/crates/plotnik-lib/src/engine/trace.rs @@ -0,0 +1,493 @@ +//! Tracing infrastructure for debugging VM execution. +//! +//! # Design: Zero-Cost Abstraction +//! +//! The tracer is designed as a zero-cost abstraction. When `NoopTracer` is used: +//! - All trait methods are `#[inline(always)]` empty functions +//! - The compiler eliminates all tracer calls and their arguments +//! - No tracing-related state exists in core execution structures +//! +//! # Design: Tracer-Owned State +//! +//! Tracing-only state (like checkpoint creation IPs for backtrack display) is +//! maintained by the tracer itself, not in core structures like `Checkpoint`. +//! This keeps execution structures minimal and avoids "spilling" tracing concerns +//! into `exec`. For example: +//! - `trace_checkpoint_created(ip)` - tracer pushes to its own stack +//! - `trace_backtrack()` - tracer pops its stack to get the display IP +//! +//! `NoopTracer` ignores these calls (optimized away), while `PrintTracer` +//! maintains parallel state for display purposes. + +use std::num::NonZeroU16; + +use arborium_tree_sitter::Node; + +use crate::bytecode::{ + format_effect, nav_symbol, trace, truncate_text, width_for_count, InstructionView, + LineBuilder, MatchView, Module, Nav, Symbol, +}; +use crate::Colors; + +use super::effect::RuntimeEffect; + +/// Verbosity level for trace output. +/// +/// Controls which sub-lines are shown and whether node text is included. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub enum Verbosity { + /// Default: match, backtrack, call/return. Kind only, no text. + #[default] + Default, + /// Verbose (-v): all sub-lines. Text on match/failure. + Verbose, + /// Very verbose (-vv): all sub-lines. Text on everything including nav. + VeryVerbose, +} + +/// Tracer trait for VM execution instrumentation. +/// +/// All methods receive raw data (IDs, nodes) that the VM already has. +/// Formatting and name resolution happen in the tracer implementation. +/// +/// Each method is called at a specific point during execution: +/// - `trace_instruction` - before executing an instruction +/// - `trace_nav` - after navigation succeeds +/// - `trace_match_success/failure` - after type check +/// - `trace_field_success/failure` - after field check +/// - `trace_effect` - after emitting an effect +/// - `trace_call` - when entering a definition +/// - `trace_return` - when returning from a definition +/// - `trace_checkpoint_created` - when a checkpoint is pushed +/// - `trace_backtrack` - when restoring a checkpoint +/// - `trace_enter_entrypoint` - when entering an entrypoint (for labels) +pub trait Tracer { + /// Called before executing an instruction. + fn trace_instruction(&mut self, ip: u16, instr: &InstructionView<'_>); + + /// Called after navigation succeeds. + fn trace_nav(&mut self, nav: Nav, node: Node<'_>); + + /// Called after type check succeeds. + fn trace_match_success(&mut self, node: Node<'_>); + + /// Called after type check fails. + fn trace_match_failure(&mut self, node: Node<'_>); + + /// Called after field check succeeds. + fn trace_field_success(&mut self, field_id: NonZeroU16); + + /// Called after field check fails. + fn trace_field_failure(&mut self, node: Node<'_>); + + /// Called after emitting an effect. + fn trace_effect(&mut self, effect: &RuntimeEffect<'_>); + + /// Called when entering a definition via Call. + fn trace_call(&mut self, target_ip: u16); + + /// Called when returning from a definition. + fn trace_return(&mut self); + + /// Called when a checkpoint is created. + fn trace_checkpoint_created(&mut self, ip: u16); + + /// Called when backtracking occurs. + fn trace_backtrack(&mut self); + + /// Called when entering an entrypoint (for section labels). + fn trace_enter_entrypoint(&mut self, target_ip: u16); +} + +/// No-op tracer that gets optimized away completely. +pub struct NoopTracer; + +impl Tracer for NoopTracer { + #[inline(always)] + fn trace_instruction(&mut self, _ip: u16, _instr: &InstructionView<'_>) {} + + #[inline(always)] + fn trace_nav(&mut self, _nav: Nav, _node: Node<'_>) {} + + #[inline(always)] + fn trace_match_success(&mut self, _node: Node<'_>) {} + + #[inline(always)] + fn trace_match_failure(&mut self, _node: Node<'_>) {} + + #[inline(always)] + fn trace_field_success(&mut self, _field_id: NonZeroU16) {} + + #[inline(always)] + fn trace_field_failure(&mut self, _node: Node<'_>) {} + + #[inline(always)] + fn trace_effect(&mut self, _effect: &RuntimeEffect<'_>) {} + + #[inline(always)] + fn trace_call(&mut self, _target_ip: u16) {} + + #[inline(always)] + fn trace_return(&mut self) {} + + #[inline(always)] + fn trace_checkpoint_created(&mut self, _ip: u16) {} + + #[inline(always)] + fn trace_backtrack(&mut self) {} + + #[inline(always)] + fn trace_enter_entrypoint(&mut self, _target_ip: u16) {} +} + +use std::collections::BTreeMap; + +/// Tracer that collects execution trace for debugging. +pub struct PrintTracer<'s> { + /// Source code for extracting node text. + source: &'s [u8], + /// Verbosity level for output filtering. + verbosity: Verbosity, + /// Collected trace lines. + lines: Vec, + /// Line builder for formatting. + builder: LineBuilder, + /// Maps node type ID to name. + node_type_names: BTreeMap, + /// Maps node field ID to name. + node_field_names: BTreeMap, + /// Maps member index to name (for Set/Enum effect display). + member_names: Vec, + /// Maps entrypoint target IP to name (for labels and call/return). + entrypoint_by_ip: BTreeMap, + /// Parallel stack of checkpoint creation IPs (for backtrack display). + checkpoint_ips: Vec, + /// Stack of definition names (for return display). + definition_stack: Vec, + /// Step width for formatting. + step_width: usize, + /// Color palette. + colors: Colors, +} + +impl<'s> PrintTracer<'s> { + pub fn new(source: &'s str, module: &Module, verbosity: Verbosity, colors: Colors) -> Self { + let header = module.header(); + let strings = module.strings(); + let types = module.types(); + let node_types = module.node_types(); + let node_fields = module.node_fields(); + let entrypoints = module.entrypoints(); + + let mut node_type_names = BTreeMap::new(); + for i in 0..node_types.len() { + let t = node_types.get(i); + node_type_names.insert(t.id, strings.get(t.name).to_string()); + } + + let mut node_field_names = BTreeMap::new(); + for i in 0..node_fields.len() { + let f = node_fields.get(i); + node_field_names.insert(f.id, strings.get(f.name).to_string()); + } + + // Build member names lookup (index → name) + let member_names: Vec = (0..types.members_count()) + .map(|i| strings.get(types.get_member(i).name).to_string()) + .collect(); + + // Build entrypoint IP → name lookup + let mut entrypoint_by_ip = BTreeMap::new(); + for i in 0..entrypoints.len() { + let e = entrypoints.get(i); + entrypoint_by_ip.insert(e.target.get(), strings.get(e.name).to_string()); + } + + let step_width = width_for_count(header.transitions_count as usize); + + Self { + source: source.as_bytes(), + verbosity, + lines: Vec::new(), + builder: LineBuilder::new(step_width), + node_type_names, + node_field_names, + member_names, + entrypoint_by_ip, + checkpoint_ips: Vec::new(), + definition_stack: Vec::new(), + step_width, + colors, + } + } + + fn node_type_name(&self, id: u16) -> &str { + self.node_type_names.get(&id).map_or("?", |s| s.as_str()) + } + + fn node_field_name(&self, id: u16) -> &str { + self.node_field_names.get(&id).map_or("?", |s| s.as_str()) + } + + fn member_name(&self, idx: u16) -> &str { + self.member_names + .get(idx as usize) + .map_or("?", |s| s.as_str()) + } + + fn entrypoint_name(&self, ip: u16) -> &str { + self.entrypoint_by_ip.get(&ip).map_or("?", |s| s.as_str()) + } + + /// Format a runtime effect for display. + fn format_effect(&self, effect: &RuntimeEffect<'_>) -> String { + use RuntimeEffect::*; + match effect { + Node(_) => "Node".to_string(), + Text(_) => "Text".to_string(), + Arr => "Arr".to_string(), + Push => "Push".to_string(), + EndArr => "EndArr".to_string(), + Obj => "Obj".to_string(), + EndObj => "EndObj".to_string(), + Set(idx) => format!("Set \"{}\"", self.member_name(*idx)), + Enum(idx) => format!("Enum \"{}\"", self.member_name(*idx)), + EndEnum => "EndEnum".to_string(), + Clear => "Clear".to_string(), + Null => "Null".to_string(), + } + } + + /// Format match content for instruction line (matches dump format exactly). + /// + /// Order: [pre-effects] !neg_fields field: (type) [post-effects] + fn format_match_content(&self, m: &MatchView<'_>) -> String { + let mut parts = Vec::new(); + + // Pre-effects: [Effect1 Effect2] + let pre: Vec<_> = m.pre_effects().map(|e| format_effect(&e)).collect(); + if !pre.is_empty() { + parts.push(format!("[{}]", pre.join(" "))); + } + + // Negated fields: !field1 !field2 + for field_id in m.neg_fields() { + let name = self.node_field_name(field_id); + parts.push(format!("!{name}")); + } + + // Node pattern: field: (type) / (type) / field: _ / empty + let node_part = self.format_node_pattern(m); + if !node_part.is_empty() { + parts.push(node_part); + } + + // Post-effects: [Effect1 Effect2] + let post: Vec<_> = m.post_effects().map(|e| format_effect(&e)).collect(); + if !post.is_empty() { + parts.push(format!("[{}]", post.join(" "))); + } + + parts.join(" ") + } + + /// Format node pattern: `field: (type)` or `(type)` or `field: _` or empty. + fn format_node_pattern(&self, m: &MatchView<'_>) -> String { + let mut result = String::new(); + + if let Some(f) = m.node_field { + result.push_str(self.node_field_name(f.get())); + result.push_str(": "); + } + + if let Some(t) = m.node_type { + result.push('('); + result.push_str(self.node_type_name(t.get())); + result.push(')'); + } else if m.node_field.is_some() { + result.push('_'); + } + + result + } + + /// Print all trace lines. + pub fn print(&self) { + for line in &self.lines { + println!("{}", line); + } + } + + /// Add an instruction line. + fn add_instruction(&mut self, ip: u16, symbol: Symbol, content: &str, successors: &str) { + let prefix = format!(" {:0sw$} {} ", ip, symbol.format(), sw = self.step_width); + let line = self.builder.pad_successors(format!("{prefix}{content}"), successors); + self.lines.push(line); + } + + /// Add a sub-line (blank step area + symbol + content). + fn add_subline(&mut self, symbol: Symbol, content: &str) { + let step_area = 2 + self.step_width + 1; + let prefix = format!("{:step_area$}{} ", "", symbol.format()); + self.lines.push(format!("{prefix}{content}")); + } +} + +impl Tracer for PrintTracer<'_> { + fn trace_instruction(&mut self, ip: u16, instr: &InstructionView<'_>) { + let colors = &self.colors; + match instr { + InstructionView::Match(m) => { + let symbol = format_match_symbol(m); + let content = self.format_match_content(m); + let successors = format_match_successors(m); + self.add_instruction(ip, symbol, &content, &successors); + } + InstructionView::Call(c) => { + let name = self.entrypoint_name(c.target.get()); + let symbol = nav_symbol(c.nav); + // Definition name in blue + let content = format!("({}{}{})", colors.blue, name, colors.reset); + let successors = format!("{:02} ⯇", c.next.get()); + self.add_instruction(ip, symbol, &content, &successors); + } + InstructionView::Return(_) => { + self.add_instruction(ip, Symbol::EMPTY, "", "▶"); + } + } + } + + fn trace_nav(&mut self, nav: Nav, node: Node<'_>) { + // Navigation sub-lines hidden in default verbosity + if self.verbosity == Verbosity::Default { + return; + } + + let c = &self.colors; + let kind = node.kind(); + let symbol = match nav { + Nav::Down | Nav::DownSkip | Nav::DownExact => trace::NAV_DOWN, + Nav::Next | Nav::NextSkip | Nav::NextExact => trace::NAV_NEXT, + Nav::Up(_) | Nav::UpSkipTrivia(_) | Nav::UpExact(_) => trace::NAV_UP, + Nav::Stay => Symbol::EMPTY, + }; + + // Text only in VeryVerbose (text is dim) + if self.verbosity == Verbosity::VeryVerbose { + let text = truncate_text(node.utf8_text(self.source).unwrap_or("?"), 20); + self.add_subline(symbol, &format!("{} {}\"{}\"{}", kind, c.dim, text, c.reset)); + } else { + self.add_subline(symbol, kind); + } + } + + fn trace_match_success(&mut self, node: Node<'_>) { + let c = &self.colors; + let kind = node.kind(); + + // Text on match/failure in Verbose+ (text is dim) + if self.verbosity != Verbosity::Default { + let text = truncate_text(node.utf8_text(self.source).unwrap_or("?"), 20); + self.add_subline(trace::MATCH_SUCCESS, &format!("{} {}\"{}\"{}", kind, c.dim, text, c.reset)); + } else { + self.add_subline(trace::MATCH_SUCCESS, kind); + } + } + + fn trace_match_failure(&mut self, node: Node<'_>) { + let c = &self.colors; + let kind = node.kind(); + + // Text on match/failure in Verbose+ (text is dim) + if self.verbosity != Verbosity::Default { + let text = truncate_text(node.utf8_text(self.source).unwrap_or("?"), 20); + self.add_subline(trace::MATCH_FAILURE, &format!("{} {}\"{}\"{}", kind, c.dim, text, c.reset)); + } else { + self.add_subline(trace::MATCH_FAILURE, kind); + } + } + + fn trace_field_success(&mut self, field_id: NonZeroU16) { + // Field success sub-lines hidden in default verbosity + if self.verbosity == Verbosity::Default { + return; + } + + let name = self.node_field_name(field_id.get()); + self.add_subline(trace::MATCH_SUCCESS, &format!("{}:", name)); + } + + fn trace_field_failure(&mut self, _node: Node<'_>) { + // Field failures are silent - we just backtrack + } + + fn trace_effect(&mut self, effect: &RuntimeEffect<'_>) { + // Effect sub-lines hidden in default verbosity + if self.verbosity == Verbosity::Default { + return; + } + + let effect_str = self.format_effect(effect); + self.add_subline(trace::EFFECT, &effect_str); + } + + fn trace_call(&mut self, target_ip: u16) { + let c = &self.colors; + let name = self.entrypoint_name(target_ip).to_string(); + // Definition name is blue + self.add_subline(trace::CALL, &format!("{}{}{}", c.blue, name, c.reset)); + self.definition_stack.push(name); + } + + fn trace_return(&mut self) { + let c = &self.colors; + let name = self.definition_stack.pop().unwrap_or_default(); + // Definition name is blue + self.add_subline(trace::RETURN, &format!("{}{}{}", c.blue, name, c.reset)); + } + + fn trace_checkpoint_created(&mut self, ip: u16) { + self.checkpoint_ips.push(ip); + } + + fn trace_backtrack(&mut self) { + let created_at = self.checkpoint_ips.pop().expect("backtrack without checkpoint"); + let line = format!( + " {:0sw$} {}", + created_at, + trace::BACKTRACK.format(), + sw = self.step_width + ); + self.lines.push(line); + } + + fn trace_enter_entrypoint(&mut self, target_ip: u16) { + let c = &self.colors; + let name = self.entrypoint_name(target_ip).to_string(); + // Definition name in blue + self.lines.push(format!("{}{}{}:", c.blue, name, c.reset)); + self.definition_stack.push(name); + } +} + +/// Format match symbol for instruction line. +fn format_match_symbol(m: &MatchView<'_>) -> Symbol { + if m.is_epsilon() { + Symbol::EPSILON + } else { + nav_symbol(m.nav) + } +} + + +/// Format match successors for instruction line. +fn format_match_successors(m: &MatchView<'_>) -> String { + if m.is_terminal() { + "◼".to_string() + } else if m.succ_count() == 1 { + format!("{:02}", m.successor(0).get()) + } else { + let succs: Vec<_> = m.successors().map(|s| format!("{:02}", s.get())).collect(); + succs.join(", ") + } +} diff --git a/crates/plotnik-lib/src/engine/value.rs b/crates/plotnik-lib/src/engine/value.rs new file mode 100644 index 00000000..be790f8e --- /dev/null +++ b/crates/plotnik-lib/src/engine/value.rs @@ -0,0 +1,422 @@ +//! Output value types for materialization. + +use arborium_tree_sitter::Node; +use serde::ser::{SerializeMap, SerializeSeq}; +use serde::{Serialize, Serializer}; + +use crate::Colors; + +/// Lifetime-free node handle for output. +/// +/// Captures enough information to represent a node without holding +/// a reference to the tree. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct NodeHandle { + /// Node kind name (e.g., "identifier", "number"). + pub kind: String, + /// Source text of the node. + pub text: String, + /// Byte span [start, end). + pub span: (u32, u32), +} + +impl NodeHandle { + /// Create from a tree-sitter node and source text. + pub fn from_node(node: Node<'_>, source: &str) -> Self { + let text = node + .utf8_text(source.as_bytes()) + .unwrap_or("") + .to_owned(); + Self { + kind: node.kind().to_owned(), + text, + span: (node.start_byte() as u32, node.end_byte() as u32), + } + } +} + +impl Serialize for NodeHandle { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + use serde::ser::SerializeStruct; + let mut s = serializer.serialize_struct("NodeHandle", 3)?; + s.serialize_field("kind", &self.kind)?; + s.serialize_field("text", &self.text)?; + s.serialize_field("span", &[self.span.0, self.span.1])?; + s.end() + } +} + +/// Self-contained output value. +/// +/// `Object` uses `Vec<(String, Value)>` to preserve field order from type metadata. +#[derive(Clone, Debug, PartialEq)] +pub enum Value { + Null, + String(String), + Node(NodeHandle), + Array(Vec), + /// Object with ordered fields. + Object(Vec<(String, Value)>), + /// Tagged union. + Tagged { tag: String, data: Box }, +} + +impl Serialize for Value { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + match self { + Value::Null => serializer.serialize_none(), + Value::String(s) => serializer.serialize_str(s), + Value::Node(h) => h.serialize(serializer), + Value::Array(arr) => { + let mut seq = serializer.serialize_seq(Some(arr.len()))?; + for item in arr { + seq.serialize_element(item)?; + } + seq.end() + } + Value::Object(fields) => { + let mut map = serializer.serialize_map(Some(fields.len()))?; + for (key, value) in fields { + map.serialize_entry(key, value)?; + } + map.end() + } + Value::Tagged { tag, data } => { + let mut map = serializer.serialize_map(Some(2))?; + map.serialize_entry("$tag", tag)?; + map.serialize_entry("$data", data)?; + map.end() + } + } + } +} + +impl Value { + /// Format value as colored JSON. + /// + /// Color scheme (jq-inspired): + /// - Keys: Blue + /// - String values: Green + /// - Numbers, booleans: Normal + /// - null: Dim + /// - Structure `{}[]:,`: Dim + pub fn format(&self, pretty: bool, colors: Colors) -> String { + let mut out = String::new(); + format_value(&mut out, self, &colors, pretty, 0); + out + } +} + +fn format_value(out: &mut String, value: &Value, c: &Colors, pretty: bool, indent: usize) { + match value { + Value::Null => { + out.push_str(c.dim); + out.push_str("null"); + out.push_str(c.reset); + } + Value::String(s) => { + out.push_str(c.green); + out.push('"'); + out.push_str(&escape_json_string(s)); + out.push('"'); + out.push_str(c.reset); + } + Value::Node(h) => { + format_node_handle(out, h, c, pretty, indent); + } + Value::Array(arr) => { + format_array(out, arr, c, pretty, indent); + } + Value::Object(fields) => { + format_object(out, fields, c, pretty, indent); + } + Value::Tagged { tag, data } => { + format_tagged(out, tag, data, c, pretty, indent); + } + } +} + +fn format_node_handle(out: &mut String, h: &NodeHandle, c: &Colors, pretty: bool, indent: usize) { + out.push_str(c.dim); + out.push('{'); + out.push_str(c.reset); + + let field_indent = if pretty { indent + 2 } else { 0 }; + + // Field 1: "kind" + if pretty { + out.push('\n'); + out.push_str(&" ".repeat(field_indent)); + } + out.push_str(c.blue); + out.push_str("\"kind\""); + out.push_str(c.reset); + out.push_str(c.dim); + out.push(':'); + out.push_str(c.reset); + if pretty { + out.push(' '); + } + out.push_str(c.green); + out.push('"'); + out.push_str(&escape_json_string(&h.kind)); + out.push('"'); + out.push_str(c.reset); + + // Field 2: "text" + out.push_str(c.dim); + out.push(','); + out.push_str(c.reset); + if pretty { + out.push('\n'); + out.push_str(&" ".repeat(field_indent)); + } + out.push_str(c.blue); + out.push_str("\"text\""); + out.push_str(c.reset); + out.push_str(c.dim); + out.push(':'); + out.push_str(c.reset); + if pretty { + out.push(' '); + } + out.push_str(c.green); + out.push('"'); + out.push_str(&escape_json_string(&h.text)); + out.push('"'); + out.push_str(c.reset); + + // Field 3: "span" + out.push_str(c.dim); + out.push(','); + out.push_str(c.reset); + if pretty { + out.push('\n'); + out.push_str(&" ".repeat(field_indent)); + } + out.push_str(c.blue); + out.push_str("\"span\""); + out.push_str(c.reset); + out.push_str(c.dim); + out.push(':'); + out.push_str(c.reset); + if pretty { + out.push(' '); + } + out.push_str(c.dim); + out.push('['); + out.push_str(c.reset); + out.push_str(&h.span.0.to_string()); + out.push_str(c.dim); + out.push_str(", "); + out.push_str(c.reset); + out.push_str(&h.span.1.to_string()); + out.push_str(c.dim); + out.push(']'); + out.push_str(c.reset); + + if pretty { + out.push('\n'); + out.push_str(&" ".repeat(indent)); + } + + out.push_str(c.dim); + out.push('}'); + out.push_str(c.reset); +} + +fn format_array(out: &mut String, arr: &[Value], c: &Colors, pretty: bool, indent: usize) { + out.push_str(c.dim); + out.push('['); + out.push_str(c.reset); + + if arr.is_empty() { + out.push_str(c.dim); + out.push(']'); + out.push_str(c.reset); + return; + } + + let elem_indent = if pretty { indent + 2 } else { 0 }; + + for (i, item) in arr.iter().enumerate() { + if i > 0 { + out.push_str(c.dim); + out.push(','); + out.push_str(c.reset); + } + + if pretty { + out.push('\n'); + out.push_str(&" ".repeat(elem_indent)); + } + + format_value(out, item, c, pretty, elem_indent); + } + + if pretty { + out.push('\n'); + out.push_str(&" ".repeat(indent)); + } + + out.push_str(c.dim); + out.push(']'); + out.push_str(c.reset); +} + +fn format_object( + out: &mut String, + fields: &[(String, Value)], + c: &Colors, + pretty: bool, + indent: usize, +) { + out.push_str(c.dim); + out.push('{'); + out.push_str(c.reset); + + if fields.is_empty() { + out.push_str(c.dim); + out.push('}'); + out.push_str(c.reset); + return; + } + + let field_indent = if pretty { indent + 2 } else { 0 }; + + for (i, (key, value)) in fields.iter().enumerate() { + if i > 0 { + out.push_str(c.dim); + out.push(','); + out.push_str(c.reset); + } + + if pretty { + out.push('\n'); + out.push_str(&" ".repeat(field_indent)); + } + + // Key in blue + out.push_str(c.blue); + out.push('"'); + out.push_str(&escape_json_string(key)); + out.push('"'); + out.push_str(c.reset); + + out.push_str(c.dim); + out.push(':'); + out.push_str(c.reset); + + if pretty { + out.push(' '); + } + + format_value(out, value, c, pretty, field_indent); + } + + if pretty { + out.push('\n'); + out.push_str(&" ".repeat(indent)); + } + + out.push_str(c.dim); + out.push('}'); + out.push_str(c.reset); +} + +fn format_tagged( + out: &mut String, + tag: &str, + data: &Value, + c: &Colors, + pretty: bool, + indent: usize, +) { + out.push_str(c.dim); + out.push('{'); + out.push_str(c.reset); + + let field_indent = if pretty { indent + 2 } else { 0 }; + + if pretty { + out.push('\n'); + out.push_str(&" ".repeat(field_indent)); + } + + // $tag key in blue + out.push_str(c.blue); + out.push_str("\"$tag\""); + out.push_str(c.reset); + + out.push_str(c.dim); + out.push(':'); + out.push_str(c.reset); + + if pretty { + out.push(' '); + } + + // Tag value is green (string) + out.push_str(c.green); + out.push('"'); + out.push_str(&escape_json_string(tag)); + out.push('"'); + out.push_str(c.reset); + + out.push_str(c.dim); + out.push(','); + out.push_str(c.reset); + + if pretty { + out.push('\n'); + out.push_str(&" ".repeat(field_indent)); + } + + // $data key in blue + out.push_str(c.blue); + out.push_str("\"$data\""); + out.push_str(c.reset); + + out.push_str(c.dim); + out.push(':'); + out.push_str(c.reset); + + if pretty { + out.push(' '); + } + + format_value(out, data, c, pretty, field_indent); + + if pretty { + out.push('\n'); + out.push_str(&" ".repeat(indent)); + } + + out.push_str(c.dim); + out.push('}'); + out.push_str(c.reset); +} + +fn escape_json_string(s: &str) -> String { + let mut result = String::with_capacity(s.len()); + for ch in s.chars() { + match ch { + '"' => result.push_str("\\\""), + '\\' => result.push_str("\\\\"), + '\n' => result.push_str("\\n"), + '\r' => result.push_str("\\r"), + '\t' => result.push_str("\\t"), + c if c.is_control() => { + result.push_str(&format!("\\u{:04x}", c as u32)); + } + c => result.push(c), + } + } + result +} diff --git a/crates/plotnik-lib/src/engine/vm.rs b/crates/plotnik-lib/src/engine/vm.rs new file mode 100644 index 00000000..1da8b3ef --- /dev/null +++ b/crates/plotnik-lib/src/engine/vm.rs @@ -0,0 +1,352 @@ +//! Virtual machine for executing compiled Plotnik queries. + +use arborium_tree_sitter::{Node, Tree}; + +use crate::bytecode::{ + Call, EffectOp, EffectOpcode, Entrypoint, InstructionView, MatchView, Module, Nav, +}; + +/// Get the nav for continue_search (always a sibling move). +fn continuation_nav(nav: Nav) -> Nav { + match nav { + Nav::Down | Nav::Next => Nav::Next, + Nav::DownSkip | Nav::NextSkip => Nav::NextSkip, + Nav::DownExact | Nav::NextExact => Nav::NextExact, + // Up/Stay don't have search loops + _ => Nav::Next, + } +} + +use super::checkpoint::{Checkpoint, CheckpointStack}; +use super::cursor::{CursorWrapper, SkipPolicy}; +use super::effect::{EffectLog, RuntimeEffect}; +use super::error::RuntimeError; +use super::frame::FrameArena; +use super::trace::{NoopTracer, Tracer}; + +/// Runtime limits for query execution. +#[derive(Clone, Copy, Debug)] +pub struct FuelLimits { + /// Maximum total steps (default: 1,000,000). + pub exec_fuel: u32, + /// Maximum call depth (default: 1,024). + pub recursion_limit: u32, +} + +impl Default for FuelLimits { + fn default() -> Self { + Self { + exec_fuel: 1_000_000, + recursion_limit: 1024, + } + } +} + +/// Virtual machine state for query execution. +pub struct VM<'t> { + cursor: CursorWrapper<'t>, + /// Current instruction pointer (raw u16, 0 is valid at runtime). + ip: u16, + frames: FrameArena, + checkpoints: CheckpointStack, + effects: EffectLog<'t>, + matched_node: Option>, + + // Fuel tracking + exec_fuel: u32, + recursion_depth: u32, + limits: FuelLimits, +} + +impl<'t> VM<'t> { + /// Create a new VM for execution. + pub fn new(tree: &'t Tree, trivia_types: Vec, limits: FuelLimits) -> Self { + Self { + cursor: CursorWrapper::new(tree.walk(), trivia_types), + ip: 0, + frames: FrameArena::new(), + checkpoints: CheckpointStack::new(), + effects: EffectLog::new(), + matched_node: None, + exec_fuel: limits.exec_fuel, + recursion_depth: 0, + limits, + } + } + + /// Execute query from entrypoint, returning effect log. + /// + /// This is a convenience method that uses `NoopTracer`, which gets + /// completely optimized away at compile time. + pub fn execute( + self, + module: &Module, + entrypoint: &Entrypoint, + ) -> Result, RuntimeError> { + self.execute_with(module, entrypoint, &mut NoopTracer) + } + + /// Execute query with a tracer for debugging. + /// + /// The tracer is generic, so `NoopTracer` calls are optimized away + /// while `PrintTracer` calls collect execution trace. + pub fn execute_with( + mut self, + module: &Module, + entrypoint: &Entrypoint, + tracer: &mut T, + ) -> Result, RuntimeError> { + self.ip = entrypoint.target.get(); + tracer.trace_enter_entrypoint(self.ip); + + loop { + // Fuel check + if self.exec_fuel == 0 { + return Err(RuntimeError::ExecFuelExhausted(self.limits.exec_fuel)); + } + self.exec_fuel -= 1; + + // Fetch and dispatch + let instr = module.decode_step(self.ip); + tracer.trace_instruction(self.ip, &instr); + + let result = match instr { + InstructionView::Match(m) => self.exec_match(m, tracer), + InstructionView::Call(c) => self.exec_call(c, tracer), + InstructionView::Return(_) => self.exec_return(tracer), + }; + + match result { + Ok(()) | Err(RuntimeError::Backtracked) => continue, + Err(RuntimeError::Accept) => return Ok(self.effects), + Err(e) => return Err(e), + } + } + } + + fn exec_match( + &mut self, + m: MatchView<'_>, + tracer: &mut T, + ) -> Result<(), RuntimeError> { + for effect_op in m.pre_effects() { + self.emit_effect(effect_op, tracer); + } + + self.matched_node = None; + + if !m.is_epsilon() { + self.navigate_and_match(m, tracer)?; + } + + for effect_op in m.post_effects() { + self.emit_effect(effect_op, tracer); + } + + self.branch_to_successors(m, tracer) + } + + fn navigate_and_match( + &mut self, + m: MatchView<'_>, + tracer: &mut T, + ) -> Result<(), RuntimeError> { + let Some(policy) = self.cursor.navigate(m.nav) else { + return self.backtrack(tracer); + }; + tracer.trace_nav(m.nav, self.cursor.node()); + + let cont_nav = continuation_nav(m.nav); + loop { + if !self.node_matches(m, tracer) { + self.advance_or_backtrack(policy, cont_nav, tracer)?; + continue; + } + break; + } + + tracer.trace_match_success(self.cursor.node()); + if let Some(field_id) = m.node_field { + tracer.trace_field_success(field_id); + } + + self.matched_node = Some(self.cursor.node()); + + for field_id in m.neg_fields() { + if self.cursor.node().child_by_field_id(field_id).is_some() { + return self.backtrack(tracer); + } + } + + Ok(()) + } + + /// Check if current node matches type and field constraints. + fn node_matches(&self, m: MatchView<'_>, tracer: &mut T) -> bool { + if let Some(expected) = m.node_type + && self.cursor.node().kind_id() != expected.get() + { + tracer.trace_match_failure(self.cursor.node()); + return false; + } + if let Some(expected) = m.node_field + && self.cursor.field_id() != Some(expected) + { + tracer.trace_field_failure(self.cursor.node()); + return false; + } + true + } + + fn branch_to_successors( + &mut self, + m: MatchView<'_>, + tracer: &mut T, + ) -> Result<(), RuntimeError> { + if m.succ_count() == 0 { + return Err(RuntimeError::Accept); + } + + // Push checkpoints for alternate branches (in reverse order) + for i in (1..m.succ_count()).rev() { + self.checkpoints.push(Checkpoint { + descendant_index: self.cursor.descendant_index(), + effect_watermark: self.effects.len(), + frame_index: self.frames.current(), + ip: m.successor(i).get(), + }); + tracer.trace_checkpoint_created(self.ip); + } + + self.ip = m.successor(0).get(); + Ok(()) + } + + fn exec_call(&mut self, c: Call, tracer: &mut T) -> Result<(), RuntimeError> { + if self.recursion_depth >= self.limits.recursion_limit { + return Err(RuntimeError::RecursionLimitExceeded(self.recursion_depth)); + } + + self.navigate_to_field(c.nav, c.node_field, tracer)?; + + tracer.trace_call(c.target.get()); + self.frames.push(c.next.get()); + self.recursion_depth += 1; + self.ip = c.target.get(); + Ok(()) + } + + fn navigate_to_field( + &mut self, + nav: Nav, + field: Option, + tracer: &mut T, + ) -> Result<(), RuntimeError> { + if nav == Nav::Stay { + return self.check_field(field, tracer); + } + + let Some(policy) = self.cursor.navigate(nav) else { + return self.backtrack(tracer); + }; + tracer.trace_nav(nav, self.cursor.node()); + + let Some(field_id) = field else { + return Ok(()); + }; + + let cont_nav = continuation_nav(nav); + loop { + if self.cursor.field_id() == Some(field_id) { + tracer.trace_field_success(field_id); + return Ok(()); + } + tracer.trace_field_failure(self.cursor.node()); + self.advance_or_backtrack(policy, cont_nav, tracer)?; + } + } + + fn check_field( + &mut self, + field: Option, + tracer: &mut T, + ) -> Result<(), RuntimeError> { + let Some(field_id) = field else { + return Ok(()); + }; + if self.cursor.field_id() != Some(field_id) { + tracer.trace_field_failure(self.cursor.node()); + return self.backtrack(tracer); + } + tracer.trace_field_success(field_id); + Ok(()) + } + + fn exec_return(&mut self, tracer: &mut T) -> Result<(), RuntimeError> { + tracer.trace_return(); + + // If no frames, we're returning from top-level entrypoint → Accept + if self.frames.is_empty() { + return Err(RuntimeError::Accept); + } + + let return_addr = self.frames.pop(); + self.recursion_depth -= 1; + + // Prune frames (O(1) amortized) + self.frames.prune(self.checkpoints.max_frame_ref()); + + self.ip = return_addr; + Ok(()) + } + + fn backtrack(&mut self, tracer: &mut T) -> Result<(), RuntimeError> { + let cp = self.checkpoints.pop().ok_or(RuntimeError::NoMatch)?; + tracer.trace_backtrack(); + self.cursor.goto_descendant(cp.descendant_index); + self.effects.truncate(cp.effect_watermark); + self.frames.restore(cp.frame_index); + self.ip = cp.ip; + Err(RuntimeError::Backtracked) + } + + /// Advance to next sibling or backtrack if search exhausted. + fn advance_or_backtrack( + &mut self, + policy: SkipPolicy, + cont_nav: Nav, + tracer: &mut T, + ) -> Result<(), RuntimeError> { + if !self.cursor.continue_search(policy) { + return self.backtrack(tracer); + } + tracer.trace_nav(cont_nav, self.cursor.node()); + Ok(()) + } + + fn emit_effect(&mut self, op: EffectOp, tracer: &mut T) { + use EffectOpcode::*; + let effect = match op.opcode { + Node => RuntimeEffect::Node( + self.matched_node + .expect("Node effect without matched_node"), + ), + Text => RuntimeEffect::Text( + self.matched_node + .expect("Text effect without matched_node"), + ), + Arr => RuntimeEffect::Arr, + Push => RuntimeEffect::Push, + EndArr => RuntimeEffect::EndArr, + Obj => RuntimeEffect::Obj, + EndObj => RuntimeEffect::EndObj, + Set => RuntimeEffect::Set(op.payload as u16), + Enum => RuntimeEffect::Enum(op.payload as u16), + EndEnum => RuntimeEffect::EndEnum, + Clear => RuntimeEffect::Clear, + Null => RuntimeEffect::Null, + }; + tracer.trace_effect(&effect); + self.effects.push(effect); + } +} diff --git a/crates/plotnik-lib/src/lib.rs b/crates/plotnik-lib/src/lib.rs index cdf99a45..daf8a89f 100644 --- a/crates/plotnik-lib/src/lib.rs +++ b/crates/plotnik-lib/src/lib.rs @@ -22,6 +22,7 @@ pub mod colors; pub mod compile; pub mod diagnostics; pub mod emit; +pub mod engine; pub mod parser; pub mod query; pub mod type_system;