VM spec in SX + 72 tests passing on both JS and OCaml

spec/vm.sx — bytecode VM written in SX (the spec):
  - Stack-based interpreter for bytecode from compiler.sx
  - 24 opcodes: constants, variables (local/upvalue/global), control flow,
    function calls (with TCO), closures with upvalue capture, collections,
    string concat, define
  - Upvalue cells for shared mutable closure variables
  - Call dispatch: vm-closure (fast path), native-fn, CEK fallback
  - Platform interface: 7 primitives (vm-stack-*, call-primitive, cek-call,
    get-primitive, env-parent)

spec/tests/test-vm.sx — 72 tests exercising compile→bytecode→VM pipeline:
  constants, arithmetic, comparison, control flow (if/when/cond/case/and/or),
  let bindings, lambda, closures, upvalue mutation, TCO (10K iterations),
  collections, strings, define, letrec, quasiquote, threading, integration
  (fibonacci, recursive map/filter/reduce, compose)

spec/compiler.sx — fix :else keyword detection in case/cond compilation
  (was comparing Keyword object to evaluated string, now checks type)

Platform primitives added (JS + OCaml):
  make-vm-stack, vm-stack-get, vm-stack-set!, vm-stack-length, vm-stack-copy!,
  primitive?, get-primitive, call-primitive, set-nth! (JS)

Test runners updated to load bytecode.sx + compiler.sx + vm.sx for --full.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-24 01:20:00 +00:00
parent 5270d2e956
commit 23c8b97cb1
8 changed files with 1169 additions and 7 deletions

View File

@@ -63,11 +63,12 @@ SPEC_MODULES = {
"signals": ("signals.sx", "signals (reactive signal runtime)"),
"page-helpers": ("page-helpers.sx", "page-helpers (pure data transformation helpers)"),
"types": ("types.sx", "types (gradual type system)"),
"vm": ("vm.sx", "vm (bytecode virtual machine)"),
}
# Note: frames and cek are now part of evaluator.sx (always loaded as core)
# Explicit ordering for spec modules with dependencies.
SPEC_MODULE_ORDER = ["deps", "page-helpers", "router", "signals", "types"]
SPEC_MODULE_ORDER = ["deps", "page-helpers", "router", "signals", "types", "vm"]
EXTENSION_NAMES = {"continuations"}
@@ -1139,6 +1140,44 @@ PRIMITIVES_JS_MODULES: dict[str, str] = {
}
PRIMITIVES["scope-emit!"] = scopeEmit;
PRIMITIVES["scope-peek"] = scopePeek;
// ---- VM stack primitives ----
// The VM spec (vm.sx) requires these array-like operations.
// In JS, a plain Array serves as the stack.
PRIMITIVES["make-vm-stack"] = function(size) {
var a = new Array(size);
for (var i = 0; i < size; i++) a[i] = NIL;
return a;
};
PRIMITIVES["vm-stack-get"] = function(stack, idx) { return stack[idx]; };
PRIMITIVES["vm-stack-set!"] = function(stack, idx, value) { stack[idx] = value; return NIL; };
PRIMITIVES["vm-stack-length"] = function(stack) { return stack.length; };
PRIMITIVES["vm-stack-copy!"] = function(src, dst, count) {
for (var i = 0; i < count; i++) dst[i] = src[i];
return NIL;
};
PRIMITIVES["get-primitive"] = function(name) {
if (name in PRIMITIVES) return PRIMITIVES[name];
throw new Error("VM undefined: " + name);
};
PRIMITIVES["call-primitive"] = function(name, args) {
if (!(name in PRIMITIVES)) throw new Error("VM undefined: " + name);
var fn = PRIMITIVES[name];
return fn.apply(null, Array.isArray(args) ? args : []);
};
PRIMITIVES["primitive?"] = function(name) {
return name in PRIMITIVES;
};
PRIMITIVES["set-nth!"] = function(lst, idx, val) {
lst[idx] = val;
return NIL;
};
PRIMITIVES["env-parent"] = function(env) {
if (env && Object.getPrototypeOf(env) !== Object.prototype &&
Object.getPrototypeOf(env) !== null)
return Object.getPrototypeOf(env);
return NIL;
};
''',
}
# Modules to include by default (all)

View File

@@ -81,6 +81,7 @@ env["env-extend"] = function(e) { return Object.create(e); };
env["env-merge"] = function(a, b) { return Object.assign({}, a, b); };
// Missing primitives referenced by tests
// primitive? is now in platform.py PRIMITIVES
env["upcase"] = function(s) { return s.toUpperCase(); };
env["downcase"] = function(s) { return s.toLowerCase(); };
env["make-keyword"] = function(name) { return new Sx.Keyword(name); };
@@ -277,6 +278,23 @@ for (const expr of frameworkExprs) {
Sx.eval(expr, env);
}
// Load compiler + VM spec when running full tests
if (fullBuild) {
const specDir = path.join(projectDir, "spec");
for (const specFile of ["bytecode.sx", "compiler.sx", "vm.sx"]) {
const specPath = path.join(specDir, specFile);
if (fs.existsSync(specPath)) {
const src = fs.readFileSync(specPath, "utf8");
const exprs = Sx.parse(src);
for (const expr of exprs) {
try { Sx.eval(expr, env); } catch (e) {
console.error(`Error loading ${specFile}: ${e.message}`);
}
}
}
}
}
// Determine which tests to run
const args = process.argv.slice(2).filter(a => !a.startsWith("--"));
let testFiles = [];
@@ -293,7 +311,7 @@ if (args.length > 0) {
}
} else {
// Tests requiring optional modules (only run with --full)
const requiresFull = new Set(["test-continuations.sx", "test-types.sx", "test-freeze.sx"]);
const requiresFull = new Set(["test-continuations.sx", "test-continuations-advanced.sx", "test-types.sx", "test-freeze.sx", "test-vm.sx"]);
// All spec tests
for (const f of fs.readdirSync(specTests).sort()) {
if (f.startsWith("test-") && f.endsWith(".sx") && f !== "test-framework.sx") {

View File

@@ -637,12 +637,24 @@ let run_spec_tests env test_files =
Printf.printf "\nLoading test framework...\n%!";
load_and_eval framework_path;
(* Load compiler + VM spec for test-vm.sx *)
let spec_dir = Filename.concat project_dir "spec" in
List.iter (fun name ->
let path = Filename.concat spec_dir name in
if Sys.file_exists path then begin
Printf.printf "Loading %s...\n%!" name;
(try load_and_eval path
with e -> Printf.eprintf "Warning: %s: %s\n%!" name (Printexc.to_string e))
end
) ["bytecode.sx"; "compiler.sx"; "vm.sx"];
(* Determine test files *)
let files = if test_files = [] then begin
let entries = Sys.readdir spec_tests_dir in
Array.sort String.compare entries;
let requires_full = ["test-continuations.sx"; "test-types.sx"; "test-freeze.sx";
"test-continuations-advanced.sx"; "test-signals-advanced.sx"] in
"test-continuations-advanced.sx"; "test-signals-advanced.sx";
"test-vm.sx"] in
Array.to_list entries
|> List.filter (fun f ->
String.length f > 5 &&

View File

@@ -594,7 +594,8 @@ let () =
| _ -> raise (Eval_error "error: 1 arg"));
register "apply" (fun args ->
match args with
| [NativeFn (_, f); List a] -> f a
| [NativeFn (_, f); (List a | ListRef { contents = a })] -> f a
| [NativeFn (_, f); Nil] -> f []
| _ -> raise (Eval_error "apply: function and list"));
register "identical?" (fun args ->
match args with [a; b] -> Bool (a == b) | _ -> raise (Eval_error "identical?: 2 args"));
@@ -668,4 +669,53 @@ let () =
Bool (List.for_all (fun x -> sx_truthy (call_any f [x])) items)
| [_; Nil] -> Bool true
| _ -> raise (Eval_error "every?: expected (fn list)"));
(* ---- VM stack primitives (vm.sx platform interface) ---- *)
register "make-vm-stack" (fun args ->
match args with
| [Number n] -> ListRef (ref (List.init (int_of_float n) (fun _ -> Nil)))
| _ -> raise (Eval_error "make-vm-stack: expected (size)"));
register "vm-stack-get" (fun args ->
match args with
| [ListRef r; Number n] -> List.nth !r (int_of_float n)
| _ -> raise (Eval_error "vm-stack-get: expected (stack idx)"));
register "vm-stack-set!" (fun args ->
match args with
| [ListRef r; Number n; v] ->
let i = int_of_float n in
r := List.mapi (fun j x -> if j = i then v else x) !r; Nil
| _ -> raise (Eval_error "vm-stack-set!: expected (stack idx val)"));
register "vm-stack-length" (fun args ->
match args with
| [ListRef r] -> Number (float_of_int (List.length !r))
| _ -> raise (Eval_error "vm-stack-length: expected (stack)"));
register "vm-stack-copy!" (fun args ->
match args with
| [ListRef src; ListRef dst; Number n] ->
let count = int_of_float n in
let src_items = !src in
dst := List.mapi (fun i x -> if i < count then List.nth src_items i else x) !dst; Nil
| _ -> raise (Eval_error "vm-stack-copy!: expected (src dst count)"));
register "primitive?" (fun args ->
match args with
| [String name] -> Bool (Hashtbl.mem primitives name)
| _ -> Bool false);
register "get-primitive" (fun args ->
match args with
| [String name] ->
(match Hashtbl.find_opt primitives name with
| Some fn -> NativeFn (name, fn)
| None -> raise (Eval_error ("VM undefined: " ^ name)))
| _ -> raise (Eval_error "get-primitive: expected (name)"));
register "call-primitive" (fun args ->
match args with
| [String name; (List a | ListRef { contents = a })] ->
(match Hashtbl.find_opt primitives name with
| Some fn -> fn a
| None -> raise (Eval_error ("VM undefined: " ^ name)))
| [String name; Nil] ->
(match Hashtbl.find_opt primitives name with
| Some fn -> fn []
| None -> raise (Eval_error ("VM undefined: " ^ name)))
| _ -> raise (Eval_error "call-primitive: expected (name args-list)"));
()