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
22 changes: 22 additions & 0 deletions examples/qq-call-default.ilo
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
-- Prefix `??` accepts a CALL expression as its value side.
-- Common case: nil-coalesce on a map lookup with a default.
-- `??mget m "k" 0` parses as `??(mget m "k") 0`, the arity-aware
-- parser stops after `mget`'s 2 args and leaves `0` for the default.
-- Previously you had to write `??(mget m "k") 0` or bind first; now
-- the bare form Just Works, which is the most common nil-coalesce
-- shape in agent-written ilo (mget lookups with a fallback).

hit>n;m=mset mmap "k" 42;??mget m "k" 0
miss>n;m=mset mmap "k" 42;??mget m "missing" 99

-- Chained: first non-nil wins, mirroring infix chaining.
chain>n;m=mset mmap "a" 1;??mget m "x" ??mget m "a" 0

-- run: hit
-- out: 42

-- run: miss
-- out: 99

-- run: chain
-- out: 1
14 changes: 11 additions & 3 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1898,10 +1898,15 @@ impl Parser {
| Some(Token::Amp)
| Some(Token::Pipe)
| Some(Token::PlusEq) => self.parse_prefix_binop(),
// Prefix nil-coalesce: ??a b — mirror of infix `a ?? b`
// Prefix nil-coalesce: ??a b, mirror of infix `a ?? b`.
// The value side uses `parse_call_arg` (not `parse_operand`) so
// a known-arity function with args expands into a call expression,
// consuming exactly its declared arity. This lets `??mget m "k" 0`
// parse as `??(mget m "k") 0` without forcing parens or a bind-first
// line. The most common nil-coalesce site is `??mget ... default`.
Some(Token::NilCoalesce) => {
self.advance();
let value = self.parse_operand()?;
let value = self.parse_call_arg(false, None)?;
let default = self.parse_expr_inner()?;
Ok(Expr::NilCoalesce {
value: Box::new(value),
Expand Down Expand Up @@ -2571,8 +2576,11 @@ or write `({fmt_name} \"...\" ...)` so its args are grouped."
| Some(Token::Pipe)
| Some(Token::PlusEq) => self.parse_prefix_binop(),
Some(Token::NilCoalesce) => {
// See `parse_expr_inner` for the rationale: `parse_call_arg`
// expands a known-arity function into a call so `??mget m "k" 0`
// parses as `??(mget m "k") 0`.
self.advance();
let value = self.parse_operand()?;
let value = self.parse_call_arg(false, None)?;
let default = self.parse_expr_inner()?;
Ok(Expr::NilCoalesce {
value: Box::new(value),
Expand Down
36 changes: 36 additions & 0 deletions tests/regression_prefix_nil_coalesce.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,22 @@ const PREFIX_INFIX_DEFAULT_VAL: &str = "f>n;a=3;b=4;c=5;?? c a+b";
// Nested prefix nil-coalesce — confirm right-associativity matches infix.
const PREFIX_NESTED_NIL: &str = "f>n;c=nil;d=nil;??c ??d 0";
const PREFIX_NESTED_INNER: &str = "f>n;c=nil;d=9;??c ??d 0";
// Prefix `??` where the value side is a CALL expression. Previously the
// value side used `parse_operand` (atom-only), so `??mget m "k" 0` mis-parsed:
// `mget` was taken as the value atom and `m "k" 0` as the default expression
// (which then failed because `m` is a Map, not a function). With the arity-cap
// fix, the value side is parsed via `parse_call_arg`, so a known-arity
// function consumes exactly its declared arity and the remaining tokens
// become the default. Workarounds (`??(mget m "k") 0`, bind-first) keep
// working; the new shape is purely additive.
const PREFIX_CALL_HIT: &str = "f>n;m=mset mmap \"k\" 42;??mget m \"k\" 0";
const PREFIX_CALL_MISS: &str = "f>n;m=mset mmap \"k\" 42;??mget m \"missing\" 99";
// Chained prefix `??` with two call value-sides:
// `??mget m "x" ??mget m "a" 0` reads as `??(mget m "x") (??(mget m "a") 0)`,
// first miss, second hit, expect `1`.
const PREFIX_CALL_CHAIN: &str = "f>n;m=mset mmap \"a\" 1;??mget m \"x\" ??mget m \"a\" 0";
// Paren workaround still parses correctly post-fix.
const PREFIX_CALL_PAREN: &str = "f>n;m=mset mmap \"k\" 42;??(mget m \"k\") 0";

fn check_all(engine: &str) {
assert_eq!(
Expand Down Expand Up @@ -110,6 +126,26 @@ fn check_all(engine: &str) {
"9",
"prefix nested inner engine={engine}"
);
assert_eq!(
run(engine, PREFIX_CALL_HIT, "f"),
"42",
"prefix call hit engine={engine}"
);
assert_eq!(
run(engine, PREFIX_CALL_MISS, "f"),
"99",
"prefix call miss engine={engine}"
);
assert_eq!(
run(engine, PREFIX_CALL_CHAIN, "f"),
"1",
"prefix call chain engine={engine}"
);
assert_eq!(
run(engine, PREFIX_CALL_PAREN, "f"),
"42",
"prefix call paren workaround engine={engine}"
);
}

#[test]
Expand Down
Loading