Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions crates/plotnik-lib/src/bytecode/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
22 changes: 14 additions & 8 deletions crates/plotnik-lib/src/bytecode/dump.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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();

Expand All @@ -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('_');
}
Expand Down
2 changes: 1 addition & 1 deletion crates/plotnik-lib/src/bytecode/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down
6 changes: 4 additions & 2 deletions crates/plotnik-lib/src/compile/expressions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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<NonZeroU16> {
// 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()?;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 ▶
Original file line number Diff line number Diff line change
Expand Up @@ -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 ▶
14 changes: 14 additions & 0 deletions crates/plotnik-lib/src/engine/engine_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
Original file line number Diff line number Diff line change
@@ -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
]
}
}
Original file line number Diff line number Diff line change
@@ -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
]
}
}
17 changes: 12 additions & 5 deletions crates/plotnik-lib/src/engine/vm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -222,11 +223,17 @@ impl<'t> VM<'t> {

/// Check if current node matches type and field constraints.
fn node_matches<T: Tracer>(&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)
Expand Down