diff --git a/examples/qq-call-default.ilo b/examples/qq-call-default.ilo new file mode 100644 index 00000000..ed26135a --- /dev/null +++ b/examples/qq-call-default.ilo @@ -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 diff --git a/src/parser/mod.rs b/src/parser/mod.rs index 125053fd..92647069 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -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), @@ -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), diff --git a/tests/regression_prefix_nil_coalesce.rs b/tests/regression_prefix_nil_coalesce.rs index 45831868..d53e45e3 100644 --- a/tests/regression_prefix_nil_coalesce.rs +++ b/tests/regression_prefix_nil_coalesce.rs @@ -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!( @@ -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]