Skip to content

Commit d4bebe7

Browse files
committed
Add List.empty plus Map.map/Map.iter externs
1 parent 64358cc commit d4bebe7

File tree

9 files changed

+84
-1
lines changed

9 files changed

+84
-1
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ FScript currently includes:
2828
- optional type annotations on parameters,
2929
- type declarations: records and unions (including recursive forms),
3030
- interpolation, pipeline operator, `typeof` type tokens, and `nameof` identifier tokens for host workflows.
31+
- unified brace literals for records/maps (`{ Field = value }`, `{ [key] = value }`, `{}` for empty map).
3132

3233
## Quick Start
3334

docs/external-functions.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,10 +58,15 @@ Higher-order externs (for example `List.map`, `Map.fold`, `Option.map`) are impl
5858
- `Map.filter`
5959
- `Map.fold`
6060
- `Map.choose`
61+
- `Map.map`
62+
- `Map.iter`
6163
- `Map.containsKey`
6264
- `Map.remove`
6365

66+
`Map.empty` is an arity-0 value (`'a map`), not an invokable function.
67+
6468
### List
69+
- `List.empty`
6570
- `List.map`
6671
- `List.choose`
6772
- `List.collect`

docs/supported-types.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@ This document specifies the value and type system used by the interpreter.
2323
- empty: `{}`
2424
- populated: `{ ["a"] = 1; ["b"] = 2 }`
2525
- multiline entries are supported in an indented block.
26-
- Keys are bracketed expressions and must have type `string`.
26+
- Keys are bracketed expressions and must have type `string` (for example `{ [keyExpr] = value }`).
27+
- Record literals and map literals share `{ ... }` braces:
28+
- map entries use `[expr] = value`
29+
- record entries use `Field = value`
30+
- `{}` denotes an empty map.
2731
- Values are inferred and unified to a single value type.
2832

2933
## Function types

docs/syntax-and-indentation.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ This document describes the concrete syntax accepted by the interpreter and the
4141
- Maps:
4242
- empty `{}`
4343
- literal `{ ["a"] = 1; ["b"] = 2 }`
44+
- map entries always use bracketed keys (`[expr] = value`)
45+
- record entries use field assignments (`Field = value`)
46+
- when braces are empty (`{}`), the literal is a map
4447
- keys are bracketed expressions (`[expr]`) and must infer to `string`
4548
- multiline:
4649
- `{`

src/FScript.Runtime/ListExterns.fs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@ namespace FScript.Runtime
33
open FScript.Language
44

55
module ListExterns =
6+
let empty : ExternalFunction =
7+
{ Name = "List.empty"
8+
Scheme = Forall([ 0 ], TList (TVar 0))
9+
Arity = 0
10+
Impl = fun _ -> function
11+
| [] -> VList []
12+
| _ -> raise (HostCommon.evalError "List.empty expects no arguments") }
13+
614
let rec private valueEquals a b =
715
match a, b with
816
| VUnit, VUnit -> true

src/FScript.Runtime/MapExterns.fs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,33 @@ module MapExterns =
112112
VStringMap chosen
113113
| _ -> raise (HostCommon.evalError "Map.choose expects (function, map)") }
114114

115+
let map : ExternalFunction =
116+
{ Name = "Map.map"
117+
Scheme = Forall([ 0; 1 ], TFun(TFun(TVar 0, TVar 1), TFun(TStringMap (TVar 0), TStringMap (TVar 1))))
118+
Arity = 2
119+
Impl = fun ctx -> function
120+
| [ mapper; mapValue ] ->
121+
let m = asStringMap mapValue
122+
let mapped =
123+
m
124+
|> Map.map (fun _ value -> ctx.Apply mapper value)
125+
VStringMap mapped
126+
| _ -> raise (HostCommon.evalError "Map.map expects (function, map)") }
127+
128+
let iter : ExternalFunction =
129+
{ Name = "Map.iter"
130+
Scheme = Forall([ 0 ], TFun(TFun(TString, TFun(TVar 0, TUnit)), TFun(TStringMap (TVar 0), TUnit)))
131+
Arity = 2
132+
Impl = fun ctx -> function
133+
| [ iterator; mapValue ] ->
134+
let m = asStringMap mapValue
135+
m
136+
|> Map.iter (fun key value ->
137+
let iteratorWithKey = ctx.Apply iterator (VString key)
138+
ctx.Apply iteratorWithKey value |> ignore)
139+
VUnit
140+
| _ -> raise (HostCommon.evalError "Map.iter expects (function, map)") }
141+
115142
let containsKey : ExternalFunction =
116143
{ Name = "Map.containsKey"
117144
Scheme = Forall([ 0 ], TFun(TString, TFun(TStringMap (TVar 0), TBool)))

src/FScript.Runtime/Registry.fs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,8 +28,11 @@ module Registry =
2828
MapExterns.filter
2929
MapExterns.fold
3030
MapExterns.choose
31+
MapExterns.map
32+
MapExterns.iter
3133
MapExterns.containsKey
3234
MapExterns.remove
35+
ListExterns.empty
3336
ListExterns.map
3437
ListExterns.choose
3538
ListExterns.collect

tests/FScript.Language.Tests/HostExternTests.fs

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,26 @@ type HostExternTests () =
7777
| VOption (Some (VInt 20L)) -> ()
7878
| _ -> Assert.Fail("Expected chosen map value 20 for key b")
7979

80+
[<Test>]
81+
member _.``Map.map maps values and Map.iter applies key-value iterator`` () =
82+
match Helpers.evalWithExterns externs "Map.ofList [(\"a\", 1); (\"b\", 2)] |> Map.map (fun v -> v + 10) |> Map.tryGet \"b\"" with
83+
| VOption (Some (VInt 12L)) -> ()
84+
| _ -> Assert.Fail("Expected mapped map value 12 for key b")
85+
86+
let original = Console.Out
87+
use writer = new StringWriter()
88+
Console.SetOut(writer)
89+
try
90+
let result =
91+
Helpers.evalWithExterns externs
92+
"Map.ofList [(\"a\", 1); (\"b\", 2)] |> Map.iter (fun key -> fun value -> print $\"{key}:{value}\")"
93+
match result with
94+
| VUnit -> ()
95+
| _ -> Assert.Fail("Expected unit")
96+
writer.ToString().Replace("\r\n", "\n").TrimEnd() |> should equal "a:1\nb:2"
97+
finally
98+
Console.SetOut(original)
99+
80100
[<Test>]
81101
member _.``Fs write-side externs operate under root confinement`` () =
82102
let root = Path.Combine(Path.GetTempPath(), "fscript-host-extern-tests", Guid.NewGuid().ToString("N"))
@@ -105,6 +125,15 @@ type HostExternTests () =
105125
| VList [ VInt 1L; VInt 2L; VInt 3L; VInt 4L ] -> ()
106126
| _ -> Assert.Fail("Expected mapped int list")
107127

128+
[<Test>]
129+
member _.``List.empty behaves as a value and cannot be invoked`` () =
130+
match Helpers.evalWithExterns externs "List.empty |> List.length" with
131+
| VInt 0L -> ()
132+
| _ -> Assert.Fail("Expected empty list length 0")
133+
134+
let act () = Helpers.evalWithExterns externs "List.empty ()" |> ignore
135+
act |> should throw typeof<TypeException>
136+
108137
[<Test>]
109138
member _.``List.iter external applies function and returns unit`` () =
110139
let original = Console.Out

tests/FScript.Runtime.Tests/RegistryTests.fs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@ type RegistryTests () =
3535
names.Contains "Map.filter" |> should equal true
3636
names.Contains "Map.fold" |> should equal true
3737
names.Contains "Map.choose" |> should equal true
38+
names.Contains "Map.map" |> should equal true
39+
names.Contains "Map.iter" |> should equal true
3840
names.Contains "Map.remove" |> should equal true
41+
names.Contains "List.empty" |> should equal true
3942
names.Contains "List.map" |> should equal true
4043
names.Contains "List.choose" |> should equal true
4144
names.Contains "List.collect" |> should equal true

0 commit comments

Comments
 (0)