Skip to content

Commit d50e974

Browse files
committed
Support non-empty list patterns and robust multiline lambda application
1 parent b4cb99f commit d50e974

File tree

7 files changed

+109
-6
lines changed

7 files changed

+109
-6
lines changed

src/FScript.Language/Ast.fs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ and Expr =
4141
| EUnit of Span
4242
| ELiteral of Literal * Span
4343
| EVar of string * Span
44+
| EParen of Expr * Span
4445
| ELambda of Param * Expr * Span
4546
| EApply of Expr * Expr * Span
4647
| EIf of Expr * Expr * Expr * Span
@@ -96,6 +97,7 @@ module Ast =
9697
| EUnit s -> s
9798
| ELiteral (_, s) -> s
9899
| EVar (_, s) -> s
100+
| EParen (_, s) -> s
99101
| ELambda (_, _, s) -> s
100102
| EApply (_, _, s) -> s
101103
| EIf (_, _, _, s) -> s

src/FScript.Language/Eval.fs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,8 @@ module Eval =
150150
match env |> Map.tryFind name with
151151
| Some v -> v
152152
| None -> raise (EvalException { Message = sprintf "Unbound variable '%s'" name; Span = span })
153+
| EParen (inner, _) ->
154+
evalExpr typeDefs env inner
153155
| ELambda (param, body, _) -> VClosure (param.Name, body, ref env)
154156
| EApply (fn, arg, span) ->
155157
let fVal = evalExpr typeDefs env fn

src/FScript.Language/Parser.fs

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,17 @@ module Parser =
378378
if stream.Match(RBracket) then
379379
PNil (mkSpanFrom lb.Span lb.Span)
380380
else
381-
raise (ParseException { Message = "Only empty list pattern [] supported"; Span = lb.Span })
381+
let first = parsePatternCons()
382+
let elements = ResizeArray<Pattern>()
383+
elements.Add(first)
384+
while stream.Match(Semicolon) do
385+
if stream.Peek().Kind <> RBracket then
386+
elements.Add(parsePatternCons())
387+
let rb = stream.Expect(RBracket, "Expected ']' in list pattern")
388+
let listPattern =
389+
(elements |> Seq.toList, PNil (mkSpanFrom rb.Span rb.Span))
390+
||> List.foldBack (fun head tail -> PCons(head, tail, mkSpanFrom (Ast.spanOfPattern head) (Ast.spanOfPattern tail)))
391+
listPattern
382392
| LParen ->
383393
let lp = stream.Next()
384394
let first = parsePatternCons()
@@ -460,8 +470,8 @@ module Parser =
460470
let rp = stream.Expect(RParen, "Expected ')' after tuple expression")
461471
ETuple(elements |> Seq.toList, mkSpanFrom lp.Span rp.Span)
462472
else
463-
stream.Expect(RParen, "Expected ')' after expression") |> ignore
464-
first
473+
let rp = stream.Expect(RParen, "Expected ')' after expression")
474+
EParen(first, mkSpanFrom lp.Span rp.Span)
465475
| LBracket ->
466476
let lb = stream.Next()
467477
if stream.Match(RBracket) then
@@ -935,6 +945,10 @@ module Parser =
935945
| Dedent ->
936946
stream.Next() |> ignore
937947
doneBlock <- true
948+
| RParen
949+
| RBracket
950+
| RBrace ->
951+
doneBlock <- true
938952
| EOF -> doneBlock <- true
939953
| Let ->
940954
statements.Add(parseStmt())
@@ -1042,7 +1056,7 @@ module Parser =
10421056
while not stream.AtEnd do
10431057
match stream.Peek().Kind with
10441058
| EOF -> stream.Next() |> ignore
1045-
| Dedent -> raise (ParseException { Message = "Unexpected dedent at top level"; Span = stream.Peek().Span })
1059+
| Dedent -> stream.Next() |> ignore
10461060
| _ ->
10471061
let stmt = parseStmt()
10481062
program.Add(stmt)

src/FScript.Language/TypeInfer.fs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,9 @@ module TypeInfer =
256256
let t = instantiate scheme
257257
emptySubst, t, asTyped expr t
258258
| None -> raise (TypeException { Message = sprintf "Unbound variable '%s'" name; Span = span })
259+
| EParen (inner, _) ->
260+
let s, t, _ = inferExpr typeDefs constructors env inner
261+
s, t, asTyped expr t
259262
| ETypeOf (name, span) ->
260263
if typeDefs.ContainsKey name then
261264
emptySubst, TTypeToken, asTyped expr TTypeToken

tests/FScript.Language.Tests/EvalTests.fs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,29 @@ type EvalTests () =
9696
let src = "match [1;2] with\n | x::xs -> x\n | [] -> 0"
9797
Helpers.eval src |> assertInt 1L
9898

99+
[<Test>]
100+
member _.``Evaluates match on non-empty list literal pattern`` () =
101+
let src = "match [1] with\n | [x] -> x\n | _ -> 0"
102+
Helpers.eval src |> assertInt 1L
103+
104+
[<Test>]
105+
member _.``Evaluates match on Some with non-empty list literal pattern`` () =
106+
let src = "match Some [1] with\n | Some [x] -> x\n | _ -> 0"
107+
Helpers.eval src |> assertInt 1L
108+
109+
[<Test>]
110+
member _.``Evaluates multiline map fold with match and cons`` () =
111+
let src =
112+
"let apply f x = f x\n" +
113+
"let f value = apply (fun item ->\n" +
114+
" match item with\n" +
115+
" | \"workspace:*\" -> value :: []\n" +
116+
" | _ -> []) value\n" +
117+
"f \"workspace:*\""
118+
match Helpers.eval src with
119+
| VList [ VString "workspace:*" ] -> ()
120+
| _ -> Assert.Fail("Expected [\"workspace:*\"]")
121+
99122
[<Test>]
100123
member _.``Evaluates option values`` () =
101124
match Helpers.eval "Some 4" with

tests/FScript.Language.Tests/ParserTests.fs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ type ParserTests () =
131131
member _.``Parses let expression without in`` () =
132132
let p = Helpers.parse "let x = (let y = 1\n y + 1\n)"
133133
match p.[0] with
134-
| SLet (_, _, ELet ("y", _, _, _, _), _, _, _) -> ()
134+
| SLet (_, _, EParen (ELet ("y", _, _, _, _), _), _, _, _) -> ()
135135
| _ -> Assert.Fail("Expected nested let expression")
136136

137137
[<Test>]
@@ -249,6 +249,22 @@ type ParserTests () =
249249
| SExpr (EMatch (_, cases, _)) -> cases.Length |> should equal 2
250250
| _ -> Assert.Fail("Expected match")
251251

252+
[<Test>]
253+
member _.``Parses match with non-empty list literal pattern`` () =
254+
let src = "match [1] with\n| [x] -> x\n| _ -> 0"
255+
let program = Helpers.parse src
256+
match program.[0] with
257+
| SExpr (EMatch (_, (PCons (_, PNil _, _), _, _) :: _, _)) -> ()
258+
| _ -> Assert.Fail("Expected non-empty list pattern")
259+
260+
[<Test>]
261+
member _.``Parses Some with non-empty list literal pattern`` () =
262+
let src = "match Some [1] with\n| Some [x] -> x\n| _ -> 0"
263+
let program = Helpers.parse src
264+
match program.[0] with
265+
| SExpr (EMatch (_, (PSome (PCons (_, PNil _, _), _), _, _) :: _, _)) -> ()
266+
| _ -> Assert.Fail("Expected Some with non-empty list pattern")
267+
252268
[<Test>]
253269
member _.``Parses match with option patterns`` () =
254270
let src = "match Some 1 with\n | Some x -> x\n | None -> 0"
@@ -313,6 +329,14 @@ type ParserTests () =
313329
| SLet ("f", [_], ELet _, false, _, _) -> ()
314330
| _ -> Assert.Fail("Expected block-desugared let")
315331

332+
[<Test>]
333+
member _.``Parses multiline lambda argument closed by parenthesis on same line`` () =
334+
let src = "Map.fold (fun acc key value ->\n match value with\n | \"workspace:*\" -> key :: acc\n | _ -> acc) [] #{ \"a\" = \"workspace:*\" }"
335+
let p = Helpers.parse src
336+
match p.[0] with
337+
| SExpr (EApply (EApply (EApply (EFieldGet (EVar ("Map", _), "fold", _), _, _), _, _), _, _)) -> ()
338+
| _ -> Assert.Fail("Expected multiline lambda argument application")
339+
316340
[<Test>]
317341
member _.``Parses exported top-level let binding`` () =
318342
let p = Helpers.parse "export let cosine x = x"
@@ -346,7 +370,7 @@ type ParserTests () =
346370
let src = "(let rec even n = if n = 0 then true else odd (n - 1)\nand odd n = if n = 0 then false else even (n - 1)\neven 4\n)"
347371
let p = Helpers.parse src
348372
match p.[0] with
349-
| SExpr (ELetRecGroup (bindings, _, _)) -> bindings.Length |> should equal 2
373+
| SExpr (EParen (ELetRecGroup (bindings, _, _), _)) -> bindings.Length |> should equal 2
350374
| _ -> Assert.Fail("Expected recursive let-expression group")
351375

352376
[<Test>]

tests/FScript.Language.Tests/TypeInferenceTests.fs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,20 @@ type TypeInferenceTests () =
230230
| TypeInfer.TSExpr te -> te.Type |> should equal TInt
231231
| _ -> Assert.Fail("Expected expression")
232232

233+
[<Test>]
234+
member _.``Infers match on non-empty list literal pattern`` () =
235+
let typed = Helpers.infer "match [1] with\n | [x] -> x\n | _ -> 0"
236+
match typed |> List.last with
237+
| TypeInfer.TSExpr te -> te.Type |> should equal TInt
238+
| _ -> Assert.Fail("Expected expression")
239+
240+
[<Test>]
241+
member _.``Infers match on Some with non-empty list literal pattern`` () =
242+
let typed = Helpers.infer "match Some [1] with\n | Some [x] -> x\n | _ -> 0"
243+
match typed |> List.last with
244+
| TypeInfer.TSExpr te -> te.Type |> should equal TInt
245+
| _ -> Assert.Fail("Expected expression")
246+
233247
[<Test>]
234248
member _.``Infers match on tuple`` () =
235249
let typed = Helpers.infer "match (1, true) with\n | (x, true) -> x\n | _ -> 0"
@@ -330,6 +344,27 @@ type TypeInferenceTests () =
330344
let act () = Helpers.infer "let f (x: int) = x + true" |> ignore
331345
act |> should throw typeof<TypeException>
332346

347+
[<Test>]
348+
member _.``Infers multiline map fold with match and cons`` () =
349+
let typed =
350+
Helpers.infer
351+
"let apply f x = f x\nlet f value = apply (fun item ->\n match item with\n | \"workspace:*\" -> value :: []\n | _ -> []) value\nf"
352+
match typed |> List.last with
353+
| TypeInfer.TSExpr te ->
354+
match te.Type with
355+
| TFun (TString, TList TString) -> ()
356+
| _ -> Assert.Fail($"Expected string -> string list, got {Types.typeToString te.Type}")
357+
| _ -> Assert.Fail("Expected expression")
358+
359+
[<Test>]
360+
member _.``Infers annotated lambda field access inside Option.map`` () =
361+
let typed =
362+
Helpers.infer
363+
"type Package = { name: string }\nlet get_name = fun (value: Package) -> value.name\nget_name"
364+
match typed |> List.last with
365+
| TypeInfer.TSExpr te -> te.Type |> should equal (TFun (TRecord (Map.ofList [ "name", TString ]), TString))
366+
| _ -> Assert.Fail("Expected expression")
367+
333368
[<Test>]
334369
member _.``Infers typeof as type token`` () =
335370
let typed = Helpers.infer "type Package = { Name: string }\ntypeof Package"

0 commit comments

Comments
 (0)