From a4a8353ea79e7d5126ec17714c915079acb8abff Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Sat, 29 Nov 2025 16:06:58 -0500 Subject: [PATCH] Fix qualified arrow function calls --- src/fmt/fmt.zig | 14 +- src/parse/Parser.zig | 52 +++--- test/snapshots/arrow_qualified_functions.md | 187 ++++++++++++++++++++ test/snapshots/fuzz_crash/fuzz_crash_023.md | 6 +- test/snapshots/fuzz_crash/fuzz_crash_027.md | 6 +- test/snapshots/fuzz_crash/fuzz_crash_028.md | Bin 56742 -> 56744 bytes test/snapshots/syntax_grab_bag.md | 6 +- 7 files changed, 237 insertions(+), 34 deletions(-) create mode 100644 test/snapshots/arrow_qualified_functions.md diff --git a/src/fmt/fmt.zig b/src/fmt/fmt.zig index c31a369b0c..dd9f99346b 100644 --- a/src/fmt/fmt.zig +++ b/src/fmt/fmt.zig @@ -953,7 +953,19 @@ const Formatter = struct { if (multiline and try fmt.flushCommentsAfter(ld.operator)) { try fmt.pushIndent(); } - _ = try fmt.formatExprInner(ld.right, .no_indent_on_access); + // For arrow syntax, omit empty parens: `foo->bar()` becomes `foo->bar` + const right_expr = fmt.ast.store.getExpr(ld.right); + if (right_expr == .apply) { + const apply = right_expr.apply; + if (fmt.ast.store.exprSlice(apply.args).len == 0) { + // Zero-arg apply: just format the function, not the empty parens + _ = try fmt.formatExprInner(apply.@"fn", .no_indent_on_access); + } else { + _ = try fmt.formatExprInner(ld.right, .no_indent_on_access); + } + } else { + _ = try fmt.formatExprInner(ld.right, .no_indent_on_access); + } }, .int => |i| { try fmt.pushTokenText(i.token); diff --git a/src/parse/Parser.zig b/src/parse/Parser.zig index 712efebc60..605b665593 100644 --- a/src/parse/Parser.zig +++ b/src/parse/Parser.zig @@ -2199,30 +2199,34 @@ pub fn parseExprWithBp(self: *Parser, min_bp: u8) Error!AST.Expr.Idx { } else if (self.peek() == .OpArrow) { const s = self.pos; self.advance(); - if (self.peek() == .LowerIdent) { - const empty_qualifiers = try self.store.tokenSpanFrom(self.store.scratchTokenTop()); - const ident = try self.store.addExpr(.{ .ident = .{ - .region = .{ .start = self.pos, .end = self.pos }, - .token = self.pos, - .qualifiers = empty_qualifiers, - } }); - self.advance(); - const ident_suffixed = try self.parseExprSuffix(s, ident); - expression = try self.store.addExpr(.{ .local_dispatch = .{ - .region = .{ .start = start, .end = self.pos }, - .operator = s, - .left = expression, - .right = ident_suffixed, - } }); - } else if (self.peek() == .UpperIdent) { // UpperIdent - should be a tag - const empty_qualifiers = try self.store.tokenSpanFrom(self.store.scratchTokenTop()); - const tag = try self.store.addExpr(.{ .tag = .{ - .region = .{ .start = self.pos, .end = self.pos }, - .token = self.pos, - .qualifiers = empty_qualifiers, - } }); - self.advance(); - const ident_suffixed = try self.parseExprSuffix(s, tag); + const first_token_tag = self.peek(); + if (first_token_tag == .LowerIdent or first_token_tag == .UpperIdent) { + const ident_start = self.pos; + const qual_result = try self.parseQualificationChain(); + // Use final token as end position to avoid newline tokens + self.pos = qual_result.final_token + 1; + + // Determine if final token is a tag (UpperIdent or ends with NoSpaceDotUpperIdent) + // For unqualified names, check the original token; for qualified names, use is_upper + const is_tag = if (qual_result.qualifiers.span.len == 0) + first_token_tag == .UpperIdent + else + qual_result.is_upper; + + const expr_node = if (is_tag) + try self.store.addExpr(.{ .tag = .{ + .region = .{ .start = ident_start, .end = self.pos }, + .token = qual_result.final_token, + .qualifiers = qual_result.qualifiers, + } }) + else + try self.store.addExpr(.{ .ident = .{ + .region = .{ .start = ident_start, .end = self.pos }, + .token = qual_result.final_token, + .qualifiers = qual_result.qualifiers, + } }); + + const ident_suffixed = try self.parseExprSuffix(s, expr_node); expression = try self.store.addExpr(.{ .local_dispatch = .{ .region = .{ .start = start, .end = self.pos }, .operator = s, diff --git a/test/snapshots/arrow_qualified_functions.md b/test/snapshots/arrow_qualified_functions.md new file mode 100644 index 0000000000..240c5bd4fa --- /dev/null +++ b/test/snapshots/arrow_qualified_functions.md @@ -0,0 +1,187 @@ +# META +~~~ini +description=Arrow syntax with qualified functions and formatter dropping empty parens +type=snippet +~~~ +# SOURCE +~~~roc +# Test qualified function calls with arrow syntax +test1 = "hello"->Str.is_empty +test2 = "hello"->Str.is_empty() +test3 = "hello"->Str.concat("bar") + +# Test unqualified function calls +fn0 = |a| a +test4 = 10->fn0 +test5 = 10->fn0() + +# Test tag syntax +test6 = 42->Ok +test7 = 42->Ok() +~~~ +# EXPECTED +NIL +# PROBLEMS +NIL +# TOKENS +~~~zig +LowerIdent,OpAssign,StringStart,StringPart,StringEnd,OpArrow,UpperIdent,NoSpaceDotLowerIdent, +LowerIdent,OpAssign,StringStart,StringPart,StringEnd,OpArrow,UpperIdent,NoSpaceDotLowerIdent,NoSpaceOpenRound,CloseRound, +LowerIdent,OpAssign,StringStart,StringPart,StringEnd,OpArrow,UpperIdent,NoSpaceDotLowerIdent,NoSpaceOpenRound,StringStart,StringPart,StringEnd,CloseRound, +LowerIdent,OpAssign,OpBar,LowerIdent,OpBar,LowerIdent, +LowerIdent,OpAssign,Int,OpArrow,LowerIdent, +LowerIdent,OpAssign,Int,OpArrow,LowerIdent,NoSpaceOpenRound,CloseRound, +LowerIdent,OpAssign,Int,OpArrow,UpperIdent, +LowerIdent,OpAssign,Int,OpArrow,UpperIdent,NoSpaceOpenRound,CloseRound, +EndOfFile, +~~~ +# PARSE +~~~clojure +(file + (type-module) + (statements + (s-decl + (p-ident (raw "test1")) + (e-local-dispatch + (e-string + (e-string-part (raw "hello"))) + (e-ident (raw "Str.is_empty")))) + (s-decl + (p-ident (raw "test2")) + (e-local-dispatch + (e-string + (e-string-part (raw "hello"))) + (e-apply + (e-ident (raw "Str.is_empty"))))) + (s-decl + (p-ident (raw "test3")) + (e-local-dispatch + (e-string + (e-string-part (raw "hello"))) + (e-apply + (e-ident (raw "Str.concat")) + (e-string + (e-string-part (raw "bar")))))) + (s-decl + (p-ident (raw "fn0")) + (e-lambda + (args + (p-ident (raw "a"))) + (e-ident (raw "a")))) + (s-decl + (p-ident (raw "test4")) + (e-local-dispatch + (e-int (raw "10")) + (e-ident (raw "fn0")))) + (s-decl + (p-ident (raw "test5")) + (e-local-dispatch + (e-int (raw "10")) + (e-apply + (e-ident (raw "fn0"))))) + (s-decl + (p-ident (raw "test6")) + (e-local-dispatch + (e-int (raw "42")) + (e-tag (raw "Ok")))) + (s-decl + (p-ident (raw "test7")) + (e-local-dispatch + (e-int (raw "42")) + (e-apply + (e-tag (raw "Ok"))))))) +~~~ +# FORMATTED +~~~roc +# Test qualified function calls with arrow syntax +test1 = "hello"->Str.is_empty +test2 = "hello"->Str.is_empty +test3 = "hello"->Str.concat("bar") + +# Test unqualified function calls +fn0 = |a| a +test4 = 10->fn0 +test5 = 10->fn0 + +# Test tag syntax +test6 = 42->Ok +test7 = 42->Ok +~~~ +# CANONICALIZE +~~~clojure +(can-ir + (d-let + (p-assign (ident "test1")) + (e-call + (e-lookup-external + (builtin)) + (e-string + (e-literal (string "hello"))))) + (d-let + (p-assign (ident "test2")) + (e-call + (e-lookup-external + (builtin)) + (e-string + (e-literal (string "hello"))))) + (d-let + (p-assign (ident "test3")) + (e-call + (e-lookup-external + (builtin)) + (e-string + (e-literal (string "hello"))) + (e-string + (e-literal (string "bar"))))) + (d-let + (p-assign (ident "fn0")) + (e-lambda + (args + (p-assign (ident "a"))) + (e-lookup-local + (p-assign (ident "a"))))) + (d-let + (p-assign (ident "test4")) + (e-call + (e-lookup-local + (p-assign (ident "fn0"))) + (e-num (value "10")))) + (d-let + (p-assign (ident "test5")) + (e-call + (e-lookup-local + (p-assign (ident "fn0"))) + (e-num (value "10")))) + (d-let + (p-assign (ident "test6")) + (e-tag (name "Ok") + (args + (e-num (value "42"))))) + (d-let + (p-assign (ident "test7")) + (e-tag (name "Ok") + (args + (e-num (value "42")))))) +~~~ +# TYPES +~~~clojure +(inferred-types + (defs + (patt (type "Bool")) + (patt (type "Bool")) + (patt (type "Str")) + (patt (type "b -> b")) + (patt (type "b where [b.from_numeral : Numeral -> Try(b, [InvalidNumeral(Str)])]")) + (patt (type "b where [b.from_numeral : Numeral -> Try(b, [InvalidNumeral(Str)])]")) + (patt (type "[Ok(b)]_others where [b.from_numeral : Numeral -> Try(b, [InvalidNumeral(Str)])]")) + (patt (type "[Ok(b)]_others where [b.from_numeral : Numeral -> Try(b, [InvalidNumeral(Str)])]"))) + (expressions + (expr (type "Bool")) + (expr (type "Bool")) + (expr (type "Str")) + (expr (type "b -> b")) + (expr (type "b where [b.from_numeral : Numeral -> Try(b, [InvalidNumeral(Str)])]")) + (expr (type "b where [b.from_numeral : Numeral -> Try(b, [InvalidNumeral(Str)])]")) + (expr (type "[Ok(b)]_others where [b.from_numeral : Numeral -> Try(b, [InvalidNumeral(Str)])]")) + (expr (type "[Ok(b)]_others where [b.from_numeral : Numeral -> Try(b, [InvalidNumeral(Str)])]")))) +~~~ diff --git a/test/snapshots/fuzz_crash/fuzz_crash_023.md b/test/snapshots/fuzz_crash/fuzz_crash_023.md index 3e5e165386..cc417bb0a1 100644 --- a/test/snapshots/fuzz_crash/fuzz_crash_023.md +++ b/test/snapshots/fuzz_crash/fuzz_crash_023.md @@ -243,7 +243,7 @@ UNUSED VARIABLE - fuzz_crash_023.md:1:1:1:1 NOT IMPLEMENTED - :0:0:0:0 UNUSED VARIABLE - fuzz_crash_023.md:1:1:1:1 NOT IMPLEMENTED - :0:0:0:0 -UNDEFINED VARIABLE - fuzz_crash_023.md:121:37:121:37 +UNDEFINED VARIABLE - fuzz_crash_023.md:121:37:121:40 UNUSED VARIABLE - fuzz_crash_023.md:121:21:121:27 UNUSED VARIABLE - fuzz_crash_023.md:127:4:128:9 NOT IMPLEMENTED - :0:0:0:0 @@ -594,11 +594,11 @@ This error doesn't have a proper diagnostic report yet. Let us know if you want Nothing is named `add` in this scope. Is there an `import` or `exposing` missing up-top? -**fuzz_crash_023.md:121:37:121:37:** +**fuzz_crash_023.md:121:37:121:40:** ```roc { foo: 1, bar: 2, ..rest } => 12->add(34) ``` - ^ + ^^^ **UNUSED VARIABLE** diff --git a/test/snapshots/fuzz_crash/fuzz_crash_027.md b/test/snapshots/fuzz_crash/fuzz_crash_027.md index 5d924181d2..581b83ca50 100644 --- a/test/snapshots/fuzz_crash/fuzz_crash_027.md +++ b/test/snapshots/fuzz_crash/fuzz_crash_027.md @@ -199,7 +199,7 @@ NOT IMPLEMENTED - :0:0:0:0 UNUSED VARIABLE - fuzz_crash_027.md:1:1:1:1 UNUSED VARIABLE - fuzz_crash_027.md:76:1:76:4 NOT IMPLEMENTED - :0:0:0:0 -UNDEFINED VARIABLE - fuzz_crash_027.md:82:37:82:37 +UNDEFINED VARIABLE - fuzz_crash_027.md:82:37:82:40 UNUSED VARIABLE - fuzz_crash_027.md:82:21:82:27 NOT IMPLEMENTED - :0:0:0:0 NOT IMPLEMENTED - :0:0:0:0 @@ -603,11 +603,11 @@ This error doesn't have a proper diagnostic report yet. Let us know if you want Nothing is named `add` in this scope. Is there an `import` or `exposing` missing up-top? -**fuzz_crash_027.md:82:37:82:37:** +**fuzz_crash_027.md:82:37:82:40:** ```roc { foo: 1, bar: 2, ..rest } => 12->add(34) ``` - ^ + ^^^ **UNUSED VARIABLE** diff --git a/test/snapshots/fuzz_crash/fuzz_crash_028.md b/test/snapshots/fuzz_crash/fuzz_crash_028.md index 24b2c8e315f3d26ca578b9298fd4e6cf76e1c258..e75835434f87b8f40443c3fb16397bf1da8e99bd 100644 GIT binary patch delta 41 wcmZ3sn|Z}<<_*?-OeO}KZTW(ff%N2wh6$5z+j35}_LQ3JuX}RyZKKnx050bbssI20 delta 41 xcmZ3nn|ax8<_*?-OvdJ$ZTW(ff%N2wh6$7J%d=0u=^{1RU-#tZyGEy10RT0=5H$b* diff --git a/test/snapshots/syntax_grab_bag.md b/test/snapshots/syntax_grab_bag.md index 0aee394e23..a615352ca4 100644 --- a/test/snapshots/syntax_grab_bag.md +++ b/test/snapshots/syntax_grab_bag.md @@ -238,7 +238,7 @@ UNUSED VARIABLE - syntax_grab_bag.md:1:1:1:1 NOT IMPLEMENTED - :0:0:0:0 UNUSED VARIABLE - syntax_grab_bag.md:1:1:1:1 NOT IMPLEMENTED - :0:0:0:0 -UNDEFINED VARIABLE - syntax_grab_bag.md:121:37:121:37 +UNDEFINED VARIABLE - syntax_grab_bag.md:121:37:121:40 UNUSED VARIABLE - syntax_grab_bag.md:121:21:121:27 UNUSED VARIABLE - syntax_grab_bag.md:127:4:128:9 NOT IMPLEMENTED - :0:0:0:0 @@ -529,11 +529,11 @@ This error doesn't have a proper diagnostic report yet. Let us know if you want Nothing is named `add` in this scope. Is there an `import` or `exposing` missing up-top? -**syntax_grab_bag.md:121:37:121:37:** +**syntax_grab_bag.md:121:37:121:40:** ```roc { foo: 1, bar: 2, ..rest } => 12->add(34) ``` - ^ + ^^^ **UNUSED VARIABLE**