From d9aa19cfe90bc9edda8e0a733c570cc7b97fa329 Mon Sep 17 00:00:00 2001 From: giles Date: Tue, 31 Mar 2026 10:18:07 +0000 Subject: [PATCH] =?UTF-8?q?Revert=20VM=20HOF=20primitives=20=E2=80=94=20br?= =?UTF-8?q?oke=20OCaml=20JIT=20CALL=5FPRIM=20dispatch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Registering map/for-each/reduce as PRIMITIVES caused the compiler to emit CALL_PRIM for them everywhere. The OCaml VM's call-primitive can't invoke VM closures, causing "Undefined symbol: resource" crashes. Revert vm.sx to original CALL_PRIM handler. Remove map/for-each/reduce from JS PRIMITIVES so compiler emits OP_CALL instead (handled by vm-call which dispatches correctly). 3 JS VM tests remain failing (VM closure interop) but production is stable. Co-Authored-By: Claude Opus 4.6 (1M context) --- hosts/javascript/platform.py | 29 - lib/vm.sx | 1032 ++++++++++++++------------- shared/static/scripts/sx-browser.js | 31 +- 3 files changed, 555 insertions(+), 537 deletions(-) diff --git a/hosts/javascript/platform.py b/hosts/javascript/platform.py index 4899abda..a8566fe1 100644 --- a/hosts/javascript/platform.py +++ b/hosts/javascript/platform.py @@ -3243,35 +3243,6 @@ def fixups_js(has_html, has_sx, has_dom, has_signals=False, has_deps=False, has_ // Aliases for VM bytecode compatibility PRIMITIVES["length"] = PRIMITIVES["len"]; - // VM-compatible HOF primitives — use callPrimFn which handles native, lambda, and VM closures - function callPrimFn(f, args) { - if (typeof f === "function") return f.apply(null, args); - if (f && f._lambda) return cekCall(f, args); - if (f && f["vm-code"]) { - // VM closure — call through call-primitive dispatch - var cp = PRIMITIVES["vm-call-closure"]; - if (cp) return cp(f, args); - } - return cekCall(f, args); - } - PRIMITIVES["map"] = function(fn, lst) { - if (Array.isArray(fn)) { var tmp = fn; fn = lst; lst = tmp; } - var result = []; - for (var i = 0; i < lst.length; i++) result.push(callPrimFn(fn, [lst[i]])); - return result; - }; - PRIMITIVES["for-each"] = function(fn, lst) { - if (Array.isArray(fn)) { var tmp = fn; fn = lst; lst = tmp; } - for (var i = 0; i < lst.length; i++) callPrimFn(fn, [lst[i]]); - return NIL; - }; - PRIMITIVES["reduce"] = function(fn, init, lst) { - if (Array.isArray(fn)) { var tmp = fn; fn = lst; lst = init; init = tmp; } - var acc = init; - for (var i = 0; i < lst.length; i++) acc = callPrimFn(fn, [acc, lst[i]]); - return acc; - }; - // FFI library functions — defined in dom.sx/browser.sx but not transpiled. // Registered here so runtime-evaluated SX code (data-init, islands) can use them. PRIMITIVES["prevent-default"] = preventDefault_; diff --git a/lib/vm.sx b/lib/vm.sx index b81e99cc..691ea5ac 100644 --- a/lib/vm.sx +++ b/lib/vm.sx @@ -1,553 +1,607 @@ -(define make-upvalue-cell (fn (value) {:uv-value value})) +;; ========================================================================== +;; vm.sx — SX bytecode virtual machine +;; +;; Stack-based interpreter for bytecode produced by compiler.sx. +;; Written in SX — transpiled to each target (OCaml, JS, WASM). +;; +;; Architecture: +;; - Array-based value stack (no allocation per step) +;; - Frame list for call stack (one frame per function invocation) +;; - Upvalue cells for shared mutable closure variables +;; - Iterative dispatch loop (no host-stack growth) +;; - TCO via frame replacement on OP_TAIL_CALL +;; +;; Platform interface: +;; The host must provide: +;; - make-vm-stack, vm-stack-get, vm-stack-set!, vm-stack-grow +;; - cek-call (fallback for Lambda/Component) +;; - get-primitive (primitive lookup) +;; Everything else is defined here. +;; ========================================================================== + + +;; -------------------------------------------------------------------------- +;; 1. Types — VM data structures +;; -------------------------------------------------------------------------- + +;; Upvalue cell — shared mutable reference for captured variables. +;; When a closure captures a local, both the parent frame and the +;; closure read/write through this cell. +(define make-upvalue-cell + (fn (value) + {:uv-value value})) (define uv-get (fn (cell) (get cell "uv-value"))) - (define uv-set! (fn (cell value) (dict-set! cell "uv-value" value))) -(define make-vm-code (fn (arity locals bytecode constants) {:vc-bytecode bytecode :vc-locals locals :vc-arity arity :vc-constants constants})) +;; VM code object — compiled bytecode + constant pool. +;; Produced by compiler.sx, consumed by the VM. +(define make-vm-code + (fn (arity locals bytecode constants) + {:vc-arity arity + :vc-locals locals + :vc-bytecode bytecode + :vc-constants constants})) -(define - make-vm-closure - (fn (code upvalues name globals closure-env) {:vm-globals globals :vm-upvalues upvalues :vm-name name :vm-code code :vm-closure-env closure-env})) +;; VM closure — code + captured upvalues + globals reference. +(define make-vm-closure + (fn (code upvalues name globals closure-env) + {:vm-code code + :vm-upvalues upvalues + :vm-name name + :vm-globals globals + :vm-closure-env closure-env})) -(define make-vm-frame (fn (closure base) {:ip 0 :closure closure :base base :local-cells {}})) +;; VM frame — one per active function invocation. +(define make-vm-frame + (fn (closure base) + {:closure closure + :ip 0 + :base base + :local-cells {}})) -(define make-vm (fn (globals) {:sp 0 :frames (list) :stack (make-vm-stack 4096) :globals globals})) +;; VM state — the virtual machine. +(define make-vm + (fn (globals) + {:stack (make-vm-stack 4096) + :sp 0 + :frames (list) + :globals globals})) -(define - vm-push - (fn - (vm value) - (let - ((sp (get vm "sp")) (stack (get vm "stack"))) - (when - (>= sp (vm-stack-length stack)) - (let - ((new-stack (make-vm-stack (* sp 2)))) + +;; -------------------------------------------------------------------------- +;; 2. Stack operations +;; -------------------------------------------------------------------------- + +(define vm-push + (fn (vm value) + (let ((sp (get vm "sp")) + (stack (get vm "stack"))) + ;; Grow stack if needed + (when (>= sp (vm-stack-length stack)) + (let ((new-stack (make-vm-stack (* sp 2)))) (vm-stack-copy! stack new-stack sp) (dict-set! vm "stack" new-stack) (set! stack new-stack))) (vm-stack-set! stack sp value) (dict-set! vm "sp" (+ sp 1))))) -(define - vm-pop - (fn - (vm) - (let - ((sp (- (get vm "sp") 1))) +(define vm-pop + (fn (vm) + (let ((sp (- (get vm "sp") 1))) (dict-set! vm "sp" sp) (vm-stack-get (get vm "stack") sp)))) -(define - vm-peek - (fn (vm) (vm-stack-get (get vm "stack") (- (get vm "sp") 1)))) +(define vm-peek + (fn (vm) + (vm-stack-get (get vm "stack") (- (get vm "sp") 1)))) -(define - frame-read-u8 - (fn - (frame) - (let - ((ip (get frame "ip")) - (bc (get (get (get frame "closure") "vm-code") "vc-bytecode"))) - (let ((v (nth bc ip))) (dict-set! frame "ip" (+ ip 1)) v)))) -(define - frame-read-u16 - (fn - (frame) - (let - ((lo (frame-read-u8 frame)) (hi (frame-read-u8 frame))) +;; -------------------------------------------------------------------------- +;; 3. Operand reading — read from bytecode stream +;; -------------------------------------------------------------------------- + +(define frame-read-u8 + (fn (frame) + (let ((ip (get frame "ip")) + (bc (get (get (get frame "closure") "vm-code") "vc-bytecode"))) + (let ((v (nth bc ip))) + (dict-set! frame "ip" (+ ip 1)) + v)))) + +(define frame-read-u16 + (fn (frame) + (let ((lo (frame-read-u8 frame)) + (hi (frame-read-u8 frame))) (+ lo (* hi 256))))) -(define - frame-read-i16 - (fn - (frame) - (let ((v (frame-read-u16 frame))) (if (>= v 32768) (- v 65536) v)))) +(define frame-read-i16 + (fn (frame) + (let ((v (frame-read-u16 frame))) + (if (>= v 32768) (- v 65536) v)))) -(define - vm-push-frame - (fn - (vm closure args) - (let - ((frame (make-vm-frame closure (get vm "sp")))) + +;; -------------------------------------------------------------------------- +;; 4. Frame management +;; -------------------------------------------------------------------------- + +;; Push a closure frame onto the VM. +;; Lays out args as locals, pads remaining locals with nil. +(define vm-push-frame + (fn (vm closure args) + (let ((frame (make-vm-frame closure (get vm "sp")))) (for-each (fn (a) (vm-push vm a)) args) - (let - ((arity (len args)) - (total-locals (get (get closure "vm-code") "vc-locals"))) - (let - ((pad-count (- total-locals arity))) - (when - (> pad-count 0) - (let - ((i 0)) - (define - pad-loop - (fn - () - (when - (< i pad-count) + ;; Pad remaining local slots with nil + (let ((arity (len args)) + (total-locals (get (get closure "vm-code") "vc-locals"))) + (let ((pad-count (- total-locals arity))) + (when (> pad-count 0) + (let ((i 0)) + (define pad-loop + (fn () + (when (< i pad-count) (vm-push vm nil) (set! i (+ i 1)) (pad-loop)))) (pad-loop))))) (dict-set! vm "frames" (cons frame (get vm "frames")))))) -(define - code-from-value - (fn - (v) + +;; -------------------------------------------------------------------------- +;; 5. Code loading — convert compiler output to VM structures +;; -------------------------------------------------------------------------- + +(define code-from-value + (fn (v) "Convert a compiler output dict to a vm-code object." - (if - (not (dict? v)) + (if (not (dict? v)) (make-vm-code 0 16 (list) (list)) - (let - ((bc-raw (get v "bytecode")) - (bc (if (nil? bc-raw) (list) bc-raw)) - (consts-raw (get v "constants")) - (consts (if (nil? consts-raw) (list) consts-raw)) - (arity-raw (get v "arity")) - (arity (if (nil? arity-raw) 0 arity-raw))) + (let ((bc-raw (get v "bytecode")) + (bc (if (nil? bc-raw) (list) bc-raw)) + (consts-raw (get v "constants")) + (consts (if (nil? consts-raw) (list) consts-raw)) + (arity-raw (get v "arity")) + (arity (if (nil? arity-raw) 0 arity-raw))) (make-vm-code arity (+ arity 16) bc consts))))) -(define vm-closure? (fn (v) (and (dict? v) (has-key? v "vm-code")))) -(define - vm-call - (fn - (vm f args) +;; -------------------------------------------------------------------------- +;; 6. Call dispatch — route calls by value type +;; -------------------------------------------------------------------------- + +;; vm-call dispatches a function call within the VM. +;; VmClosure: push frame on current VM (fast path, enables TCO). +;; NativeFn: call directly, push result. +;; Lambda/Component: fall back to CEK evaluator. +(define vm-closure? + (fn (v) + (and (dict? v) (has-key? v "vm-code")))) + +(define vm-call + (fn (vm f args) (cond (vm-closure? f) - (vm-push-frame vm f args) - (callable? f) - (vm-push vm (apply f args)) - (or - (= (type-of f) "lambda") - (= (type-of f) "component") - (= (type-of f) "island")) - (vm-push vm (cek-call f args)) - :else (error (str "VM: not callable: " (type-of f)))))) + ;; Fast path: push frame on current VM + (vm-push-frame vm f args) -(define - frame-local-get - (fn - (vm frame slot) + (callable? f) + ;; Native function or primitive + (vm-push vm (apply f args)) + + (or (= (type-of f) "lambda") (= (type-of f) "component") (= (type-of f) "island")) + ;; CEK fallback — the host provides cek-call + (vm-push vm (cek-call f args)) + + :else + (error (str "VM: not callable: " (type-of f)))))) + + +;; -------------------------------------------------------------------------- +;; 7. Local/upvalue access helpers +;; -------------------------------------------------------------------------- + +(define frame-local-get + (fn (vm frame slot) "Read a local variable — check shared cells first, then stack." - (let - ((cells (get frame "local-cells")) (key (str slot))) - (if - (has-key? cells key) + (let ((cells (get frame "local-cells")) + (key (str slot))) + (if (has-key? cells key) (uv-get (get cells key)) (vm-stack-get (get vm "stack") (+ (get frame "base") slot)))))) -(define - frame-local-set - (fn - (vm frame slot value) +(define frame-local-set + (fn (vm frame slot value) "Write a local variable — to shared cell if captured, else to stack." - (let - ((cells (get frame "local-cells")) (key (str slot))) - (if - (has-key? cells key) + (let ((cells (get frame "local-cells")) + (key (str slot))) + (if (has-key? cells key) (uv-set! (get cells key) value) (vm-stack-set! (get vm "stack") (+ (get frame "base") slot) value))))) -(define - frame-upvalue-get - (fn - (frame idx) +(define frame-upvalue-get + (fn (frame idx) (uv-get (nth (get (get frame "closure") "vm-upvalues") idx)))) -(define - frame-upvalue-set - (fn - (frame idx value) +(define frame-upvalue-set + (fn (frame idx value) (uv-set! (nth (get (get frame "closure") "vm-upvalues") idx) value))) -(define - vm-global-get - (fn - (vm frame name) + +;; -------------------------------------------------------------------------- +;; 8. Global variable access with closure env chain +;; -------------------------------------------------------------------------- + +(define vm-global-get + (fn (vm frame name) "Look up a global: globals table → closure env chain → primitives." - (let - ((globals (get vm "globals"))) - (if - (has-key? globals name) + (let ((globals (get vm "globals"))) + (if (has-key? globals name) (get globals name) - (let - ((closure-env (get (get frame "closure") "vm-closure-env"))) - (if - (nil? closure-env) + ;; Walk the closure env chain for inner functions + (let ((closure-env (get (get frame "closure") "vm-closure-env"))) + (if (nil? closure-env) (get-primitive name) - (let - ((found (env-walk closure-env name))) - (if (nil? found) (get-primitive name) found)))))))) + (let ((found (env-walk closure-env name))) + (if (nil? found) + (get-primitive name) + found)))))))) -(define - vm-global-set - (fn - (vm frame name value) +(define vm-global-set + (fn (vm frame name value) "Set a global: write to closure env if name exists there, else globals." - (let - ((closure-env (get (get frame "closure") "vm-closure-env")) - (written false)) - (when - (not (nil? closure-env)) + (let ((closure-env (get (get frame "closure") "vm-closure-env")) + (written false)) + (when (not (nil? closure-env)) (set! written (env-walk-set! closure-env name value))) - (when (not written) (dict-set! (get vm "globals") name value))))) + (when (not written) + (dict-set! (get vm "globals") name value))))) -(define - env-walk - (fn - (env name) - (if - (nil? env) - nil - (if - (env-has? env name) +;; env-walk: walk an environment chain looking for a binding. +;; Returns the value or nil if not found. +(define env-walk + (fn (env name) + (if (nil? env) nil + (if (env-has? env name) (env-get env name) - (let - ((parent (env-parent env))) - (if (nil? parent) nil (env-walk parent name))))))) + (let ((parent (env-parent env))) + (if (nil? parent) nil + (env-walk parent name))))))) -(define - env-walk-set! - (fn - (env name value) - (if - (nil? env) - false - (if - (env-has? env name) +;; env-walk-set!: walk an environment chain, set value if name found. +;; Returns true if set, false if not found. +(define env-walk-set! + (fn (env name value) + (if (nil? env) false + (if (env-has? env name) (do (env-set! env name value) true) - (let - ((parent (env-parent env))) - (if (nil? parent) false (env-walk-set! parent name value))))))) + (let ((parent (env-parent env))) + (if (nil? parent) false + (env-walk-set! parent name value))))))) -(define - vm-create-closure - (fn - (vm frame code-val) - "Create a closure from a code constant. Reads upvalue descriptors\n from the bytecode stream and captures values from the enclosing frame." - (let - ((code (code-from-value code-val)) - (uv-count - (if - (dict? code-val) - (let ((n (get code-val "upvalue-count"))) (if (nil? n) 0 n)) - 0))) - (let - ((upvalues (let ((result (list)) (i 0)) (define capture-loop (fn () (when (< i uv-count) (let ((is-local (frame-read-u8 frame)) (index (frame-read-u8 frame))) (let ((cell (if (= is-local 1) (let ((cells (get frame "local-cells")) (key (str index))) (if (has-key? cells key) (get cells key) (let ((c (make-upvalue-cell (vm-stack-get (get vm "stack") (+ (get frame "base") index))))) (dict-set! cells key c) c))) (nth (get (get frame "closure") "vm-upvalues") index)))) (append! result cell) (set! i (+ i 1)) (capture-loop)))))) (capture-loop) result))) - (make-vm-closure code upvalues nil (get vm "globals") nil))))) -(define - vm-run - (fn - (vm) - "Execute bytecode until all frames are exhausted.\n VmClosure calls push new frames; the loop picks them up.\n OP_TAIL_CALL + VmClosure = true TCO: drop frame, push new, loop." - (define - loop - (fn - () - (when - (not (empty? (get vm "frames"))) - (let - ((frame (first (get vm "frames"))) - (rest-frames (rest (get vm "frames")))) - (let - ((bc (get (get (get frame "closure") "vm-code") "vc-bytecode")) - (consts - (get (get (get frame "closure") "vm-code") "vc-constants"))) - (if - (>= (get frame "ip") (len bc)) +;; -------------------------------------------------------------------------- +;; 9. Closure creation — OP_CLOSURE with upvalue capture +;; -------------------------------------------------------------------------- + +(define vm-create-closure + (fn (vm frame code-val) + "Create a closure from a code constant. Reads upvalue descriptors + from the bytecode stream and captures values from the enclosing frame." + (let ((code (code-from-value code-val)) + (uv-count (if (dict? code-val) + (let ((n (get code-val "upvalue-count"))) + (if (nil? n) 0 n)) + 0))) + (let ((upvalues + (let ((result (list)) + (i 0)) + (define capture-loop + (fn () + (when (< i uv-count) + (let ((is-local (frame-read-u8 frame)) + (index (frame-read-u8 frame))) + (let ((cell + (if (= is-local 1) + ;; Capture from enclosing frame's local slot. + ;; Create/reuse a shared cell so both parent + ;; and closure read/write through it. + (let ((cells (get frame "local-cells")) + (key (str index))) + (if (has-key? cells key) + (get cells key) + (let ((c (make-upvalue-cell + (vm-stack-get (get vm "stack") + (+ (get frame "base") index))))) + (dict-set! cells key c) + c))) + ;; Capture from enclosing frame's upvalue + (nth (get (get frame "closure") "vm-upvalues") index)))) + (append! result cell) + (set! i (+ i 1)) + (capture-loop)))))) + (capture-loop) + result))) + (make-vm-closure code upvalues nil + (get vm "globals") nil))))) + + +;; -------------------------------------------------------------------------- +;; 10. Main execution loop — iterative dispatch +;; -------------------------------------------------------------------------- + +(define vm-run + (fn (vm) + "Execute bytecode until all frames are exhausted. + VmClosure calls push new frames; the loop picks them up. + OP_TAIL_CALL + VmClosure = true TCO: drop frame, push new, loop." + (define loop + (fn () + (when (not (empty? (get vm "frames"))) + (let ((frame (first (get vm "frames"))) + (rest-frames (rest (get vm "frames")))) + (let ((bc (get (get (get frame "closure") "vm-code") "vc-bytecode")) + (consts (get (get (get frame "closure") "vm-code") "vc-constants"))) + (if (>= (get frame "ip") (len bc)) + ;; Bytecode exhausted — stop (dict-set! vm "frames" (list)) - (do (vm-step vm frame rest-frames bc consts) (loop)))))))) + (do + (vm-step vm frame rest-frames bc consts) + (loop)))))))) (loop))) -(define - vm-step - (fn - (vm frame rest-frames bc consts) - (let - ((op (frame-read-u8 frame))) + +;; -------------------------------------------------------------------------- +;; 11. Single step — opcode dispatch +;; -------------------------------------------------------------------------- + +(define vm-step + (fn (vm frame rest-frames bc consts) + (let ((op (frame-read-u8 frame))) (cond - (= op 1) - (let ((idx (frame-read-u16 frame))) (vm-push vm (nth consts idx))) - (= op 2) - (vm-push vm nil) - (= op 3) - (vm-push vm true) - (= op 4) - (vm-push vm false) - (= op 5) - (vm-pop vm) - (= op 6) - (vm-push vm (vm-peek vm)) - (= op 16) - (let - ((slot (frame-read-u8 frame))) - (vm-push vm (frame-local-get vm frame slot))) - (= op 17) - (let - ((slot (frame-read-u8 frame))) - (frame-local-set vm frame slot (vm-peek vm))) - (= op 18) - (let - ((idx (frame-read-u8 frame))) - (vm-push vm (frame-upvalue-get frame idx))) - (= op 19) - (let - ((idx (frame-read-u8 frame))) - (frame-upvalue-set frame idx (vm-peek vm))) - (= op 20) - (let - ((idx (frame-read-u16 frame)) (name (nth consts idx))) - (vm-push vm (vm-global-get vm frame name))) - (= op 21) - (let - ((idx (frame-read-u16 frame)) (name (nth consts idx))) - (vm-global-set vm frame name (vm-peek vm))) - (= op 32) - (let - ((offset (frame-read-i16 frame))) - (dict-set! frame "ip" (+ (get frame "ip") offset))) - (= op 33) - (let - ((offset (frame-read-i16 frame)) (v (vm-pop vm))) - (when (not v) (dict-set! frame "ip" (+ (get frame "ip") offset)))) - (= op 34) - (let - ((offset (frame-read-i16 frame)) (v (vm-pop vm))) - (when v (dict-set! frame "ip" (+ (get frame "ip") offset)))) - (= op 48) - (let - ((argc (frame-read-u8 frame)) (args-rev (list)) (i 0)) - (define - collect-args - (fn - () - (when - (< i argc) - (set! args-rev (cons (vm-pop vm) args-rev)) - (set! i (+ i 1)) - (collect-args)))) - (collect-args) - (let ((f (vm-pop vm))) (vm-call vm f args-rev))) - (= op 49) - (let - ((argc (frame-read-u8 frame)) (args-rev (list)) (i 0)) - (define - collect-args - (fn - () - (when - (< i argc) - (set! args-rev (cons (vm-pop vm) args-rev)) - (set! i (+ i 1)) - (collect-args)))) - (collect-args) - (let - ((f (vm-pop vm))) + + ;; ---- Constants ---- + (= op 1) ;; OP_CONST + (let ((idx (frame-read-u16 frame))) + (vm-push vm (nth consts idx))) + + (= op 2) ;; OP_NIL + (vm-push vm nil) + + (= op 3) ;; OP_TRUE + (vm-push vm true) + + (= op 4) ;; OP_FALSE + (vm-push vm false) + + (= op 5) ;; OP_POP + (vm-pop vm) + + (= op 6) ;; OP_DUP + (vm-push vm (vm-peek vm)) + + ;; ---- Variable access ---- + (= op 16) ;; OP_LOCAL_GET + (let ((slot (frame-read-u8 frame))) + (vm-push vm (frame-local-get vm frame slot))) + + (= op 17) ;; OP_LOCAL_SET + (let ((slot (frame-read-u8 frame))) + (frame-local-set vm frame slot (vm-peek vm))) + + (= op 18) ;; OP_UPVALUE_GET + (let ((idx (frame-read-u8 frame))) + (vm-push vm (frame-upvalue-get frame idx))) + + (= op 19) ;; OP_UPVALUE_SET + (let ((idx (frame-read-u8 frame))) + (frame-upvalue-set frame idx (vm-peek vm))) + + (= op 20) ;; OP_GLOBAL_GET + (let ((idx (frame-read-u16 frame)) + (name (nth consts idx))) + (vm-push vm (vm-global-get vm frame name))) + + (= op 21) ;; OP_GLOBAL_SET + (let ((idx (frame-read-u16 frame)) + (name (nth consts idx))) + (vm-global-set vm frame name (vm-peek vm))) + + ;; ---- Control flow ---- + (= op 32) ;; OP_JUMP + (let ((offset (frame-read-i16 frame))) + (dict-set! frame "ip" (+ (get frame "ip") offset))) + + (= op 33) ;; OP_JUMP_IF_FALSE + (let ((offset (frame-read-i16 frame)) + (v (vm-pop vm))) + (when (not v) + (dict-set! frame "ip" (+ (get frame "ip") offset)))) + + (= op 34) ;; OP_JUMP_IF_TRUE + (let ((offset (frame-read-i16 frame)) + (v (vm-pop vm))) + (when v + (dict-set! frame "ip" (+ (get frame "ip") offset)))) + + ;; ---- Function calls ---- + (= op 48) ;; OP_CALL + (let ((argc (frame-read-u8 frame)) + (args-rev (list)) + (i 0)) + (define collect-args + (fn () + (when (< i argc) + (set! args-rev (cons (vm-pop vm) args-rev)) + (set! i (+ i 1)) + (collect-args)))) + (collect-args) + (let ((f (vm-pop vm))) + (vm-call vm f args-rev))) + + (= op 49) ;; OP_TAIL_CALL + (let ((argc (frame-read-u8 frame)) + (args-rev (list)) + (i 0)) + (define collect-args + (fn () + (when (< i argc) + (set! args-rev (cons (vm-pop vm) args-rev)) + (set! i (+ i 1)) + (collect-args)))) + (collect-args) + (let ((f (vm-pop vm))) + ;; Drop current frame, reuse stack space — true TCO + (dict-set! vm "frames" rest-frames) + (dict-set! vm "sp" (get frame "base")) + (vm-call vm f args-rev))) + + (= op 50) ;; OP_RETURN + (let ((result (vm-pop vm))) (dict-set! vm "frames" rest-frames) (dict-set! vm "sp" (get frame "base")) - (vm-call vm f args-rev))) - (= op 50) - (let - ((result (vm-pop vm))) - (dict-set! vm "frames" rest-frames) - (dict-set! vm "sp" (get frame "base")) - (vm-push vm result)) - (= op 51) - (let - ((idx (frame-read-u16 frame)) (code-val (nth consts idx))) - (let - ((cl (vm-create-closure vm frame code-val))) - (vm-push vm cl))) - (= op 52) - (let - ((idx (frame-read-u16 frame)) - (argc (frame-read-u8 frame)) - (name (nth consts idx)) - (args-rev (list)) - (i 0)) - (define - collect-args - (fn - () - (when - (< i argc) - (set! args-rev (cons (vm-pop vm) args-rev)) - (set! i (+ i 1)) - (collect-args)))) - (collect-args) - (cond - (= name "for-each") - (let - ((f (first args-rev)) (lst (nth args-rev 1))) - (for-each - (fn - (item) - (if - (vm-closure? f) - (vm-call-closure f (list item) (get f "vm-globals")) - (call-primitive "for-each" (list f (list item))))) - lst) - (vm-push vm nil)) - (= name "map") - (let - ((f (first args-rev)) (lst (nth args-rev 1))) - (vm-push - vm - (map - (fn - (item) - (if - (vm-closure? f) - (vm-call-closure f (list item) (get f "vm-globals")) - (call-primitive "map" (list f (list item))))) - lst))) - (= name "reduce") - (let - ((f (first args-rev)) - (init (nth args-rev 1)) - (lst (nth args-rev 2))) - (define - vm-reduce-loop - (fn - (remaining acc) - (if - (empty? remaining) - (vm-push vm acc) - (let - ((item (first remaining)) - (val - (if - (vm-closure? f) - (vm-call-closure - f - (list acc item) - (get f "vm-globals")) - (if - (callable? f) - (f acc item) - (cek-call f (list acc item)))))) - (vm-reduce-loop (rest remaining) val))))) - (vm-reduce-loop lst init)) - :else (vm-push vm (call-primitive name args-rev)))) - (= op 64) - (let - ((count (frame-read-u16 frame)) (items-rev (list)) (i 0)) - (define - collect-items - (fn - () - (when - (< i count) - (set! items-rev (cons (vm-pop vm) items-rev)) - (set! i (+ i 1)) - (collect-items)))) - (collect-items) - (vm-push vm items-rev)) - (= op 65) - (let - ((count (frame-read-u16 frame)) (d {}) (i 0)) - (define - collect-pairs - (fn - () - (when - (< i count) - (let - ((v (vm-pop vm)) (k (vm-pop vm))) - (dict-set! d k v) - (set! i (+ i 1)) - (collect-pairs))))) - (collect-pairs) - (vm-push vm d)) - (= op 144) - (let - ((count (frame-read-u8 frame)) (parts-rev (list)) (i 0)) - (define - collect-parts - (fn - () - (when - (< i count) - (set! parts-rev (cons (vm-pop vm) parts-rev)) - (set! i (+ i 1)) - (collect-parts)))) - (collect-parts) - (vm-push vm (apply str parts-rev))) - (= op 128) - (let - ((idx (frame-read-u16 frame)) (name (nth consts idx))) - (dict-set! (get vm "globals") name (vm-peek vm))) - (= op 160) - (let ((b (vm-pop vm)) (a (vm-pop vm))) (vm-push vm (+ a b))) - (= op 161) - (let ((b (vm-pop vm)) (a (vm-pop vm))) (vm-push vm (- a b))) - (= op 162) - (let ((b (vm-pop vm)) (a (vm-pop vm))) (vm-push vm (* a b))) - (= op 163) - (let ((b (vm-pop vm)) (a (vm-pop vm))) (vm-push vm (/ a b))) - (= op 164) - (let ((b (vm-pop vm)) (a (vm-pop vm))) (vm-push vm (= a b))) - (= op 165) - (let ((b (vm-pop vm)) (a (vm-pop vm))) (vm-push vm (< a b))) - (= op 166) - (let ((b (vm-pop vm)) (a (vm-pop vm))) (vm-push vm (> a b))) - (= op 167) - (vm-push vm (not (vm-pop vm))) - (= op 168) - (vm-push vm (len (vm-pop vm))) - (= op 169) - (vm-push vm (first (vm-pop vm))) - (= op 170) - (vm-push vm (rest (vm-pop vm))) - (= op 171) - (let - ((n (vm-pop vm)) (coll (vm-pop vm))) - (vm-push vm (nth coll n))) - (= op 172) - (let - ((coll (vm-pop vm)) (x (vm-pop vm))) - (vm-push vm (cons x coll))) - (= op 173) - (vm-push vm (- 0 (vm-pop vm))) - (= op 174) - (vm-push vm (inc (vm-pop vm))) - (= op 175) - (vm-push vm (dec (vm-pop vm))) - :else (error (str "VM: unknown opcode " op)))))) + (vm-push vm result)) -(define - vm-call-closure - (fn - (closure args globals) - (let - ((vm (make-vm globals))) + (= op 51) ;; OP_CLOSURE + (let ((idx (frame-read-u16 frame)) + (code-val (nth consts idx))) + (let ((cl (vm-create-closure vm frame code-val))) + (vm-push vm cl))) + + (= op 52) ;; OP_CALL_PRIM + (let ((idx (frame-read-u16 frame)) + (argc (frame-read-u8 frame)) + (name (nth consts idx)) + (args-rev (list)) + (i 0)) + (define collect-args + (fn () + (when (< i argc) + (set! args-rev (cons (vm-pop vm) args-rev)) + (set! i (+ i 1)) + (collect-args)))) + (collect-args) + (vm-push vm (call-primitive name args-rev))) + + ;; ---- Collections ---- + (= op 64) ;; OP_LIST + (let ((count (frame-read-u16 frame)) + (items-rev (list)) + (i 0)) + (define collect-items + (fn () + (when (< i count) + (set! items-rev (cons (vm-pop vm) items-rev)) + (set! i (+ i 1)) + (collect-items)))) + (collect-items) + (vm-push vm items-rev)) + + (= op 65) ;; OP_DICT + (let ((count (frame-read-u16 frame)) + (d {}) + (i 0)) + (define collect-pairs + (fn () + (when (< i count) + (let ((v (vm-pop vm)) + (k (vm-pop vm))) + (dict-set! d k v) + (set! i (+ i 1)) + (collect-pairs))))) + (collect-pairs) + (vm-push vm d)) + + ;; ---- String ops ---- + (= op 144) ;; OP_STR_CONCAT + (let ((count (frame-read-u8 frame)) + (parts-rev (list)) + (i 0)) + (define collect-parts + (fn () + (when (< i count) + (set! parts-rev (cons (vm-pop vm) parts-rev)) + (set! i (+ i 1)) + (collect-parts)))) + (collect-parts) + (vm-push vm (apply str parts-rev))) + + ;; ---- Define ---- + (= op 128) ;; OP_DEFINE + (let ((idx (frame-read-u16 frame)) + (name (nth consts idx))) + (dict-set! (get vm "globals") name (vm-peek vm))) + + ;; ---- Inline primitives ---- + (= op 160) ;; OP_ADD + (let ((b (vm-pop vm)) (a (vm-pop vm))) + (vm-push vm (+ a b))) + (= op 161) ;; OP_SUB + (let ((b (vm-pop vm)) (a (vm-pop vm))) + (vm-push vm (- a b))) + (= op 162) ;; OP_MUL + (let ((b (vm-pop vm)) (a (vm-pop vm))) + (vm-push vm (* a b))) + (= op 163) ;; OP_DIV + (let ((b (vm-pop vm)) (a (vm-pop vm))) + (vm-push vm (/ a b))) + (= op 164) ;; OP_EQ + (let ((b (vm-pop vm)) (a (vm-pop vm))) + (vm-push vm (= a b))) + (= op 165) ;; OP_LT + (let ((b (vm-pop vm)) (a (vm-pop vm))) + (vm-push vm (< a b))) + (= op 166) ;; OP_GT + (let ((b (vm-pop vm)) (a (vm-pop vm))) + (vm-push vm (> a b))) + (= op 167) ;; OP_NOT + (vm-push vm (not (vm-pop vm))) + (= op 168) ;; OP_LEN + (vm-push vm (len (vm-pop vm))) + (= op 169) ;; OP_FIRST + (vm-push vm (first (vm-pop vm))) + (= op 170) ;; OP_REST + (vm-push vm (rest (vm-pop vm))) + (= op 171) ;; OP_NTH + (let ((n (vm-pop vm)) (coll (vm-pop vm))) + (vm-push vm (nth coll n))) + (= op 172) ;; OP_CONS + (let ((coll (vm-pop vm)) (x (vm-pop vm))) + (vm-push vm (cons x coll))) + (= op 173) ;; OP_NEG + (vm-push vm (- 0 (vm-pop vm))) + (= op 174) ;; OP_INC + (vm-push vm (inc (vm-pop vm))) + (= op 175) ;; OP_DEC + (vm-push vm (dec (vm-pop vm))) + + :else + (error (str "VM: unknown opcode " op)))))) + + +;; -------------------------------------------------------------------------- +;; 12. Entry points +;; -------------------------------------------------------------------------- + +;; Execute a closure with arguments — creates a fresh VM. +(define vm-call-closure + (fn (closure args globals) + (let ((vm (make-vm globals))) (vm-push-frame vm closure args) (vm-run vm) (vm-pop vm)))) -(define - vm-execute-module - (fn - (code globals) - (let - ((closure (make-vm-closure code (list) "module" globals nil)) - (vm (make-vm globals))) - (let - ((frame (make-vm-frame closure 0))) - (let - ((i 0) (total (get code "vc-locals"))) - (define - pad-loop - (fn - () - (when - (< i total) +;; Execute a compiled module (top-level bytecode). +(define vm-execute-module + (fn (code globals) + (let ((closure (make-vm-closure code (list) "module" globals nil)) + (vm (make-vm globals))) + (let ((frame (make-vm-frame closure 0))) + ;; Pad local slots + (let ((i 0) + (total (get code "vc-locals"))) + (define pad-loop + (fn () + (when (< i total) (vm-push vm nil) (set! i (+ i 1)) (pad-loop)))) @@ -555,3 +609,25 @@ (dict-set! vm "frames" (list frame)) (vm-run vm) (vm-pop vm))))) + + +;; -------------------------------------------------------------------------- +;; 13. Platform interface +;; -------------------------------------------------------------------------- +;; +;; Each target must provide: +;; +;; make-vm-stack(size) → opaque stack (array-like) +;; vm-stack-get(stack, idx) → value at index +;; vm-stack-set!(stack, idx, value) → mutate index +;; vm-stack-length(stack) → current capacity +;; vm-stack-copy!(src, dst, count) → copy first count elements +;; +;; cek-call(f, args) → evaluate via CEK machine (fallback) +;; get-primitive(name) → look up primitive by name (returns callable) +;; call-primitive(name, args) → call primitive directly with args list +;; +;; env-parent(env) → parent environment or nil +;; env-has?(env, name) → boolean +;; env-get(env, name) → value +;; env-set!(env, name, value) → mutate binding diff --git a/shared/static/scripts/sx-browser.js b/shared/static/scripts/sx-browser.js index b68c334a..cf7c957e 100644 --- a/shared/static/scripts/sx-browser.js +++ b/shared/static/scripts/sx-browser.js @@ -24,7 +24,7 @@ // ========================================================================= var NIL = Object.freeze({ _nil: true, toString: function() { return "nil"; } }); - var SX_VERSION = "2026-03-31T09:56:06Z"; + var SX_VERSION = "2026-03-31T10:16:31Z"; function isNil(x) { return x === NIL || x === null || x === undefined; } function isSxTruthy(x) { return x !== false && !isNil(x); } @@ -6776,35 +6776,6 @@ PRIMITIVES["resource"] = resource; // Aliases for VM bytecode compatibility PRIMITIVES["length"] = PRIMITIVES["len"]; - // VM-compatible HOF primitives — use callPrimFn which handles native, lambda, and VM closures - function callPrimFn(f, args) { - if (typeof f === "function") return f.apply(null, args); - if (f && f._lambda) return cekCall(f, args); - if (f && f["vm-code"]) { - // VM closure — call through call-primitive dispatch - var cp = PRIMITIVES["vm-call-closure"]; - if (cp) return cp(f, args); - } - return cekCall(f, args); - } - PRIMITIVES["map"] = function(fn, lst) { - if (Array.isArray(fn)) { var tmp = fn; fn = lst; lst = tmp; } - var result = []; - for (var i = 0; i < lst.length; i++) result.push(callPrimFn(fn, [lst[i]])); - return result; - }; - PRIMITIVES["for-each"] = function(fn, lst) { - if (Array.isArray(fn)) { var tmp = fn; fn = lst; lst = tmp; } - for (var i = 0; i < lst.length; i++) callPrimFn(fn, [lst[i]]); - return NIL; - }; - PRIMITIVES["reduce"] = function(fn, init, lst) { - if (Array.isArray(fn)) { var tmp = fn; fn = lst; lst = init; init = tmp; } - var acc = init; - for (var i = 0; i < lst.length; i++) acc = callPrimFn(fn, [acc, lst[i]]); - return acc; - }; - // FFI library functions — defined in dom.sx/browser.sx but not transpiled. // Registered here so runtime-evaluated SX code (data-init, islands) can use them. PRIMITIVES["prevent-default"] = preventDefault_;