Skip to content

Commit 710b6fa

Browse files
committed
Add native map literal syntax #{...}
1 parent 072059d commit 710b6fa

File tree

15 files changed

+184
-8
lines changed

15 files changed

+184
-8
lines changed

docs/language-choices.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ let x =
1919
- `'a list`
2020
- `'a option`
2121
- `'a map`
22-
- Map values can be built from tuple lists with `Map.ofList`:
23-
- `Map.ofList [("a", 1); ("b", 2)]`
22+
- Map values use native literals:
23+
- `#{ "a" = 1; "b" = 2 }`
24+
- Runtime helpers are also available for map construction/updates (for example `Map.ofList`, `Map.add`).
2425
- Function types use arrow syntax:
2526
- `int -> string`
2627
- `int -> int -> int`

docs/supported-types.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,14 @@ This document specifies the value and type system used by the interpreter.
1818
- Record: structural record types
1919
- Discriminated union: named union types with cases
2020

21+
## Map literals
22+
- Native map literal syntax:
23+
- empty: `#{}`
24+
- populated: `#{ "a" = 1; "b" = 2 }`
25+
- multiline entries are supported in an indented block.
26+
- Keys are `string`.
27+
- Values are inferred and unified to a single value type.
28+
2129
## Function types
2230
- Functions use curried arrow types:
2331
- `t1 -> t2`

docs/syntax-and-indentation.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ This document describes the concrete syntax accepted by the interpreter and the
3838
- literal `{ Name = "a"; Age = 1 }`
3939
- field access `p.Name`
4040
- copy-update `{ p with Age = 2 }`
41+
- Maps:
42+
- empty `#{}`
43+
- literal `#{ "a" = 1; "b" = 2 }`
44+
- multiline:
45+
- `#{`
46+
- ` "a" = 1`
47+
- ` "b" = 2`
48+
- `}`
4149
- Lists:
4250
- `[a; b; c]`
4351
- range `[a..b]`

samples/types-showcase.fss

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,7 @@ type Subscription =
2020

2121

2222
let scores =
23-
Map.empty
24-
|> Map.add "math" 20
25-
|> Map.add "science" 18
23+
#{ "math" = 20; "science" = 18 }
2624

2725

2826
let user = { Id = 1; Name = "Ada"; Tags = ["engineer"; "writer"]; Address = Some { City = "London"; Zip = 12345 }; Scores = scores; Pair = (7, "seven") }
@@ -42,8 +40,10 @@ let format_address (address: { City: string; Zip: int }) = $"{address.City} ({ad
4240

4341
// Name exposure for host capability maps.
4442
let capabilities =
45-
Map.ofList
46-
[(nameof format_address, "address formatting"); (nameof make_office_address, "office address constructor")]
43+
#{
44+
(nameof format_address) = "address formatting"
45+
(nameof make_office_address) = "office address constructor"
46+
}
4747

4848

4949
let city =

src/FScript.Language/Ast.fs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ and Expr =
5353
| ERange of Expr * Expr * Span
5454
| ETuple of Expr list * Span
5555
| ERecord of (string * Expr) list * Span
56+
| EMap of (Expr * Expr) list * Span
5657
| ERecordUpdate of Expr * (string * Expr) list * Span
5758
| EFieldGet of Expr * string * Span
5859
| ECons of Expr * Expr * Span
@@ -107,6 +108,7 @@ module Ast =
107108
| ERange (_, _, s) -> s
108109
| ETuple (_, s) -> s
109110
| ERecord (_, s) -> s
111+
| EMap (_, s) -> s
110112
| ERecordUpdate (_, _, s) -> s
111113
| EFieldGet (_, _, s) -> s
112114
| ECons (_, _, s) -> s

src/FScript.Language/Eval.fs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,17 @@ module Eval =
239239
|> List.map (fun (name, valueExpr) -> name, evalExpr typeDefs env valueExpr)
240240
|> Map.ofList
241241
|> VRecord
242+
| EMap (entries, _) ->
243+
entries
244+
|> List.fold (fun (acc: Map<string, Value>) (keyExpr, valueExpr) ->
245+
match evalExpr typeDefs env keyExpr with
246+
| VString key ->
247+
let value = evalExpr typeDefs env valueExpr
248+
acc.Add(key, value)
249+
| _ ->
250+
// Type checker guarantees string keys for map literals.
251+
raise (EvalException { Message = "Map literal keys must be strings"; Span = Ast.spanOfExpr keyExpr })) (Map.empty<string, Value>)
252+
|> VStringMap
242253
| ERecordUpdate (target, updates, span) ->
243254
match evalExpr typeDefs env target with
244255
| VRecord fields ->

src/FScript.Language/Lexer.fs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,7 @@ module Lexer =
335335
i <- i + 2
336336
col <- col + 2
337337
| '.' -> addToken Dot (mkSpan line col 1) tokens; i <- i + 1; col <- col + 1
338+
| '#' -> addToken Hash (mkSpan line col 1) tokens; i <- i + 1; col <- col + 1
338339
| '{' -> addToken LBrace (mkSpan line col 1) tokens; i <- i + 1; col <- col + 1
339340
| '}' -> addToken RBrace (mkSpan line col 1) tokens; i <- i + 1; col <- col + 1
340341
| _ ->

src/FScript.Language/Parser.fs

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ module Parser =
3939

4040
let private isStartAtom (k: TokenKind) =
4141
match k with
42-
| Ident _ | IntLit _ | FloatLit _ | StringLit _ | InterpString _ | BoolLit _ | LParen | LBracket | LBrace | Let | Fun | If | Raise | For | Match | Typeof | Nameof -> true
42+
| Ident _ | IntLit _ | FloatLit _ | StringLit _ | InterpString _ | BoolLit _ | LParen | LBracket | LBrace | Hash | Let | Fun | If | Raise | For | Match | Typeof | Nameof -> true
4343
| _ -> false
4444

4545
let private isUpperIdent (name: string) =
@@ -529,6 +529,60 @@ module Parser =
529529
parseField()
530530
let rb = stream.Expect(RBrace, "Expected '}' in record literal")
531531
ERecord(fields |> Seq.toList, mkSpanFrom lb.Span rb.Span)
532+
| Hash ->
533+
let hashTok = stream.Next()
534+
let lb = stream.Expect(LBrace, "Expected '{' after '#' for map literal")
535+
let mapStart = mkSpanFrom hashTok.Span lb.Span
536+
stream.SkipNewlines()
537+
let hasIndent = stream.Match(Indent)
538+
539+
let parseMapEntry () =
540+
let key = parsePostfix()
541+
stream.SkipNewlines()
542+
stream.Expect(Equals, "Expected '=' in map entry") |> ignore
543+
let value = parseExpr()
544+
key, value, (Ast.spanOfExpr value).End.Line
545+
546+
stream.SkipNewlines()
547+
if hasIndent && stream.Peek().Kind = Dedent then
548+
stream.Next() |> ignore
549+
550+
if stream.Peek().Kind = RBrace then
551+
let rb = stream.Expect(RBrace, "Expected '}' in map literal")
552+
EMap([], mkSpanFrom mapStart rb.Span)
553+
else
554+
let entries = ResizeArray<Expr * Expr>()
555+
let firstKey, firstValue, firstLine = parseMapEntry()
556+
entries.Add(firstKey, firstValue)
557+
558+
let rec parseTail (lastEntryEndLine: int) =
559+
stream.SkipNewlines()
560+
if hasIndent && stream.Peek().Kind = Dedent then
561+
stream.Next() |> ignore
562+
if stream.Peek().Kind = RBrace then
563+
()
564+
elif stream.Match(Semicolon) then
565+
stream.SkipNewlines()
566+
if hasIndent && stream.Peek().Kind = Dedent then
567+
stream.Next() |> ignore
568+
if stream.Peek().Kind = RBrace then
569+
()
570+
else
571+
let key, value, valueLine = parseMapEntry()
572+
entries.Add(key, value)
573+
parseTail valueLine
574+
else
575+
let next = stream.Peek()
576+
if next.Span.Start.Line > lastEntryEndLine then
577+
let key, value, valueLine = parseMapEntry()
578+
entries.Add(key, value)
579+
parseTail valueLine
580+
else
581+
raise (ParseException { Message = "Expected ';', newline, or '}' in map literal"; Span = next.Span })
582+
583+
parseTail firstLine
584+
let rb = stream.Expect(RBrace, "Expected '}' in map literal")
585+
EMap(entries |> Seq.toList, mkSpanFrom mapStart rb.Span)
532586
| Let ->
533587
parseLetExpr()
534588
| Fun ->

src/FScript.Language/Token.fs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ type TokenKind =
3939
| Colon
4040
| Dot
4141
| RangeDots
42+
| Hash
4243
| LBrace
4344
| RBrace
4445
| Type

src/FScript.Language/TypeInfer.fs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,24 @@ module TypeInfer =
484484
inferred <- inferred.Add(name, applyType sAcc t1)
485485
let tRes = TRecord inferred
486486
sAcc, tRes, asTyped expr tRes
487+
| EMap (entries, span) ->
488+
let valueType = Types.freshVar()
489+
let mutable sAcc = emptySubst
490+
for (keyExpr, valueExpr) in entries do
491+
let envForKey = applyEnv sAcc env
492+
let sKey, tKey, _ = inferExpr typeDefs constructors envForKey keyExpr
493+
let sKeyString = unify typeDefs (applyType sKey tKey) TString span
494+
let sAfterKey = compose sKeyString (compose sKey sAcc)
495+
496+
let envForValue = applyEnv sAfterKey env
497+
let sValue, tValue, _ = inferExpr typeDefs constructors envForValue valueExpr
498+
let expectedValueType = applyType sValue (applyType sAfterKey valueType)
499+
let sValueType = unify typeDefs expectedValueType tValue span
500+
501+
sAcc <- compose sValueType (compose sValue sAfterKey)
502+
503+
let tRes = TStringMap (applyType sAcc valueType)
504+
sAcc, tRes, asTyped expr tRes
487505
| ERecordUpdate (baseExpr, updates, span) ->
488506
let sBase, tBase, _ = inferExpr typeDefs constructors env baseExpr
489507
let baseFields =

0 commit comments

Comments
 (0)