Skip to content

Captured optional {...}? @cap at a navigating child drops its struct scope (VM panics / corrupts output) #468

Description

@zharinov

Problem

A captured optional wrapper {...}? @cap is mis-compiled when both of these are true:

  • the inner produces a struct (a bubbling sequence or untagged alternation), and
  • it sits at a navigating child position.

In that case the compiler drops the Obj/EndObj struct scope and the inner capture's Set, and sets @cap to the raw matched node. The emitted value does not match the declared { ... } | null.

# Want: outer is a struct { id: ... } | null
# Got (debug):   panic at crates/plotnik-vm/src/engine/verify.rs:348
#                "BUG: Type and value do not match — outer: type: object, value: Node"
# Got (release): {"outer":{"kind":"identifier",...}}   (bare Node; @id silently dropped)
cargo run -p plotnik -- run -l javascript -s 'a;' -q 'Q = (program (expression_statement {(identifier) @id}? @outer))'

check and infer both accept the query (infer: outer: { id: Node | null } | null). So the compiler admits a valid query, then mis-lowers it. That breaks the trust boundary: the VM panics in debug or corrupts the output in release on input the compiler said was fine.

Cause

compile/quantifier.rs:215-241 compile_skippable_with_exits only special-cases the array mechanism. It checks is_star_or_plus_quantifier (line 219); for everything else it flattens build_capture_effects(cap, inner) onto the body and recurses.

For a StructScope mechanism, compile/capture.rs:116-151 build_capture_effects returns only [Set(cap)] — no Obj/EndObj. So:

  • the struct scope is never opened, and
  • the inner Set is lost.

The correct path compile_captured_inner dispatches on capture_mechanism and calls compile_struct_scope. This code path skips that classifier.

Other cases compile correctly:

  • the same pattern at the query root, and
  • */+/plain-{...} at the same child position.

Ref and SetAfter (tagged-alt) mechanisms also work here, because their wrapper is part of the inner. Only StructScope (bubbling sequence / untagged alternation) is broken.

Fix

  • Make compile_skippable_with_exits consult capture_mechanism (the single source of truth) instead of the ad-hoc is_star_or_plus_quantifier check.
  • Route a StructScope capture through a split-exit variant of compile_struct_scope:
    • match path emits Obj … inner Sets … EndObj Set(cap)
    • skip path emits Null Set(cap)
  • Only the Node mechanism may flatten onto the body.

Acceptance

  • The repro no longer panics; outer materializes as a struct matching its declared type (or null), with @id preserved.
  • conformance optional_struct_wrapper_at_child_drops_scope runs un-ignored and passes debug_verify_type.

Related

Same function as the non-greedy implicit-array bug (compile_skippable_with_exits mishandling capture mechanisms / quantifier kinds). Recommend one PR. This is the inference/emission divergence that capture_shape.rs's "single source of truth" contract (#420) is supposed to prevent — the emit path does not honor it here.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions