feat(bytecode): add instruction set and expression compiler
This commit is contained in:
1 parent
fa1ba2ecf4
commit
bf580402e6
1 file changed
+240
@@ -0,0 +1,240 @@
|
|||||||
|
module Fml.Core.Bytecode
|
||||||
|
|
||||||
|
open System.Collections.Generic
|
||||||
|
open Fml.Core.Ast
|
||||||
|
open Fml.Core.Value
|
||||||
|
open Fml.Core.Match
|
||||||
|
|
||||||
|
// Stack-machine instruction set. The runtime environment is a de Bruijn stack
|
||||||
|
// (index 0 is the most recent binding); the operand stack holds intermediates.
|
||||||
|
// Closures capture the whole current environment; recursive groups tie the knot
|
||||||
|
// through a shared environment cell.
|
||||||
|
|
||||||
|
type Instr =
|
||||||
|
| Push of Value
|
||||||
|
| LoadLocal of int
|
||||||
|
| LoadGlobal of string
|
||||||
|
| MakeClosure of int
|
||||||
|
| MakeRec of int list
|
||||||
|
| Bind
|
||||||
|
| PopEnv of int
|
||||||
|
| Pop
|
||||||
|
| Call
|
||||||
|
| TailCall
|
||||||
|
| Ret
|
||||||
|
| Jump of int
|
||||||
|
| JumpIfFalse of int
|
||||||
|
| MakeTuple of int
|
||||||
|
| NilList
|
||||||
|
| ConsList
|
||||||
|
| MakeConstruct of string * int
|
||||||
|
| MakeRecord of string list
|
||||||
|
| GetField of string
|
||||||
|
| Proj of int
|
||||||
|
| NegInt
|
||||||
|
| Prim of string
|
||||||
|
| TestInt of int
|
||||||
|
| TestStr of string
|
||||||
|
| TestBool of bool
|
||||||
|
| TestNil
|
||||||
|
| TestCons
|
||||||
|
| TestCtor of string
|
||||||
|
| MatchFail
|
||||||
|
|
||||||
|
let private isTrivialTest (test: SwitchTest) : bool =
|
||||||
|
match test with
|
||||||
|
| SUnit | STuple | SRecord -> true
|
||||||
|
| _ -> false
|
||||||
|
|
||||||
|
let private testInstr (test: SwitchTest) : Instr =
|
||||||
|
match test with
|
||||||
|
| SInt n -> TestInt n
|
||||||
|
| SStr s -> TestStr s
|
||||||
|
| SBool b -> TestBool b
|
||||||
|
| SNil -> TestNil
|
||||||
|
| SCons -> TestCons
|
||||||
|
| SCtor tag -> TestCtor tag
|
||||||
|
| SUnit | STuple | SRecord -> failwith "trivial tests are not emitted"
|
||||||
|
|
||||||
|
// Compiles an expression into a self-contained code block, appending any nested
|
||||||
|
// function bodies to the shared functions table. rootTail marks whether the
|
||||||
|
// block sits in a tail position (top-level phrases do not).
|
||||||
|
let rec compileBlock (functions: ResizeArray<Instr []>) (rootScope: string list) (rootTail: bool) (expr: Expr) : Instr [] =
|
||||||
|
let code = ResizeArray<Instr>()
|
||||||
|
let labels = Dictionary<int, int>()
|
||||||
|
let fixups = ResizeArray<int * int>()
|
||||||
|
let mutable nextLabel = 0
|
||||||
|
let newLabel () =
|
||||||
|
let l = nextLabel
|
||||||
|
nextLabel <- nextLabel + 1
|
||||||
|
l
|
||||||
|
let emit i = code.Add i
|
||||||
|
let emitJump lbl =
|
||||||
|
fixups.Add(code.Count, lbl)
|
||||||
|
code.Add(Jump -1)
|
||||||
|
let emitJumpIfFalse lbl =
|
||||||
|
fixups.Add(code.Count, lbl)
|
||||||
|
code.Add(JumpIfFalse -1)
|
||||||
|
let mark lbl = labels.[lbl] <- code.Count
|
||||||
|
|
||||||
|
let localIndex (scope: string list) (name: string) : int option =
|
||||||
|
scope |> List.tryFindIndex ((=) name)
|
||||||
|
|
||||||
|
let compileFunction (scope: string list) (param: string) (body: Expr) : int =
|
||||||
|
let id = functions.Count
|
||||||
|
functions.Add [||]
|
||||||
|
let block = compileBlock functions (param :: scope) true body
|
||||||
|
functions.[id] <- block
|
||||||
|
id
|
||||||
|
|
||||||
|
let compileRecGroup (scope: string list) (bindings: (string * Expr) list) =
|
||||||
|
let groupScope = (bindings |> List.map fst) @ scope
|
||||||
|
let ids =
|
||||||
|
bindings
|
||||||
|
|> List.map (fun (_, rhs) ->
|
||||||
|
match rhs with
|
||||||
|
| Lambda(param, body) ->
|
||||||
|
let id = functions.Count
|
||||||
|
functions.Add [||]
|
||||||
|
let block = compileBlock functions (param :: groupScope) true body
|
||||||
|
functions.[id] <- block
|
||||||
|
id
|
||||||
|
| _ -> raise (RuntimeError "let rec binding must be a function"))
|
||||||
|
emit (MakeRec ids)
|
||||||
|
|
||||||
|
let rec go (scope: string list) (tail: bool) (e: Expr) =
|
||||||
|
match e with
|
||||||
|
| IntLit n -> emit (Push(VInt n))
|
||||||
|
| BoolLit b -> emit (Push(VBool b))
|
||||||
|
| StringLit s -> emit (Push(VString s))
|
||||||
|
| UnitLit -> emit (Push VUnit)
|
||||||
|
| Var x ->
|
||||||
|
match localIndex scope x with
|
||||||
|
| Some idx -> emit (LoadLocal idx)
|
||||||
|
| None -> emit (LoadGlobal x)
|
||||||
|
| Lambda(param, body) ->
|
||||||
|
let id = compileFunction scope param body
|
||||||
|
emit (MakeClosure id)
|
||||||
|
| App(fn, arg) ->
|
||||||
|
go scope false fn
|
||||||
|
go scope false arg
|
||||||
|
emit (if tail then TailCall else Call)
|
||||||
|
| Let(false, name, rhs, body) ->
|
||||||
|
go scope false rhs
|
||||||
|
emit Bind
|
||||||
|
go (name :: scope) tail body
|
||||||
|
if not tail then emit (PopEnv 1)
|
||||||
|
| Let(true, name, rhs, body) ->
|
||||||
|
compileRecGroup scope [ name, rhs ]
|
||||||
|
go (name :: scope) tail body
|
||||||
|
if not tail then emit (PopEnv 1)
|
||||||
|
| LetGroup(true, bindings, body) ->
|
||||||
|
compileRecGroup scope bindings
|
||||||
|
let scope' = (bindings |> List.map fst) @ scope
|
||||||
|
go scope' tail body
|
||||||
|
if not tail then emit (PopEnv(List.length bindings))
|
||||||
|
| LetGroup(false, bindings, body) ->
|
||||||
|
for (_, rhs) in bindings do
|
||||||
|
go scope false rhs
|
||||||
|
for _ in bindings do
|
||||||
|
emit Bind
|
||||||
|
let scope' = (bindings |> List.map fst) @ scope
|
||||||
|
go scope' tail body
|
||||||
|
if not tail then emit (PopEnv(List.length bindings))
|
||||||
|
| If(cond, thenBranch, elseBranch) ->
|
||||||
|
go scope false cond
|
||||||
|
let lElse = newLabel ()
|
||||||
|
let lEnd = newLabel ()
|
||||||
|
emitJumpIfFalse lElse
|
||||||
|
go scope tail thenBranch
|
||||||
|
emitJump lEnd
|
||||||
|
mark lElse
|
||||||
|
go scope tail elseBranch
|
||||||
|
mark lEnd
|
||||||
|
| Neg inner ->
|
||||||
|
go scope false inner
|
||||||
|
emit NegInt
|
||||||
|
| BinOp("&&", a, b) -> go scope tail (If(a, b, BoolLit false))
|
||||||
|
| BinOp("||", a, b) -> go scope tail (If(a, BoolLit true, b))
|
||||||
|
| BinOp(op, a, b) ->
|
||||||
|
go scope false a
|
||||||
|
go scope false b
|
||||||
|
emit (Prim op)
|
||||||
|
| Tuple items ->
|
||||||
|
for item in items do
|
||||||
|
go scope false item
|
||||||
|
emit (MakeTuple(List.length items))
|
||||||
|
| Nil -> emit NilList
|
||||||
|
| Cons(headExpr, tailExpr) ->
|
||||||
|
go scope false headExpr
|
||||||
|
go scope false tailExpr
|
||||||
|
emit ConsList
|
||||||
|
| Construct(name, args) ->
|
||||||
|
for arg in args do
|
||||||
|
go scope false arg
|
||||||
|
emit (MakeConstruct(name, List.length args))
|
||||||
|
| Record fields ->
|
||||||
|
for (_, fieldExpr) in fields do
|
||||||
|
go scope false fieldExpr
|
||||||
|
emit (MakeRecord(fields |> List.map fst))
|
||||||
|
| Field(record, fieldName) ->
|
||||||
|
go scope false record
|
||||||
|
emit (GetField fieldName)
|
||||||
|
| Match(scrut, cases) -> compileMatchExpr scope tail scrut cases
|
||||||
|
|
||||||
|
and compileMatchExpr (scope: string list) (tail: bool) (scrut: Expr) (cases: (Pattern * Expr) list) =
|
||||||
|
go scope false scrut
|
||||||
|
emit Bind
|
||||||
|
let scrutScope = "$scrut" :: scope
|
||||||
|
let tree, bindingsByRow = compileMatch (cases |> List.map fst)
|
||||||
|
let bodies = cases |> List.map snd |> Array.ofList
|
||||||
|
let lJoin = newLabel ()
|
||||||
|
let loadScrut (currentScope: string list) (path: Path) =
|
||||||
|
let idx =
|
||||||
|
match localIndex currentScope "$scrut" with
|
||||||
|
| Some i -> i
|
||||||
|
| None -> raise (RuntimeError "lost the match scrutinee")
|
||||||
|
emit (LoadLocal idx)
|
||||||
|
for step in path do
|
||||||
|
emit (Proj step)
|
||||||
|
let rec lower (tree: DecisionTree) =
|
||||||
|
match tree with
|
||||||
|
| Leaf row ->
|
||||||
|
let binds = bindingsByRow.[row]
|
||||||
|
let mutable curScope = scrutScope
|
||||||
|
for (varName, path) in binds do
|
||||||
|
loadScrut curScope path
|
||||||
|
emit Bind
|
||||||
|
curScope <- varName :: curScope
|
||||||
|
go curScope tail bodies.[row]
|
||||||
|
if not tail then emit (PopEnv(List.length binds))
|
||||||
|
emitJump lJoin
|
||||||
|
| FailNode -> emit MatchFail
|
||||||
|
| SwitchNode(path, casesList, defaultTree) ->
|
||||||
|
match casesList, defaultTree with
|
||||||
|
| [ (test, sub) ], None when isTrivialTest test -> lower sub
|
||||||
|
| _ ->
|
||||||
|
for (test, sub) in casesList do
|
||||||
|
let lNext = newLabel ()
|
||||||
|
loadScrut scrutScope path
|
||||||
|
emit (testInstr test)
|
||||||
|
emitJumpIfFalse lNext
|
||||||
|
lower sub
|
||||||
|
mark lNext
|
||||||
|
match defaultTree with
|
||||||
|
| Some dt -> lower dt
|
||||||
|
| None -> emit MatchFail
|
||||||
|
lower tree
|
||||||
|
mark lJoin
|
||||||
|
if not tail then emit (PopEnv 1)
|
||||||
|
|
||||||
|
go rootScope rootTail expr
|
||||||
|
emit Ret
|
||||||
|
for (idx, lbl) in fixups do
|
||||||
|
let target = labels.[lbl]
|
||||||
|
code.[idx] <-
|
||||||
|
match code.[idx] with
|
||||||
|
| Jump _ -> Jump target
|
||||||
|
| JumpIfFalse _ -> JumpIfFalse target
|
||||||
|
| other -> other
|
||||||
|
code.ToArray()
|
||||||
Reference in new issue
Block a user