Skip to content

Commit f58b809

Browse files
committed
feat(language): add tuple let destructuring and LSP signatures for conversions
1 parent 5f21900 commit f58b809

13 files changed

Lines changed: 368 additions & 26 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ All notable changes to FScript are documented in this file.
44

55
## [Unreleased]
66
- Added `Int/Float/Bool` conversion helpers in stdlib-style builtins (`*.tryParse` and `*.toString`) for safe scalar parsing and string formatting.
7+
- Added tuple let destructuring (`let (a, b) = ...`) for top-level, block, and let-expression non-rec bindings.
78

89
## [0.40.0]
910

docs/specs/syntax-and-indentation.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,11 @@ This document describes the concrete syntax accepted by the interpreter and the
2323
- `fun (x: int) -> expr`
2424
- Let expression (layout style):
2525
- `let x = expr`
26+
- `let (a, b) = expr` (tuple destructuring)
2627
- `let rec f x = ... and g y = ...`
2728
- nested via blocks
29+
- tuple destructuring is supported for non-`rec` `let` bindings only
30+
- `[<export>] let` requires a single identifier binding (tuple patterns are rejected)
2831
- `[<export>]` is only valid on top-level `let` bindings
2932
- attribute names are case-sensitive (`[<export>]` is valid, `[<Export>]` is not)
3033
- Conditionals:

samples/add.fss

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
let result =
1+
let (firstRaw, secondRaw) =
22
match Env.Arguments with
3-
| first :: second :: _ ->
4-
match (Int.tryParse first, Int.tryParse second) with
5-
| (Some firstValue, Some secondValue) -> firstValue + secondValue
6-
| _ -> raise "Invalid arguments: 2 ints expected"
7-
| _ -> raise "Required arguments: first second"
3+
| first :: second :: _ -> (first, second)
4+
| _ -> ("0", "0")
85

9-
result
6+
let (first, second) =
7+
match (Int.tryParse firstRaw, Int.tryParse secondRaw) with
8+
| (Some firstValue, Some secondValue) -> (firstValue, secondValue)
9+
| _ -> raise "Invalid arguments: 2 ints expected"
10+
11+
first + second

src/FScript.CSharpInterop/LanguageServer/LspSymbols.fs

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,13 +120,25 @@ module LspSymbols =
120120
let builtinSignatures =
121121
[ "ignore", "'a -> unit"
122122
"print", "string -> unit"
123+
"Int.tryParse", "string -> int option"
124+
"Float.tryParse", "string -> float option"
125+
"Bool.tryParse", "string -> bool option"
126+
"Int.toString", "int -> string"
127+
"Float.toString", "float -> string"
128+
"Bool.toString", "bool -> string"
123129
"nameof", "string -> string"
124130
"typeof", "string -> type" ]
125131
|> Map.ofList
126132

127133
let builtinParamNames =
128134
[ "ignore", [ "value" ]
129135
"print", [ "message" ]
136+
"Int.tryParse", [ "value" ]
137+
"Float.tryParse", [ "value" ]
138+
"Bool.tryParse", [ "value" ]
139+
"Int.toString", [ "value" ]
140+
"Float.toString", [ "value" ]
141+
"Bool.toString", [ "value" ]
130142
"nameof", [ "name" ]
131143
"typeof", [ "name" ] ]
132144
|> Map.ofList
@@ -425,12 +437,40 @@ module LspSymbols =
425437
match stmt with
426438
| TypeInfer.TSLet (name, _, t, _, _, _) when name = targetName ->
427439
Some t
440+
| TypeInfer.TSLetPattern (_, _, bindings, _, _) ->
441+
bindings |> Map.tryFind targetName
428442
| TypeInfer.TSLetRecGroup (bindings, _, _) ->
429443
bindings
430444
|> List.tryPick (fun (name, _, t, _) ->
431445
if name = targetName then Some t else None)
432446
| _ -> None)
433447

448+
let rec collectPatternNames (pat: Pattern) : string list =
449+
match pat with
450+
| PVar (name, _) -> [ name ]
451+
| PCons (head, tail, _) -> collectPatternNames head @ collectPatternNames tail
452+
| PTuple (items, _) -> items |> List.collect collectPatternNames
453+
| PRecord (fields, _) -> fields |> List.collect (fun (_, p) -> collectPatternNames p)
454+
| PMap (clauses, tailOpt, _) ->
455+
let fromClauses =
456+
clauses
457+
|> List.collect (fun (k, v) -> collectPatternNames k @ collectPatternNames v)
458+
let fromTail =
459+
match tailOpt with
460+
| Some tail -> collectPatternNames tail
461+
| None -> []
462+
fromClauses @ fromTail
463+
| PSome (inner, _) -> collectPatternNames inner
464+
| PUnionCase (_, _, payload, _) ->
465+
match payload with
466+
| Some p -> collectPatternNames p
467+
| None -> []
468+
| PWildcard _
469+
| PLiteral _
470+
| PNil _
471+
| PNone _
472+
| PTypeRef _ -> []
473+
434474
for stmt in program do
435475
match stmt with
436476
| SType _ ->
@@ -444,6 +484,16 @@ module LspSymbols =
444484
| Some t -> result <- result |> Map.add name t
445485
| None -> ()
446486
| None -> ()
487+
| SLetPattern (pattern, _, _, _) ->
488+
let candidate = accepted @ [ stmt ]
489+
match tryInferWithCurrent candidate with
490+
| Some typed ->
491+
accepted <- candidate
492+
for name in collectPatternNames pattern do
493+
match extractTypeForName typed name with
494+
| Some t -> result <- result |> Map.add name t
495+
| None -> ()
496+
| None -> ()
447497
| SLetRecGroup (bindings, _, _) ->
448498
let candidate = accepted @ [ stmt ]
449499
match tryInferWithCurrent candidate with
@@ -481,6 +531,7 @@ module LspSymbols =
481531
| SType _ ->
482532
accepted <- accepted @ [ stmt ]
483533
| SLet _
534+
| SLetPattern _
484535
| SLetRecGroup _ ->
485536
let candidate = accepted @ [ stmt ]
486537
match tryInferWithCurrent candidate with
@@ -529,6 +580,9 @@ module LspSymbols =
529580
| ELet (_, value, body, _, _) ->
530581
let withValue = collectExpr acc value
531582
collectExpr withValue body
583+
| ELetPattern (_, value, body, _) ->
584+
let withValue = collectExpr acc value
585+
collectExpr withValue body
532586
| ELetRecGroup (bindings, body, _) ->
533587
let withBindings =
534588
bindings
@@ -586,13 +640,44 @@ module LspSymbols =
586640
| ETypeOf _
587641
| ENameOf _ -> acc
588642

643+
let rec collectPatternVarSpans (pat: Pattern) : (string * Span) list =
644+
match pat with
645+
| PVar (name, span) -> [ name, span ]
646+
| PCons (head, tail, _) -> collectPatternVarSpans head @ collectPatternVarSpans tail
647+
| PTuple (items, _) -> items |> List.collect collectPatternVarSpans
648+
| PRecord (fields, _) -> fields |> List.collect (fun (_, p) -> collectPatternVarSpans p)
649+
| PMap (clauses, tailOpt, _) ->
650+
let fromClauses =
651+
clauses
652+
|> List.collect (fun (k, v) -> collectPatternVarSpans k @ collectPatternVarSpans v)
653+
let fromTail =
654+
match tailOpt with
655+
| Some tail -> collectPatternVarSpans tail
656+
| None -> []
657+
fromClauses @ fromTail
658+
| PSome (inner, _) -> collectPatternVarSpans inner
659+
| PUnionCase (_, _, payload, _) ->
660+
match payload with
661+
| Some p -> collectPatternVarSpans p
662+
| None -> []
663+
| PWildcard _
664+
| PLiteral _
665+
| PNil _
666+
| PNone _
667+
| PTypeRef _ -> []
668+
589669
let withDeclsAndExprs =
590670
program
591671
|> List.fold (fun state stmt ->
592672
match stmt with
593673
| SLet (name, _, expr, _, _, span) ->
594674
let withDecl = addOccurrence name span state
595675
collectExpr withDecl expr
676+
| SLetPattern (pattern, expr, _, _) ->
677+
let withDecls =
678+
collectPatternVarSpans pattern
679+
|> List.fold (fun inner (name, span) -> addOccurrence name span inner) state
680+
collectExpr withDecls expr
596681
| SLetRecGroup (bindings, _, _) ->
597682
bindings
598683
|> List.fold (fun inner (name, _, expr, span) ->
@@ -861,6 +946,8 @@ module LspSymbols =
861946
inScrutinee @ inCases
862947
| ELet (_, value, body, _, _) ->
863948
collectFieldVarUses fieldTypes value @ collectFieldVarUses fieldTypes body
949+
| ELetPattern (_, value, body, _) ->
950+
collectFieldVarUses fieldTypes value @ collectFieldVarUses fieldTypes body
864951
| ELetRecGroup (bindings, body, _) ->
865952
let inBindings =
866953
bindings
@@ -1007,6 +1094,10 @@ module LspSymbols =
10071094
| ELet (name, value, body, _, span) ->
10081095
mkBinding name span (Ast.spanOfExpr body) None
10091096
:: (collectExprBindings value @ collectExprBindings body)
1097+
| ELetPattern (pattern, value, body, _) ->
1098+
let scope = Ast.spanOfExpr body
1099+
let fromPattern = collectPatternBindings scope pattern
1100+
fromPattern @ collectExprBindings value @ collectExprBindings body
10101101
| ELetRecGroup (bindings, body, _) ->
10111102
let fromBindings =
10121103
bindings
@@ -1270,6 +1361,8 @@ module LspSymbols =
12701361
collectExpr (collectExpr acc source) body
12711362
| ELet (_, value, body, _, _) ->
12721363
collectExpr (collectExpr acc value) body
1364+
| ELetPattern (_, value, body, _) ->
1365+
collectExpr (collectExpr acc value) body
12731366
| ELetRecGroup (bindings, body, _) ->
12741367
let withBindings =
12751368
bindings |> List.fold (fun state (_, _, value, _) -> collectExpr state value) acc
@@ -1322,6 +1415,8 @@ module LspSymbols =
13221415
match stmt with
13231416
| SLet (_, _, expr, _, _, _) ->
13241417
collectExpr state expr
1418+
| SLetPattern (_, expr, _, _) ->
1419+
collectExpr state expr
13251420
| SLetRecGroup (bindings, _, _) ->
13261421
bindings |> List.fold (fun inner (_, _, expr, _) -> collectExpr inner expr) state
13271422
| SExpr expr ->
@@ -1388,6 +1483,8 @@ module LspSymbols =
13881483
collectExpr withGuard false body) withScrutinee
13891484
| ELet (_, value, body, _, _) ->
13901485
collectExpr (collectExpr acc false value) false body
1486+
| ELetPattern (_, value, body, _) ->
1487+
collectExpr (collectExpr acc false value) false body
13911488
| ELetRecGroup (bindings, body, _) ->
13921489
let withBindings =
13931490
bindings |> List.fold (fun state (_, _, value, _) -> collectExpr state false value) acc

src/FScript.Language/Ast.fs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ and Expr =
5252
| EFor of string * Expr * Expr * Span
5353
| EMatch of Expr * (Pattern * Expr option * Expr * Span) list * Span
5454
| ELet of string * Expr * Expr * bool * Span
55+
| ELetPattern of Pattern * Expr * Expr * Span
5556
| ELetRecGroup of (string * Param list * Expr * Span) list * Expr * Span
5657
| EList of Expr list * Span
5758
| ERange of Expr * Expr * Span
@@ -84,6 +85,7 @@ and Stmt =
8485
| SType of TypeDef
8586
| SImport of string * string * Span
8687
| SLet of string * Param list * Expr * bool * bool * Span
88+
| SLetPattern of Pattern * Expr * bool * Span
8789
| SLetRecGroup of (string * Param list * Expr * Span) list * bool * Span
8890
| SExpr of Expr
8991

@@ -118,6 +120,7 @@ module Ast =
118120
| EFor (_, _, _, s) -> s
119121
| EMatch (_, _, s) -> s
120122
| ELet (_, _, _, _, s) -> s
123+
| ELetPattern (_, _, _, s) -> s
121124
| ELetRecGroup (_, _, s) -> s
122125
| EList (_, s) -> s
123126
| ERange (_, _, s) -> s

src/FScript.Language/Eval.fs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,14 @@ module Eval =
481481
let v = evalExpr typeDefs env value
482482
let env' = env |> Map.add name v
483483
evalExpr typeDefs env' body
484+
| ELetPattern (pattern, value, body, span) ->
485+
let v = evalExpr typeDefs env value
486+
match patternMatch typeDefs pattern v with
487+
| Some bindings ->
488+
let env' = Map.fold (fun acc k value -> Map.add k value acc) env bindings
489+
evalExpr typeDefs env' body
490+
| None ->
491+
raise (EvalException { Message = "Let pattern did not match value"; Span = span })
484492
| ELetRecGroup (bindings, body, span) ->
485493
if bindings.IsEmpty then
486494
evalExpr typeDefs env body
@@ -736,6 +744,11 @@ module Eval =
736744
program
737745
|> List.tryPick (function
738746
| TypeInfer.TSLet(name, _, _, _, _, _) when Set.contains name reserved -> Some name
747+
| TypeInfer.TSLetPattern(_, _, bindings, _, _) ->
748+
bindings
749+
|> Map.toList
750+
|> List.tryFind (fun (name, _) -> Set.contains name reserved)
751+
|> Option.map fst
739752
| TypeInfer.TSLetRecGroup(bindings, _, _) ->
740753
bindings
741754
|> List.tryFind (fun (name, _, _, _) -> Set.contains name reserved)
@@ -872,6 +885,13 @@ module Eval =
872885
else
873886
let v = evalExpr typeDefs env expr
874887
env <- env |> Map.add name v
888+
| TypeInfer.TSLetPattern(pattern, expr, _, _, span) ->
889+
let v = evalExpr typeDefs env expr
890+
match patternMatch typeDefs pattern v with
891+
| Some bindings ->
892+
env <- Map.fold (fun acc k value -> Map.add k value acc) env bindings
893+
| None ->
894+
raise (EvalException { Message = "Let pattern did not match value"; Span = span })
875895
| TypeInfer.TSLetRecGroup(bindings, _, span) ->
876896
if bindings.IsEmpty then
877897
()

src/FScript.Language/IncludeResolver.fs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ module IncludeResolver =
174174
statements
175175
|> List.collect (function
176176
| SLet(name, _, _, _, _, _) -> [ name ]
177+
| SLetPattern(pattern, _, _, _) -> collectPatternBindings pattern |> Set.toList
177178
| SLetRecGroup(bindings, _, _) -> bindings |> List.map (fun (name, _, _, _) -> name)
178179
| _ -> [])
179180
|> Set.ofList
@@ -229,6 +230,10 @@ module IncludeResolver =
229230
ELet(name, rewriteExpr boundWithName valueExpr, rewriteExpr boundWithName bodyExpr, true, span)
230231
else
231232
ELet(name, rewriteExpr boundNames valueExpr, rewriteExpr (Set.add name boundNames) bodyExpr, false, span)
233+
| ELetPattern (pattern, valueExpr, bodyExpr, span) ->
234+
let rewrittenPattern = rewritePattern aliases selfPrefix topLevelTypeNames pattern
235+
let boundWithPattern = Set.union boundNames (collectPatternBindings rewrittenPattern)
236+
ELetPattern(rewrittenPattern, rewriteExpr boundNames valueExpr, rewriteExpr boundWithPattern bodyExpr, span)
232237
| ELetRecGroup (bindings, body, span) ->
233238
let names = bindings |> List.map (fun (name, _, _, _) -> name) |> Set.ofList
234239
let recursiveBound = Set.union boundNames names
@@ -316,6 +321,9 @@ module IncludeResolver =
316321
let bound = rewrittenArgs |> List.fold (fun s p -> Set.add p.Name s) Set.empty
317322
let bodyBound = if isRec then Set.add rewrittenName bound else bound
318323
SLet(rewrittenName, rewrittenArgs, rewriteExpr bodyBound valueExpr, isRec, isExported, span)
324+
| SLetPattern(pattern, valueExpr, isExported, span) ->
325+
let rewrittenPattern = rewritePattern aliases selfPrefix topLevelTypeNames pattern
326+
SLetPattern(rewrittenPattern, rewriteExpr Set.empty valueExpr, isExported, span)
319327
| SLetRecGroup(bindings, isExported, span) ->
320328
let names = bindings |> List.map (fun (name, _, _, _) -> qualifySelfName name) |> Set.ofList
321329
let rewrittenBindings =

0 commit comments

Comments
 (0)