A small experimental ML-like functional language with dependent types, compiled to JavaScript.
ScriptML is a research project and toy compiler, written in TypeScript, that explores:
- ML syntax (inspired by OCaml/F#)
- Inductive types (Nat, List, user ADTs)
- Pattern matching
- Recursion and totality checks
- Dependent function types (Ξ -types)
- IO via embedded thunks that lower to JS functions
- Modules and imports with qualified names and
open
The compiler emits either standalone JavaScript or directly evaluates programs.
git clone https://github.com/yourname/ScriptML
cd ScriptML
npm installRun the CLI:
# Compile and run a program
npx ts-node src/index.ts run demo/arith.sml
# Just compile to JS
npx ts-node src/index.ts js demo/arith.sml > out.js
node out.jsRun the test suite:
npm testScriptML has a typed Ξ»-calculus core with Nats, Lists, user ADTs, IO, and now modules.
zero // the natural number 0
succ zero // 1
succ (succ zero) // 2
"hello" // string literal
["a", "b", "c"] // list literallet id =
fun (x: _) -> x
in
id (succ zero)Functions are curried. Type annotations are optional (: followed by type).
let two = succ (succ zero) in
let three = succ two in
threelet is recursive by default: the name is in scope in its own definition.
if n = zero then "empty"
else "not empty"Common operators are supported: + - * / % = <> < > <= >= ^
(where ^ is string concatenation).
match n with
| zero -> zero
| succ k, acc -> succ (double k)type List a = Nil | Cons of a, List a
let length =
fun (xs: _) ->
match xs with
| Nil -> zero
| Cons of h, t -> succ (length t)
in
length [zero, succ zero, succ (succ zero)]ScriptML supports a lightweight module system.
- A file can begin with
module <Name>(optional; defaults to the filename). - Import another file with
import "path/to/file.sml" as Alias. - Use qualified names like
Math.add. - Use
open <Module>to bring a moduleβs values into scope without qualification.
Example module:
module Math
val add = fun (a: _) -> fun (b: _) -> a + b;
val sub = fun (a: _) -> fun (b: _) -> a - b;Use it:
import "std/math.sml" as Math
println ("3 + 4 = " ^ Math.add 3 4)Or with open:
import "std/math.sml" as Math
open Math
println ("3 - 4 = " ^ sub 3 4)All val and type decls in a module are exported by default.
open is just syntactic sugar for creating local aliases.
IO is modeled with thunks (() => value) and bind / pure:
println "Hello, world!"
bind (promptYesNo "Continue? (y/n)")
(fun (ans: _) ->
if ans = 1 then println "OK" else println "Bye")Builtins provided via __io:
println : String -> IO Unitprint : String -> IO UnitreadLine : IO StringpromptYesNo : String -> IO Nat(1for yes,0for no`)readFileUtf8,writeFileUtf8,readStdinUtf8args : List String
A simplified EBNF for ScriptML:
program ::= [ "module" Ident ] { import | decl } [ term ]
import ::= "import" Str "as" Ident
| "open" Ident
decl ::= "type" Ident { Ident } "=" ctor { "|" ctor }
| "val" Ident "=" term ";"
ctor ::= Ident [ "of" type { "," type } ]
term ::= let | fun | if | match | expr
let ::= "let" Ident "=" term "in" term
fun ::= "fun" "(" Ident [ ":" type ] ")" "->" term
if ::= "if" term "then" term "else" term
match ::= "match" term "with"
{ "|" pattern "->" term }
pattern ::= "zero"
| "succ" Ident "," Ident
| Ident
| Ident "of" Ident { "," Ident }
expr ::= expr atom | atom
atom ::= Ident
| Ident "." Ident // qualified variable
| Int
| Str
| "zero"
| "succ" atom
| "[" [ term { "," term } ] "]"
| "(" term ")"
type ::= type "->" type
| "Nat"
| "Unit"
| Ident
| Ident type
| "(" type ")"
let add =
fun (a: _) ->
fun (b: _) ->
match a with
| zero -> b
| succ k, acc -> succ (add k b)
in
add (succ (succ zero)) (succ zero) // 2 + 1 = 3let rec loop =
fun (lo: _) ->
fun (hi: _) ->
if lo = hi then
println ("Your number is " ^ lo)
else
let mid = lo + ((hi - lo) / 2) in
bind (promptYesNo ("Is your number greater than " ^ mid ^ "? (y/n)"))
(fun (ans: _) ->
if ans = 1
then loop (mid + 1) hi
else loop lo mid)
in
loop 1 100// 21 sticks, each turn take 1β3, computer replies.
let rec play =
fun (n: _) ->
if n = 0 then println "Game over!"
else
bind (println ("Sticks remaining: " ^ n))
(fun (_: _) ->
bind (promptYesNo "Take 1? (y/n)")
(fun (ans: _) ->
let n1 = n - (if ans = 1 then 1 else 2) in
play n1))
in
play 21-
Compiler written in TypeScript, split across:
lexer.tsparser.ts(Pratt parser)elab_emit.ts(elaboration + JS codegen)compiler.ts(top-level driver, import/module loader)
-
Tests in
__tests__, goldens intests/fixtures
MIT