Files
olisp/lib/compiler/emit.ml
T
haxala1r 947d2274bb
ci/woodpecker/push/nix Pipeline was successful
ci/woodpecker/push/fedora Pipeline was successful
ci/woodpecker/push/debian Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful
vm: modified StoreLocal and StoreGlobal logic to be more consistent with the rest of the VM,
and modified the emit module to emit a Pop instruction after every top-level expression.

This change was required because the semantics of the language are pretty clear.
Every expression evaluates to something - meaning that, in the corresponding bytecode,
every expression must have exactly a +1 effect on the data stack. I.e. every expression,
when its corresponding bytecode is evaluated, has the effect of pushing something
to the stack. For values that are not used by another expression, this value
must be immediately popped.

Some optimizations could target this area. For example, for top-level expressions,
it is obvious to the compiler that their values will not be used - hence the compiler
can use optimized versions of some instructions (like StoreLocal and StoreGlobal)
to simply never leave the value on the stack, thus saving an extra Pop instruction
(good for performance and code size).

Same thing applies in function bodies, letrec/let/begin bodies, where expressions
whose values are never used may appear.

It may also make sense to introduce registers to the VM, for the purposes of
parameter passing (such that up to a predetermined number of parameters are
progressively passed through registers instead of pushed to the stack).
This would pair well with eliminating unnecessary currying in the byte code.
2026-04-26 01:20:05 +03:00

149 lines
4.8 KiB
OCaml

type literal = Core_ast.literal
type expression = Scope_analysis.expression
module SymbolTable = Scope_analysis.SymbolTable
type instr = Vm.Types.instr
type pre_instr =
| Instr of instr
| BackPatchMkClosure
| BackPatchJumpF
type program = {
instrs : pre_instr Dynarray.t;
constants : Vm.Types.value Dynarray.t;
sym_table : int SymbolTable.t;
(* This array holds the lambda bodies that we have to compiler later, and
the index we have to patch the address back into.
*)
backpatch : (int * expression) Queue.t;
}
let ( let* ) = Result.bind
let current_index p =
Dynarray.length p.instrs
let set_instr p i ins =
Dynarray.set p.instrs i (Instr ins)
let emit_mkclosure p =
Ok (Dynarray.add_last p.instrs BackPatchMkClosure)
let emit_jumpf p =
Ok (Dynarray.add_last p.instrs BackPatchJumpF)
let emit_instr p i =
Ok (Dynarray.add_last p.instrs (Instr i))
let emit_constant p c =
Dynarray.add_last p.constants c;
emit_instr p (Constant ((Dynarray.length p.constants) - 1))
(* evaluating an expression ALWAYS has the effect of pushing exactly
one element to the stack. For top-level items, this element is
silently popped.
*)
let rec compile_one p = function
| Scope_analysis.Literal (Int x) -> emit_constant p (Vm.Types.Int x)
| Literal Nil -> emit_constant p (Vm.Types.Nil)
| Literal (Double x) -> emit_constant p (Vm.Types.Double x)
| Literal (String s) -> emit_constant p (Vm.Types.String s)
| Literal (Symbol s) -> emit_constant p (Vm.Types.Symbol s)
| Literal (Cons (a, b)) ->
let* _ = compile_one p (Literal a) in
let* _ = compile_one p (Literal b) in
emit_instr p (Vm.Types.MakeCons)
| Var (Scope_analysis.Local i) ->
emit_instr p (Vm.Types.LoadLocal i)
| Var (Global i) ->
emit_instr p (Vm.Types.LoadGlobal i)
| Set (Local i, expr) ->
let* _ = compile_one p expr in
emit_instr p (Vm.Types.StoreLocal i)
| Set (Global i, expr) ->
let* _ = compile_one p expr in
emit_instr p (Vm.Types.StoreGlobal i)
| Apply (f, arg) ->
let* _ = compile_one p f in
let* _ = compile_one p arg in
emit_instr p Vm.Types.Apply
| Lambda body ->
let* _ = emit_mkclosure p in
Ok (Queue.push ((Dynarray.length p.instrs) - 1, body) p.backpatch)
| If (test, t, f) ->
(* *)
let* _ = compile_one p test in (* compile the expression to be tested *)
let jumpf_index = current_index p in
let* _ = emit_jumpf p in (* jump if false, to the false branch*)
let* _ = compile_one p t in (* true branch *)
let jump_index = current_index p in
let* _ = emit_jumpf p in (* jump unconditionally to the common point*)
let false_index = current_index p in
let* _ = compile_one p f in (* false branch *)
let reunite_index = current_index p in
let* _ = emit_instr p NOOP in
(* Now we can immediately backpatch the dummy instructions we put in place *)
set_instr p jumpf_index (JumpF false_index);
set_instr p jump_index (Jump reunite_index);
Ok ()
| Begin [] ->
Error "Cannot compile empty begin "
| Begin (e1 :: []) ->
compile_one p e1
| Begin (e1 :: e2 :: rest) ->
let* _ = compile_one p e1 in
let* _ = emit_instr p Vm.Types.Pop in
compile_one p (Begin (e2 :: rest))
and compile_all p exprs =
Util.traverse
(fun e ->
let* _ = compile_one p e in
emit_instr p Pop) exprs
(* Once we have compiled the top-level expressions, we must now compile
all of the lambdas we held off on. Some of these will hold more
lambdas - that should be fine, they'll just get added to the end
of the backpatch queue.
*)
let backpatch_one p (i, b) =
Dynarray.set p.instrs i (Instr (MakeClosure (current_index p)));
let* _ = compile_one p b in
emit_instr p End
let rec backpatch p =
if Queue.is_empty p.backpatch then
Ok ()
else
(let* _ = backpatch_one p (Queue.pop p.backpatch) in
backpatch p)
let print_instr = function
| Instr i -> Vm.Types.print_one i
| BackPatchJumpF -> "BACKPATCH JUMPF\n"
| BackPatchMkClosure -> "BACKPATCH CLOSURE\n"
let print_instrs =
Array.mapi_inplace (fun i ins ->
print_endline (Printf.sprintf "%d: %s" i (print_instr ins)); ins)
let smooth_one = function
| Instr i -> i
| _ -> failwith "backpatching process was not complete!"
let smooth_instrs p =
Dynarray.to_array (Dynarray.map smooth_one p.instrs)
let compile (exprs : expression list) (tbl : int SymbolTable.t) =
let program = {
instrs=Dynarray.create ();
constants=Dynarray.create ();
sym_table=tbl;
backpatch=Queue.create ();
} in
let* _ = compile_all program exprs in
let* _ = emit_instr program End in
let* _ = backpatch program in
let final_instrs = smooth_instrs program in
Ok (Vm.make_vm final_instrs (Dynarray.to_array program.constants) ((SymbolTable.cardinal tbl) + 1))
let compile_src src =
let* (exprs, tbl) = Scope_analysis.of_src src in
compile exprs tbl