Skip to content

Commit 280e1cd

Browse files
SchlenkRclaude
authored andcommitted
Parser: collapse double-parens at call sites with records
Call-site `(…)` now accepts the same content as a record primary. Named items or a trailing `,` signal "one record arg"; pure-positional lists keep today's curried fold so existing code is unchanged: f() App(f, UnitValue) unchanged f(x) App(f, x) unchanged f(x, y) App(App(f, x), y) unchanged (curried) f((age: 30)) App(f, record) unchanged (inner primary is record) f(age: 30) App(f, record) NEW — collapses to single paren f(x,) App(f, 1-record(x)) NEW — trailing-comma as record f(x, name: v) App(f, record) NEW — mixed items Surface-syntax only; no semantic shift. Ronald's framing: "application is a space, and (…) is either grouping or a record — both work the same way whether or not the function is juxtaposed with them." Playground examples with record args now use single parens: getName(name: "Ada", age: 42) useName(age: 30) ToString(x: 1, y: 2) Two new parser tests verify the collapse and the equivalence with the double-paren form. Tests: 87 Parser + 75 Core + 25 Playground (all green). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 8fb080b commit 280e1cd

3 files changed

Lines changed: 38 additions & 9 deletions

File tree

‎src/TypeFighter.Parser01.Tests/TestCases/Records.fs‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,20 @@ let ``parens without trailing comma are grouping`` () =
3232
"""( 42 )"""
3333
|> shouldParseTo (X.Lit 42)
3434

35+
[<Test>]
36+
let ``call-with-named-arg collapses to single parens`` () =
37+
"""f(name: "Ada")"""
38+
|> shouldParseTo
39+
(X.App (X.Var "f")
40+
(X.MkRecord [ X.Property "name" (X.Lit "Ada") ]))
41+
42+
[<Test>]
43+
let ``call-with-double-parens equals call-with-single`` () =
44+
let a = parse """f(name: "Ada")"""
45+
let b = parse """f((name: "Ada"))"""
46+
if a <> b then
47+
failwithf "Expected same AST.\n a: %A\n b: %A" a b
48+
3549

3650
// ---- Property access --------------------------------------------
3751

‎src/TypeFighter.Parser01/Syntax.fs‎

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -391,17 +391,32 @@ let private arrow =
391391

392392
type private CallTail = Expr<unit> -> Expr<unit>
393393

394+
// Call-site `(…)` accepts the same shape as a record primary.
395+
// Named items or a trailing `,` ⇒ one record arg; pure-positional
396+
// lists keep today's curried fold so existing code is unaffected.
394397
let private callArgs : Parser<CallTail> =
398+
let trailingComma =
399+
(sym "," |> map (fun _ -> true))
400+
<|> mkParser (fun inp -> POk.create inp.idx inp.idx false)
395401
parse {
396402
let! openP = sym "("
397-
let! args = commaList expr
403+
let! items = commaList recordItem
404+
let! comma = trailingComma
398405
let! closeP = sym ")"
399406
let apply (src: Expr<unit>) =
400-
match args.result with
401-
| [] -> X.App src (X.Var "UnitValue") // f() ≡ f UnitValue
402-
| xs -> xs |> List.fold (fun acc a -> X.App acc a) src
407+
let hasNamed =
408+
items.result |> List.exists (fun i -> i.TryAsPositional.IsNone)
409+
match items.result, comma.result, hasNamed with
410+
| [], _, _ ->
411+
X.App src (X.Var "UnitValue")
412+
| _, true, _
413+
| _, _, true ->
414+
X.App src (X.MkRecord items.result)
415+
| xs, false, false ->
416+
let exprs = xs |> List.choose (fun i -> i.TryAsPositional)
417+
exprs |> List.fold (fun acc a -> X.App acc a) src
403418
return
404-
{ range = Range.merge [ openP.range; args.range; closeP.range ]
419+
{ range = Range.merge [ openP.range; items.range; closeP.range ]
405420
result = apply }
406421
}
407422

‎web/src/examples.json‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
},
3434
{
3535
"title": "structural accessor",
36-
"source": "// `getName` is inferred from its body alone — no annotations.\n// Any record with a `name` field of type String works.\n\nlet getName = r => r.name;\nlog(getName((name: \"Ada\", age: 42)));\nlog(getName((name: \"Bob\", age: 31)));"
36+
"source": "// `getName` is inferred from its body alone — no annotations.\n// Any record with a `name` field of type String works.\n\nlet getName = r => r.name;\nlog(getName(name: \"Ada\", age: 42));\nlog(getName(name: \"Bob\", age: 31));"
3737
},
3838
{
3939
"title": "record from function",
@@ -84,15 +84,15 @@
8484
"examples": [
8585
{
8686
"title": "ToString is polymorphic",
87-
"source": "// ToString's type is `tv_0 -> String` — one instantiation per\n// call site. Three unrelated argument types, one function.\n\nlog(ToString(42));\nlog(ToString(true));\nlog(ToString((x: 1, y: 2)));"
87+
"source": "// ToString's type is `tv_0 -> String` — one instantiation per\n// call site. Three unrelated argument types, one function.\n\nlog(ToString(42));\nlog(ToString(true));\nlog(ToString(x: 1, y: 2));"
8888
},
8989
{
9090
"title": "inferred function type",
9191
"source": "// No annotations anywhere. The type checker works out:\n// greet : String -> String\n// from the shape of concat and \"Hello, \".\n\nlet greet = s => concat(\"Hello, \")(s);\nlog(greet(\"Ada\"));"
9292
},
9393
{
9494
"title": "inferred record shape",
95-
"source": "// `getX`'s inferred type is `( x: tv_0 & y: Number ) -> tv_0` —\n// both the field set and each field's type come from the call\n// sites, with no annotation on `getX`.\n\nlet getX = p => p.x;\nlog(ToString(getX((x: 10, y: 20))));\nlog(ToString(getX((x: 99, y: 100))));"
95+
"source": "// `getX`'s inferred type is `( x: tv_0 & y: Number ) -> tv_0` —\n// both the field set and each field's type come from the call\n// sites, with no annotation on `getX`.\n\nlet getX = p => p.x;\nlog(ToString(getX(x: 10, y: 20)));\nlog(ToString(getX(x: 99, y: 100)));"
9696
},
9797
{
9898
"title": "function as value",
@@ -122,7 +122,7 @@
122122
},
123123
{
124124
"title": "record shape mismatch",
125-
"source": "// useName demands a record with a `name` field. The argument has\n// `age` but no `name` — \"required field missing\" fires.\n\nlet useName = r => r.name;\nuseName((age: 30));"
125+
"source": "// useName demands a record with a `name` field. The argument has\n// `age` but no `name` — \"required field missing\" fires.\n\nlet useName = r => r.name;\nuseName(age: 30);"
126126
},
127127
{
128128
"title": "parse error",

0 commit comments

Comments
 (0)