From 00cf273d476f008741cf942742b6cf9a6beb9304 Mon Sep 17 00:00:00 2001 From: Sergei Zharinov Date: Mon, 5 Jan 2026 15:34:03 -0300 Subject: [PATCH] fix: named wildcard `(_)` now correctly matches only named nodes --- crates/plotnik-lib/src/bytecode/constants.rs | 7 ++++++ crates/plotnik-lib/src/bytecode/dump.rs | 22 ++++++++++++------- crates/plotnik-lib/src/bytecode/mod.rs | 2 +- crates/plotnik-lib/src/compile/expressions.rs | 6 +++-- ..._emit__codegen_tests__fields_multiple.snap | 4 ++-- ...__codegen_tests__nodes_wildcard_named.snap | 2 +- crates/plotnik-lib/src/engine/engine_tests.rs | 14 ++++++++++++ ...ests__wildcard_bare_matches_anonymous.snap | 17 ++++++++++++++ ...tests__wildcard_named_skips_anonymous.snap | 17 ++++++++++++++ crates/plotnik-lib/src/engine/vm.rs | 17 +++++++++----- 10 files changed, 89 insertions(+), 19 deletions(-) create mode 100644 crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__wildcard_bare_matches_anonymous.snap create mode 100644 crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__wildcard_named_skips_anonymous.snap diff --git a/crates/plotnik-lib/src/bytecode/constants.rs b/crates/plotnik-lib/src/bytecode/constants.rs index 4bb1c566..acf2f1ce 100644 --- a/crates/plotnik-lib/src/bytecode/constants.rs +++ b/crates/plotnik-lib/src/bytecode/constants.rs @@ -11,3 +11,10 @@ pub const SECTION_ALIGN: usize = 64; /// Step size in bytes (all instructions are 8-byte aligned). pub const STEP_SIZE: usize = 8; + +/// Sentinel value for "any named node" wildcard `(_)`. +/// +/// When `node_type` equals this value, the VM checks `node.is_named()` +/// instead of comparing type IDs. This distinguishes `(_)` (any named) +/// from `_` (any node including anonymous). +pub const NAMED_WILDCARD: u16 = 0xFFFF; diff --git a/crates/plotnik-lib/src/bytecode/dump.rs b/crates/plotnik-lib/src/bytecode/dump.rs index 14ead7c1..bccf1c37 100644 --- a/crates/plotnik-lib/src/bytecode/dump.rs +++ b/crates/plotnik-lib/src/bytecode/dump.rs @@ -7,6 +7,7 @@ use std::fmt::Write as _; use crate::colors::Colors; +use super::NAMED_WILDCARD; use super::format::{LineBuilder, Symbol, format_effect, nav_symbol_epsilon, width_for_count}; use super::ids::QTypeId; use super::instructions::StepId; @@ -471,7 +472,7 @@ fn format_match_content(m: &Match, ctx: &DumpContext) -> String { parts.join(" ") } -/// Format node pattern: `field: (type)` or `(type)` or `field: _` +/// Format node pattern: `field: (type)` or `(type)` or `field: _` or `(_)` fn format_node_pattern(m: &Match, ctx: &DumpContext) -> String { let mut result = String::new(); @@ -485,13 +486,18 @@ fn format_node_pattern(m: &Match, ctx: &DumpContext) -> String { } if let Some(type_id) = m.node_type { - let name = ctx - .node_type_name(type_id.get()) - .map(String::from) - .unwrap_or_else(|| format!("node#{}", type_id.get())); - result.push('('); - result.push_str(&name); - result.push(')'); + if type_id.get() == NAMED_WILDCARD { + // Named wildcard: any named node + result.push_str("(_)"); + } else { + let name = ctx + .node_type_name(type_id.get()) + .map(String::from) + .unwrap_or_else(|| format!("node#{}", type_id.get())); + result.push('('); + result.push_str(&name); + result.push(')'); + } } else if m.node_field.is_some() { result.push('_'); } diff --git a/crates/plotnik-lib/src/bytecode/mod.rs b/crates/plotnik-lib/src/bytecode/mod.rs index 0e11fad3..7579d62d 100644 --- a/crates/plotnik-lib/src/bytecode/mod.rs +++ b/crates/plotnik-lib/src/bytecode/mod.rs @@ -16,7 +16,7 @@ mod nav; mod sections; mod type_meta; -pub use constants::{MAGIC, SECTION_ALIGN, STEP_SIZE, VERSION}; +pub use constants::{MAGIC, NAMED_WILDCARD, SECTION_ALIGN, STEP_SIZE, VERSION}; pub use ids::{QTypeId, StringId}; diff --git a/crates/plotnik-lib/src/compile/expressions.rs b/crates/plotnik-lib/src/compile/expressions.rs index b96c0cc6..2cc1a690 100644 --- a/crates/plotnik-lib/src/compile/expressions.rs +++ b/crates/plotnik-lib/src/compile/expressions.rs @@ -10,6 +10,7 @@ use std::num::NonZeroU16; use crate::analyze::type_check::TypeShape; +use crate::bytecode::NAMED_WILDCARD; use crate::bytecode::ir::{EffectIR, Instruction, Label, MatchIR}; use crate::bytecode::{EffectOpcode, Nav}; use crate::parser::ast::{self, Expr}; @@ -535,10 +536,11 @@ impl Compiler<'_> { /// /// In linked mode, returns the grammar NodeTypeId. /// In unlinked mode, returns the StringId of the type name. + /// For the wildcard `(_)`, returns `NAMED_WILDCARD` sentinel. pub(super) fn resolve_node_type(&mut self, node: &ast::NamedNode) -> Option { - // For wildcard (_), no constraint + // For wildcard (_), return sentinel for "any named node" if node.is_any() { - return None; + return NonZeroU16::new(NAMED_WILDCARD); } let type_token = node.node_type()?; 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 d6a325a8..37bd54da 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 @@ -43,7 +43,7 @@ _ObjWrap: Test: 06 ε 07 07 ! (binary_expression) 08 - 08 ▽ left: _ [Node Set(M0)] 10 - 10 ▷ right: _ [Node Set(M1)] 12 + 08 ▽ left: (_) [Node Set(M0)] 10 + 10 ▷ right: (_) [Node Set(M1)] 12 12 △ 13 13 ▶ 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 3cf67bfd..83b3ded0 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 @@ -35,6 +35,6 @@ _ObjWrap: Test: 06 ε 07 07 ! (pair) 08 - 08 ▽ key: _ [Node Set(M0)] 10 + 08 ▽ key: (_) [Node Set(M0)] 10 10 △ 11 11 ▶ diff --git a/crates/plotnik-lib/src/engine/engine_tests.rs b/crates/plotnik-lib/src/engine/engine_tests.rs index 844258f3..432c3c20 100644 --- a/crates/plotnik-lib/src/engine/engine_tests.rs +++ b/crates/plotnik-lib/src/engine/engine_tests.rs @@ -533,3 +533,17 @@ fn suppressive_capture_with_wrap() { entry: "Q" ); } + +/// Named wildcard `(_)` matches only named nodes, skipping anonymous nodes. +/// In `return 42`, `(_)` should match `number`, not the `return` keyword. +#[test] +fn wildcard_named_skips_anonymous() { + snap!("Q = (program (return_statement (_) @x))", "return 42"); +} + +/// Bare wildcard `_` matches any node including anonymous ones. +/// In `return 42`, `_` should match the first child which is `return` keyword. +#[test] +fn wildcard_bare_matches_anonymous() { + snap!("Q = (program (return_statement _ @x))", "return 42"); +} diff --git a/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__wildcard_bare_matches_anonymous.snap b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__wildcard_bare_matches_anonymous.snap new file mode 100644 index 00000000..95660719 --- /dev/null +++ b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__wildcard_bare_matches_anonymous.snap @@ -0,0 +1,17 @@ +--- +source: crates/plotnik-lib/src/engine/engine_tests.rs +--- +Q = (program (return_statement _ @x)) +--- +return 42 +--- +{ + "x": { + "kind": "return", + "text": "return", + "span": [ + 0, + 6 + ] + } +} diff --git a/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__wildcard_named_skips_anonymous.snap b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__wildcard_named_skips_anonymous.snap new file mode 100644 index 00000000..54d9c2a5 --- /dev/null +++ b/crates/plotnik-lib/src/engine/snapshots/plotnik_lib__engine__engine_tests__wildcard_named_skips_anonymous.snap @@ -0,0 +1,17 @@ +--- +source: crates/plotnik-lib/src/engine/engine_tests.rs +--- +Q = (program (return_statement (_) @x)) +--- +return 42 +--- +{ + "x": { + "kind": "number", + "text": "42", + "span": [ + 7, + 9 + ] + } +} diff --git a/crates/plotnik-lib/src/engine/vm.rs b/crates/plotnik-lib/src/engine/vm.rs index 2cb7af05..81d0eb20 100644 --- a/crates/plotnik-lib/src/engine/vm.rs +++ b/crates/plotnik-lib/src/engine/vm.rs @@ -2,6 +2,7 @@ use arborium_tree_sitter::{Node, Tree}; +use crate::bytecode::NAMED_WILDCARD; use crate::bytecode::{ Call, EffectOp, EffectOpcode, Entrypoint, InstructionView, MatchView, Module, Nav, StepAddr, Trampoline, @@ -222,11 +223,17 @@ impl<'t> VM<'t> { /// 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_type { + if expected.get() == NAMED_WILDCARD { + // Special case: `(_)` wildcard matches any named node + if !self.cursor.node().is_named() { + tracer.trace_match_failure(self.cursor.node()); + return false; + } + } else if 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)