Skip to content

Accept ?? as a prefix operator#163

Merged
danieljohnmorris merged 2 commits into
mainfrom
fix/prefix-nil-coalesce
May 11, 2026
Merged

Accept ?? as a prefix operator#163
danieljohnmorris merged 2 commits into
mainfrom
fix/prefix-nil-coalesce

Conversation

@danieljohnmorris
Copy link
Copy Markdown
Collaborator

Summary

Resolves a 4+ persona doc citation: ?? was the only binary operator in ilo that didn't work in prefix form. Every other binary op accepts both forms (+a b vs a + b, *x y vs x * y), but ?? required infix only. Writing ??c 0 produced the cryptic expected expression, got NilCoalesce error. Agents naturally reached for the prefix form when composing with calls (??mget m k 0) and had to fall back to a two-step bind every time.

What's in the diff

  1. parser: accept ?? as a prefix operator?? is its own AST variant Expr::NilCoalesce { value, default } rather than a BinOp arm, so the existing parse_prefix_binop scaffolding doesn't apply. Added dedicated arms in parse_expr_inner and parse_operand that build the same Expr::NilCoalesce shape as the infix path. New is_infix_or_suffix_op helper wraps both the Pratt-table check and a NilCoalesce check, used at the two call-arg termination sites without changing Pratt precedence semantics.

    The prefix default operand parses via parse_expr_inner (matching the infix path) for full semantic parity — ?? c +a b correctly treats +a b as the default expression. Caught in review: an earlier draft used the tighter parse_operand, which would have left +b dangling.

  2. tests + example: pin ?? prefix behaviour across engines — 10 cases × 3 engines covering prefix nil-path, prefix value-path, infix regression, ?? as a call arg, and the new prefix-default-is-an-expression cases (?? c +a b and nested ?? c ?? d 0). Also fixed a latent pid-only temp-file race in run_file (atomic counter appended). examples/prefix-nil-coalesce.ilo demonstrates the new shape via a dflt o:O n d:n>n;??o d helper.

Test plan

  • cargo test --release --features cranelift — full suite green
  • cargo fmt --all -- --check clean
  • cargo clippy --all-targets --features cranelift -- -D warnings clean
  • ?? c 0 with c=nil → 0; with c=5 → 5; across all three engines
  • c??0 infix still works (no regression)
  • ?? c +a b parses with +a b as a full expression in default position (semantic parity with infix)
  • Nested ?? c ?? d 0 parses right-associatively
  • Code-reviewed by subagent; the AST asymmetry (parse_operand vs parse_expr_inner in default position) was caught and fixed before commit

The doc cites this asymmetry across 4+ personas: ?? is the only
binary operator in ilo that doesn't work in prefix form. Every
other binary op accepts both forms (+a b vs a + b, *x y vs x * y),
but ?? required infix only - writing ??c 0 produced the cryptic
"expected expression, got NilCoalesce" error. Agents naturally
reached for the prefix form when composing with calls
(??mget m k 0) and had to fall back to a two-step bind every time.

?? is its own AST variant Expr::NilCoalesce { value, default },
not a BinOp arm, so the existing parse_prefix_binop scaffolding
doesn't apply. Added dedicated arms in parse_expr_inner and
parse_operand that build the same Expr::NilCoalesce shape as the
infix path. The new is_infix_or_suffix_op helper wraps both the
Pratt-table check and a NilCoalesce check - used in the call-arg
termination heuristic without changing Pratt precedence semantics.

The prefix `default` operand parses via parse_expr_inner (same as
the infix path) for full semantic parity: ?? c +a b correctly
treats +a b as the default expression. Earlier draft used the
tighter parse_operand which would have left +b dangling. value
stays as parse_operand since prefix-binary LHSs are operand-shape.

Added NilCoalesce to can_start_operand and to the recursive
prefix-binary scan in scan_prefix_binary_end so the call-arg
loop handles ??c b correctly without bailing.
10 cross-engine cases × 3 engines covering: prefix nil-path,
prefix value-path, infix regression (both nil and value), ?? as a
call arg (both paths), and the new prefix-default-is-an-expression
cases ?? c +a b and the nested ?? c ?? d 0 which exercise full
expression depth in the default position.

Also fixed a pid-only temp-file race in run_file that surfaced
with the new tests writing in parallel - appended an atomic
counter to avoid collisions between threads writing the same
temp path with different content.

examples/prefix-nil-coalesce.ilo demonstrates the new shape via
a `dflt o:O n d:n>n;??o d` helper with two run/out cases (nil
fallback and value passthrough). One-line header.
@codecov
Copy link
Copy Markdown

codecov Bot commented May 11, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ All tests successful. No failed tests found.

📢 Thoughts on this report? Let us know!

danieljohnmorris added a commit that referenced this pull request May 11, 2026
check_infix_on_call and check_single_atom_after_op both wrote to
fixed-name temp files. Under cargo llvm-cov's parallel test runner,
two threads writing different content to the same path raced;
whichever finished writing last "won" but the other test's ilo
process might have already opened the file with mid-write content,
producing ILO-R002 undefined function failures intermittently.

Same fix shape as PR #163's run_file: AtomicU64 counter per
function, appended to filename along with pid for uniqueness.
Relaxed ordering matches the eval_inline.rs precedent (the counter
only needs atomic increment; no cross-variable synchronization
required).
@danieljohnmorris danieljohnmorris merged commit 7766952 into main May 11, 2026
5 checks passed
@danieljohnmorris danieljohnmorris deleted the fix/prefix-nil-coalesce branch May 11, 2026 18:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant