Summary
- A captured ref can fabricate a node when the callee returns without matching anything.
- Repro via CLI:
A = (identifier)??; Q = (A) @x on source foo returns the root program node as x.
- Suspected root cause:
exec_return() unconditionally writes matched_node = Some(self.cursor.node()) even when the callee matched nothing.
Repro
Run:
cargo run -p plotnik -- exec -q 'A = (identifier)??
Q = (A) @x' -s 'foo' -l javascript --entry Q
Observed output:
{
"x": {
"kind": "program",
"text": "foo",
"span": [0, 3]
}
}
Trace also shows the callee taking the epsilon skip path and then the caller executing an epsilon [Node Set(M0)] capture:
cargo run -p plotnik -- trace -q 'A = (identifier)??
Q = (A) @x' -s 'foo' -l javascript --entry Q -vv
And the lowered bytecode contains:
Q:
08 ! (A) 06 : 09
09 ε [Node Set(M0)] 11
Why this looks wrong
A = (identifier)?? is a non-greedy optional, so A is allowed to return without matching an identifier.
In that case, (A) @x should not fabricate a program node that A never matched.
Regardless of whether the intended behavior is null / absent / no match, returning the root node is incorrect.
Suspected root cause
In crates/plotnik-vm/src/engine/vm.rs, exec_return() does:
- set
matched_node = Some(self.cursor.node())
- then restore tree depth
That seems correct when the callee actually matched something, but incorrect when the callee returned through a zero-match path (like a skipped optional).
A fix likely needs to distinguish:
- return after a real match
- return after a zero-width / skipped path
and avoid manufacturing matched_node in the latter case.
Summary
A = (identifier)??; Q = (A) @xon sourcefooreturns the rootprogramnode asx.exec_return()unconditionally writesmatched_node = Some(self.cursor.node())even when the callee matched nothing.Repro
Run:
Observed output:
{ "x": { "kind": "program", "text": "foo", "span": [0, 3] } }Trace also shows the callee taking the epsilon skip path and then the caller executing an epsilon
[Node Set(M0)]capture:And the lowered bytecode contains:
Why this looks wrong
A = (identifier)??is a non-greedy optional, soAis allowed to return without matching anidentifier.In that case,
(A) @xshould not fabricate aprogramnode thatAnever matched.Regardless of whether the intended behavior is null / absent / no match, returning the root node is incorrect.
Suspected root cause
In
crates/plotnik-vm/src/engine/vm.rs,exec_return()does:matched_node = Some(self.cursor.node())That seems correct when the callee actually matched something, but incorrect when the callee returned through a zero-match path (like a skipped optional).
A fix likely needs to distinguish:
and avoid manufacturing
matched_nodein the latter case.