Skip to content

Captured optional scope {...}? @cap double-nullifies inner fields #485

Description

@zharinov

Problem

When you capture an optional scope {...}? @cap, the inner fields become nullable on top of the scope itself becoming nullable. The nullability gets applied twice.

cargo run -p plotnik -- infer -q 'Q = {(identifier) @foo}? @bar' -l typescript
interface QBar { foo: Node | null; }   // foo should not be nullable
interface Q    { bar: QBar | null; }

Without the ?, the scope is correct:

cargo run -p plotnik -- infer -q 'Q = {(identifier) @foo} @bar' -l typescript
interface QBar { foo: Node; }
interface Q    { bar: QBar; }

QBar should be the same type in both cases. Only the outer bar field should gain | null.

Why it's wrong

{(identifier) @foo}? @bar has two runtime outcomes:

  • the group matched → bar is present, and @foo is unconditional inside it, so foo is present
  • the group didn't match → bar is null

There is no state where bar is non-null but foo is null. The foo: Node | null type describes an unreachable value.

Expected:

interface QBar { foo: Node; }
interface Q    { bar: QBar | null; }

Cause

Two places each add optionality for the same ?:

  • make_flow_optional (crates/plotnik-compiler/src/analyze/type_check/infer.rs:592) distributes make_optional() over every field of the scope struct, turning foo into Node | null.
  • infer_captured_expr also marks the capture field itself optional via is_optional (infer.rs:383), turning bar into QBar | null.

When a ?-quantified scope is directly captured, the capture already absorbs the optionality at the scope boundary, so the field-distributing step is redundant.

Related

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions