Merge pull request #8502 from roc-lang/fix-qualified-arrow

Fix qualified arrow function calls
This commit is contained in:
Richard Feldman 2025-11-29 17:10:37 -05:00 committed by GitHub
commit 2eb0f3e127
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 237 additions and 34 deletions

View file

@ -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);

View file

@ -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,

View file

@ -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)])]"))))
~~~

View file

@ -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**

View file

@ -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**

View file

@ -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**