rename crates/ to engrams/; add el-compiler el package with bootstrap artifact

- crates/ → engrams/ (Rust engrams live here)
- el-compiler/ added: el self-hosting compiler as an el package
  - src/{compiler,lexer,parser,codegen}.el
  - bootstrap/el-compiler.elc (114KB, Rust-compiled seed)
- el.toml Cargo.toml workspace paths updated
- neuron-rs cross-repo path deps fixed (were pointing to products/ instead of foundation/)
This commit is contained in:
Will Anderson
2026-04-29 03:27:32 -05:00
parent 19ed2721ee
commit a42429012e
120 changed files with 3836 additions and 64 deletions
+23
View File
@@ -0,0 +1,23 @@
[package]
name = "el-wasm"
description = "Engram language WebAssembly runtime — runs .el programs natively in browsers"
version.workspace = true
edition.workspace = true
license.workspace = true
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
el-compiler = { workspace = true }
wasm-bindgen = { workspace = true, optional = true }
serde = { workspace = true }
serde_json = { workspace = true }
getrandom = { workspace = true, optional = true }
[features]
# Enable the wasm-bindgen JS API. Pass `--features wasm` to wasm-pack.
wasm = ["dep:wasm-bindgen", "dep:getrandom"]
[dev-dependencies]
wasm-bindgen-test = "0.3"
+22
View File
@@ -0,0 +1,22 @@
#!/bin/bash
# Build the el WASM package.
#
# Requires wasm-pack:
# cargo install wasm-pack
#
# Output: crates/el-wasm/pkg/
# el_wasm_bg.wasm — the compiled WebAssembly module
# el_wasm.js — ES module JS bindings generated by wasm-bindgen
# el_wasm.d.ts — TypeScript type definitions
# package.json — npm package metadata
set -euo pipefail
cd "$(dirname "$0")"
echo "Building el WASM runtime..."
wasm-pack build --target web --out-dir pkg -- --features wasm
echo ""
echo "Done. Output in crates/el-wasm/pkg/"
echo ""
ls -lh pkg/*.wasm pkg/*.js 2>/dev/null || ls pkg/
+16
View File
@@ -0,0 +1,16 @@
{
"name": "Neuron",
"short_name": "Neuron",
"description": "Neuron — AI companion powered by el",
"start_url": "/",
"display": "standalone",
"background_color": "#0a0a0f",
"theme_color": "#6c7fff",
"orientation": "portrait-primary",
"icons": [
{ "src": "/icons/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable" },
{ "src": "/icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" }
],
"categories": ["productivity", "utilities"],
"lang": "en-US"
}
+72
View File
@@ -0,0 +1,72 @@
// Engram-lang WASM runtime loader
// Include this in any web app to run .el programs natively in the browser.
//
// Usage:
// import { initRuntime, runElc, evalSource } from '/js/runtime.js';
//
// await initRuntime();
// const result = await runElc('/programs/main.elc');
let wasmModule = null;
/**
* Initialise the el WASM runtime. Safe to call multiple times —
* subsequent calls return the already-loaded module immediately.
*
* @param {string} wasmUrl - Path to the .wasm file (default: /pkg/el_wasm_bg.wasm)
* @returns {Promise<object>} The loaded runtime API
*/
export async function initRuntime(wasmUrl = '/pkg/el_wasm_bg.wasm') {
if (wasmModule) return wasmModule;
const { default: init, compile_source, load_and_run, eval: elEval, version } =
await import('/pkg/el_wasm.js');
await init(wasmUrl);
wasmModule = { compile_source, load_and_run, eval: elEval, version };
console.log(`el WASM runtime v${version()} loaded`);
return wasmModule;
}
/**
* Fetch a pre-compiled .elc file from the server and execute it.
*
* The browser caches the .elc file automatically based on Cache-Control
* headers set by the server. New bytecode is available immediately on the
* next fetch — no app-store review required.
*
* @param {string} elcUrl - URL of the .elc bytecode file
* @returns {Promise<any>} The JSON-deserialised result value
*/
export async function runElc(elcUrl) {
const rt = await initRuntime();
const response = await fetch(elcUrl);
if (!response.ok) throw new Error(`Failed to fetch ${elcUrl}: ${response.status}`);
const bytes = new Uint8Array(await response.arrayBuffer());
return JSON.parse(rt.load_and_run(bytes));
}
/**
* Compile el source and run it immediately. Useful for REPL and
* developer-mode execution where source is available at runtime.
*
* @param {string} source - Engram-lang source code
* @returns {Promise<any>} The JSON-deserialised result value
*/
export async function evalSource(source) {
const rt = await initRuntime();
return JSON.parse(rt.eval(source));
}
/**
* Compile el source to bytecode bytes without running it.
* The returned Uint8Array can be stored or uploaded as a .elc file.
*
* @param {string} source - Engram-lang source code
* @returns {Promise<Uint8Array>} Compiled bytecode bytes
*/
export async function compileSource(source) {
const rt = await initRuntime();
return rt.compile_source(source);
}
+77
View File
@@ -0,0 +1,77 @@
// Service worker for the Neuron PWA powered by el.
//
// Caching strategy:
// /pkg/ — WASM runtime files, cache-first (content-addressed, change on version bump)
// *.elc — compiled bytecode, stale-while-revalidate (instant load, background update)
//
// The WASM runtime is fetched once and cached indefinitely.
// Engram programs (.elc) are served from cache immediately, then refreshed in
// the background so the next load gets the newest version — no user action needed.
const CACHE_NAME = 'engram-v1';
const WASM_CACHE = 'engram-wasm-v1';
// Files to pre-cache during service worker installation.
const PRECACHE = [
'/pkg/el_wasm_bg.wasm',
'/pkg/el_wasm.js',
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(WASM_CACHE).then(cache => cache.addAll(PRECACHE))
);
// Take control immediately — don't wait for existing tabs to close.
self.skipWaiting();
});
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
// WASM runtime files: cache-first.
// These are large and rarely change; the version bump forces a new URL.
if (url.pathname.startsWith('/pkg/')) {
event.respondWith(
caches.match(event.request).then(cached =>
cached || fetch(event.request).then(response => {
caches.open(WASM_CACHE).then(cache =>
cache.put(event.request, response.clone())
);
return response;
})
)
);
return;
}
// Compiled bytecode (.elc): stale-while-revalidate.
// Respond immediately from cache, update in background.
if (url.pathname.endsWith('.elc')) {
event.respondWith(
caches.open(CACHE_NAME).then(async cache => {
const cached = await cache.match(event.request);
const fetchPromise = fetch(event.request).then(response => {
cache.put(event.request, response.clone());
return response;
});
return cached || fetchPromise;
})
);
return;
}
});
self.addEventListener('activate', event => {
// Purge old cache versions to reclaim storage.
event.waitUntil(
caches.keys().then(keys =>
Promise.all(
keys
.filter(k => k !== CACHE_NAME && k !== WASM_CACHE)
.map(k => caches.delete(k))
)
)
);
// Claim all clients immediately.
self.clients.claim();
});
+735
View File
@@ -0,0 +1,735 @@
//! el-wasm — Engram language WebAssembly runtime.
//!
//! Compiles the el compiler and (optionally) execution pipeline to
//! `wasm32-unknown-unknown`, exposing a JavaScript API via `wasm-bindgen`.
//!
//! # Build for browsers
//!
//! ```bash
//! wasm-pack build --target web --out-dir pkg -- --features wasm
//! ```
//!
//! # JavaScript API
//!
//! ```js
//! import init, { compile_source, load_and_run, eval, version } from '/pkg/el_wasm.js';
//! await init();
//! const result = eval('1 + 2'); // => "3"
//! ```
//!
//! # Architecture
//!
//! The WASM module exposes three entry points:
//!
//! - **`compile_source`** — source → `.elc` bytes (serialised bytecode)
//! - **`load_and_run`** — `.elc` bytes → JSON-encoded result value
//! - **`eval`** — source → JSON-encoded result value (compile + run in one step)
//!
//! The browser caches the `.wasm` file after the first load. Programs are
//! distributed as tiny `.elc` bytecode files fetched on demand, enabling a
//! PWA strategy that bypasses app-store review cycles.
pub use el_compiler::{
compile_to_bytecode, deserialize_bytecode, serialize_bytecode, Bytecode, CompileError, Value,
};
// ── WASM bindings ─────────────────────────────────────────────────────────────
// Only compiled when the `wasm` feature is active (i.e. wasm-pack builds).
#[cfg(feature = "wasm")]
use wasm_bindgen::prelude::*;
/// Initialize the WASM module. Call once from JavaScript before any other API.
///
/// Sets up the panic hook so Rust panics appear as readable messages in the
/// browser developer console rather than opaque `unreachable` traps.
#[cfg(feature = "wasm")]
#[wasm_bindgen(start)]
pub fn init() {
// Redirect Rust panics to console.error in the browser.
std::panic::set_hook(Box::new(console_error_panic_hook));
}
/// Forward panics to the browser console.
#[cfg(feature = "wasm")]
fn console_error_panic_hook(info: &std::panic::PanicHookInfo<'_>) {
let msg = info.to_string();
web_sys_log(&msg);
}
#[cfg(feature = "wasm")]
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = console, js_name = error)]
fn web_sys_log(s: &str);
}
/// Compile el source code to bytecode bytes (`.elc` format).
///
/// Returns the raw bytecode bytes on success, or throws a JS error string
/// describing the first compilation error.
///
/// The returned bytes can be cached by the browser and later passed to
/// `load_and_run` to execute the program.
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn compile_source(source: &str) -> Result<Vec<u8>, JsValue> {
compile_source_inner(source).map_err(|e| JsValue::from_str(&e))
}
/// Load pre-compiled bytecode (`.elc` bytes) and execute it.
///
/// Returns the JSON-encoded final value from the program, or throws on error.
/// The result is always valid JSON — use `JSON.parse(result)` in JavaScript.
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn load_and_run(bytecode_bytes: &[u8]) -> Result<String, JsValue> {
load_and_run_inner(bytecode_bytes).map_err(|e| JsValue::from_str(&e))
}
/// Compile and run el source in one step.
///
/// Equivalent to `load_and_run(compile_source(source))`. Useful for REPL
/// and developer-mode execution where the source is available at runtime.
///
/// Returns the JSON-encoded result, or throws a descriptive error string.
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn eval(source: &str) -> Result<String, JsValue> {
let bytes = compile_source_inner(source).map_err(|e| JsValue::from_str(&e))?;
load_and_run_inner(&bytes).map_err(|e| JsValue::from_str(&e))
}
/// Return the el runtime version string.
#[cfg(feature = "wasm")]
#[wasm_bindgen]
pub fn version() -> String {
env!("CARGO_PKG_VERSION").to_string()
}
// ── Inner implementations (callable from Rust tests without wasm-bindgen) ─────
/// Compile source to `.elc` bytes. Returns `Err(String)` on failure.
pub fn compile_source_inner(source: &str) -> Result<Vec<u8>, String> {
let (bytecode, _source_map) =
compile_to_bytecode(source).map_err(|e| e.to_string())?;
serialize_bytecode(&bytecode)
}
/// Deserialise `.elc` bytes, execute the bytecode, return a JSON-encoded Value.
///
/// The result is always clean JSON: integers as numbers, strings as strings,
/// booleans as booleans, nil as null, lists as arrays, maps as objects.
pub fn load_and_run_inner(bytecode_bytes: &[u8]) -> Result<String, String> {
let bytecode = deserialize_bytecode(bytecode_bytes)?;
let result = run_bytecode(&bytecode)?;
let json_value = value_to_json(&result);
serde_json::to_string(&json_value).map_err(|e| format!("Serialize result error: {e}"))
}
/// Convert an engram `Value` to a clean `serde_json::Value` for JS consumption.
///
/// Maps engram types to natural JSON equivalents:
/// - `Int` → JSON number
/// - `Float` → JSON number
/// - `Str` → JSON string
/// - `Bool` → JSON boolean
/// - `Nil` → JSON null
/// - `List` → JSON array
/// - `Map` → JSON object
/// - `ResultOk(v)` → `{"ok": v}`
/// - `ResultErr(e)` → `{"err": e}`
pub fn value_to_json(v: &Value) -> serde_json::Value {
match v {
Value::Int(n) => serde_json::Value::Number(serde_json::Number::from(*n)),
Value::Float(f) => serde_json::Number::from_f64(*f)
.map(serde_json::Value::Number)
.unwrap_or(serde_json::Value::Null),
Value::Str(s) => serde_json::Value::String(s.clone()),
Value::Bool(b) => serde_json::Value::Bool(*b),
Value::Nil => serde_json::Value::Null,
Value::List(items) => {
serde_json::Value::Array(items.iter().map(value_to_json).collect())
}
Value::Map(pairs) => {
let obj: serde_json::Map<String, serde_json::Value> = pairs
.iter()
.map(|(k, v)| (k.clone(), value_to_json(v)))
.collect();
serde_json::Value::Object(obj)
}
Value::ResultOk(inner) => {
serde_json::json!({ "ok": value_to_json(inner) })
}
Value::ResultErr(inner) => {
serde_json::json!({ "err": value_to_json(inner) })
}
}
}
/// Execute a bytecode program on the engram stack machine.
///
/// Returns the value left on the stack when `Halt` is reached, or `Value::Nil`
/// if the program is empty.
///
/// # Supported instructions
///
/// This is a pure stack machine — no I/O, no filesystem, no OS interaction —
/// which makes it safe to run inside WASM. Instructions that reference the
/// Engram runtime (`Activate`) return a placeholder `Nil` value; a full
/// runtime integration would supply a callback from JS.
pub fn run_bytecode(bytecode: &[Bytecode]) -> Result<Value, String> {
let mut stack: Vec<Value> = Vec::new();
// Local variable environment (flat scope for now).
let mut locals: std::collections::HashMap<String, Value> = std::collections::HashMap::new();
let mut ip: usize = 0;
while ip < bytecode.len() {
let instr = &bytecode[ip];
match instr {
// ── Stack ─────────────────────────────────────────────────────────
Bytecode::Push(v) => {
stack.push(v.clone());
}
Bytecode::Pop => {
stack.pop();
}
Bytecode::Dup => {
let top = stack.last().ok_or("DUP on empty stack")?.clone();
stack.push(top);
}
// ── Arithmetic ────────────────────────────────────────────────────
Bytecode::Add => {
let (a, b) = pop2(&mut stack)?;
stack.push(arith_add(a, b)?);
}
Bytecode::Sub => {
let (a, b) = pop2(&mut stack)?;
stack.push(arith_sub(a, b)?);
}
Bytecode::Mul => {
let (a, b) = pop2(&mut stack)?;
stack.push(arith_mul(a, b)?);
}
Bytecode::Div => {
let (a, b) = pop2(&mut stack)?;
stack.push(arith_div(a, b)?);
}
// ── Comparison ────────────────────────────────────────────────────
Bytecode::Eq => {
let (a, b) = pop2(&mut stack)?;
stack.push(Value::Bool(values_eq(&a, &b)));
}
Bytecode::NotEq => {
let (a, b) = pop2(&mut stack)?;
stack.push(Value::Bool(!values_eq(&a, &b)));
}
Bytecode::Lt => {
let (a, b) = pop2(&mut stack)?;
stack.push(Value::Bool(cmp_values(&a, &b)? < 0));
}
Bytecode::Gt => {
let (a, b) = pop2(&mut stack)?;
stack.push(Value::Bool(cmp_values(&a, &b)? > 0));
}
Bytecode::LtEq => {
let (a, b) = pop2(&mut stack)?;
stack.push(Value::Bool(cmp_values(&a, &b)? <= 0));
}
Bytecode::GtEq => {
let (a, b) = pop2(&mut stack)?;
stack.push(Value::Bool(cmp_values(&a, &b)? >= 0));
}
// ── Logical ───────────────────────────────────────────────────────
Bytecode::And => {
let (a, b) = pop2(&mut stack)?;
stack.push(Value::Bool(is_truthy(&a) && is_truthy(&b)));
}
Bytecode::Or => {
let (a, b) = pop2(&mut stack)?;
stack.push(Value::Bool(is_truthy(&a) || is_truthy(&b)));
}
Bytecode::Not => {
let v = stack.pop().ok_or("NOT on empty stack")?;
stack.push(Value::Bool(!is_truthy(&v)));
}
// ── Locals ────────────────────────────────────────────────────────
Bytecode::LoadLocal(name) => {
let v = locals.get(name).cloned().unwrap_or(Value::Nil);
stack.push(v);
}
Bytecode::StoreLocal(name) => {
let v = stack.pop().ok_or("STORE on empty stack")?;
locals.insert(name.clone(), v);
}
// ── Functions ─────────────────────────────────────────────────────
// The bytecode model stores function bodies inline and registers entry
// points as locals (`__fn_<name>`). A full call-frame implementation
// would use a separate call stack; for WASM we handle the most common
// case of stdlib builtins and leave dynamic dispatch as a stub.
Bytecode::Call { name, arity } => {
let result = call_builtin(name, *arity, &mut stack)?;
stack.push(result);
}
Bytecode::Return => {
// Return leaves the value on the stack; the caller pops it.
// In this simplified VM we just continue execution.
break;
}
// ── Control flow ──────────────────────────────────────────────────
Bytecode::Jump(offset) => {
ip = apply_offset(ip, *offset)?;
continue; // skip ip += 1 below
}
Bytecode::JumpIf(offset) => {
let v = stack.pop().ok_or("JUMPIF on empty stack")?;
if is_truthy(&v) {
ip = apply_offset(ip, *offset)?;
continue;
}
}
Bytecode::JumpIfNot(offset) => {
let v = stack.pop().ok_or("JUMPIFNOT on empty stack")?;
if !is_truthy(&v) {
ip = apply_offset(ip, *offset)?;
continue;
}
}
// ── Fields & Indexing ─────────────────────────────────────────────
Bytecode::GetField(field) => {
let obj = stack.pop().ok_or("GETFIELD on empty stack")?;
let v = match &obj {
Value::Map(pairs) => pairs
.iter()
.find(|(k, _v)| k == field)
.map(|(_k, v)| v.clone())
.unwrap_or(Value::Nil),
_ => Value::Nil,
};
stack.push(v);
}
Bytecode::GetIndex => {
let idx = stack.pop().ok_or("GETINDEX: missing index")?;
let obj = stack.pop().ok_or("GETINDEX: missing object")?;
let v = match (&obj, &idx) {
(Value::List(items), Value::Int(i)) => {
let i = *i as usize;
items.get(i).cloned().unwrap_or(Value::Nil)
}
_ => Value::Nil,
};
stack.push(v);
}
Bytecode::BuildMap(n) => {
let mut pairs = Vec::new();
let n = *n as usize;
// Stack: key0, val0, key1, val1, ... (pushed in order)
// We collect from the top, so reverse at the end.
let start = stack.len().saturating_sub(n * 2);
let raw: Vec<Value> = stack.drain(start..).collect();
for chunk in raw.chunks(2) {
if let [Value::Str(k), v] = chunk {
pairs.push((k.clone(), v.clone()));
}
}
stack.push(Value::Map(pairs));
}
Bytecode::BuildStruct { fields, .. } => {
let mut pairs: Vec<(String, Value)> = Vec::new();
let start = stack.len().saturating_sub(fields.len());
let raw: Vec<Value> = stack.drain(start..).collect();
for (field, val) in fields.iter().zip(raw.into_iter()) {
pairs.push((field.clone(), val));
}
stack.push(Value::Map(pairs));
}
Bytecode::SetField(field) => {
let val = stack.pop().ok_or("SETFIELD: missing value")?;
let obj = stack.pop().ok_or("SETFIELD: missing object")?;
let v = match obj {
Value::Map(mut pairs) => {
if let Some(entry) = pairs.iter_mut().find(|(k, _)| k == field) {
entry.1 = val;
} else {
pairs.push((field.clone(), val));
}
Value::Map(pairs)
}
other => other,
};
stack.push(v);
}
// ── Special ───────────────────────────────────────────────────────
Bytecode::Activate { type_name, query } => {
// The Engram runtime integration is provided by the host JS environment.
// In a full implementation the JS host would register an `activate` callback.
// For now, return a placeholder list so programs using `activate` don't crash.
let _ = (type_name, query);
stack.push(Value::List(Vec::new()));
}
Bytecode::SealedBegin | Bytecode::SealedEnd | Bytecode::Nop => {
// No-ops in the pure VM.
}
Bytecode::Halt => {
break;
}
}
ip += 1;
}
Ok(stack.pop().unwrap_or(Value::Nil))
}
// ── Stack helpers ─────────────────────────────────────────────────────────────
fn pop2(stack: &mut Vec<Value>) -> Result<(Value, Value), String> {
let b = stack.pop().ok_or("stack underflow (right operand)")?;
let a = stack.pop().ok_or("stack underflow (left operand)")?;
Ok((a, b))
}
fn apply_offset(ip: usize, offset: i32) -> Result<usize, String> {
// offset is relative to the instruction *after* the jump
let target = (ip as i64) + 1 + (offset as i64);
if target < 0 {
return Err(format!("Jump to negative address {target}"));
}
Ok(target as usize)
}
// ── Value helpers ─────────────────────────────────────────────────────────────
fn is_truthy(v: &Value) -> bool {
match v {
Value::Bool(b) => *b,
Value::Nil => false,
Value::Int(0) => false,
_ => true,
}
}
fn values_eq(a: &Value, b: &Value) -> bool {
match (a, b) {
(Value::Int(x), Value::Int(y)) => x == y,
(Value::Float(x), Value::Float(y)) => x == y,
(Value::Str(x), Value::Str(y)) => x == y,
(Value::Bool(x), Value::Bool(y)) => x == y,
(Value::Nil, Value::Nil) => true,
_ => false,
}
}
/// Compare two values; returns negative / zero / positive like `Ord::cmp`.
fn cmp_values(a: &Value, b: &Value) -> Result<i32, String> {
match (a, b) {
(Value::Int(x), Value::Int(y)) => Ok(x.cmp(y) as i32),
(Value::Float(x), Value::Float(y)) => Ok(x.partial_cmp(y).map(|o| o as i32).unwrap_or(0)),
(Value::Str(x), Value::Str(y)) => Ok(x.cmp(y) as i32),
_ => Err(format!("Cannot compare {a:?} and {b:?}")),
}
}
// ── Arithmetic helpers ────────────────────────────────────────────────────────
fn arith_add(a: Value, b: Value) -> Result<Value, String> {
match (a, b) {
(Value::Int(x), Value::Int(y)) => Ok(Value::Int(x.wrapping_add(y))),
(Value::Float(x), Value::Float(y)) => Ok(Value::Float(x + y)),
(Value::Str(x), Value::Str(y)) => Ok(Value::Str(x + &y)),
(a, b) => Err(format!("ADD: type mismatch {a:?} + {b:?}")),
}
}
fn arith_sub(a: Value, b: Value) -> Result<Value, String> {
match (a, b) {
(Value::Int(x), Value::Int(y)) => Ok(Value::Int(x.wrapping_sub(y))),
(Value::Float(x), Value::Float(y)) => Ok(Value::Float(x - y)),
(a, b) => Err(format!("SUB: type mismatch {a:?} - {b:?}")),
}
}
fn arith_mul(a: Value, b: Value) -> Result<Value, String> {
match (a, b) {
(Value::Int(x), Value::Int(y)) => Ok(Value::Int(x.wrapping_mul(y))),
(Value::Float(x), Value::Float(y)) => Ok(Value::Float(x * y)),
(a, b) => Err(format!("MUL: type mismatch {a:?} * {b:?}")),
}
}
fn arith_div(a: Value, b: Value) -> Result<Value, String> {
match (a, b) {
(Value::Int(_), Value::Int(0)) => Err("Division by zero".to_string()),
(Value::Int(x), Value::Int(y)) => Ok(Value::Int(x / y)),
(Value::Float(x), Value::Float(y)) => Ok(Value::Float(x / y)),
(a, b) => Err(format!("DIV: type mismatch {a:?} / {b:?}")),
}
}
// ── Builtin function dispatch ─────────────────────────────────────────────────
fn call_builtin(name: &str, arity: u32, stack: &mut Vec<Value>) -> Result<Value, String> {
match name {
"__build_list__" => {
let n = arity as usize;
let start = stack.len().saturating_sub(n);
let items: Vec<Value> = stack.drain(start..).collect();
Ok(Value::List(items))
}
"print" | "println" => {
// In WASM, print is a no-op unless the host wires up a callback.
let n = arity as usize;
let start = stack.len().saturating_sub(n);
let _args: Vec<Value> = stack.drain(start..).collect();
Ok(Value::Nil)
}
"len" => {
let n = arity as usize;
let start = stack.len().saturating_sub(n);
let mut args: Vec<Value> = stack.drain(start..).collect();
let v = args.pop().unwrap_or(Value::Nil);
let len = match &v {
Value::List(items) => items.len() as i64,
Value::Str(s) => s.len() as i64,
Value::Map(pairs) => pairs.len() as i64,
_ => 0,
};
Ok(Value::Int(len))
}
_ => {
// Unknown function: consume args, return Nil.
let n = arity as usize;
let start = stack.len().saturating_sub(n);
let _: Vec<Value> = stack.drain(start..).collect();
Ok(Value::Nil)
}
}
}
// ── Tests ─────────────────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
use super::*;
// ── Compile pipeline tests ────────────────────────────────────────────────
#[test]
fn test_compile_source_produces_bytes() {
let bytes = compile_source_inner("42").unwrap();
assert!(!bytes.is_empty());
}
#[test]
fn test_roundtrip_bytecode_serialization() {
let source = "let x = 1 + 2";
let bytes = compile_source_inner(source).unwrap();
let (original, _) = compile_to_bytecode(source).unwrap();
let restored = deserialize_bytecode(&bytes).unwrap();
assert_eq!(original, restored);
}
#[test]
fn test_compile_function_def() {
let source = r#"fn add(a: Int, b: Int) -> Int { a + b }"#;
let bytes = compile_source_inner(source).unwrap();
assert!(!bytes.is_empty());
}
#[test]
fn test_compile_activate() {
let source = r#"activate User where "active users""#;
let (bytecode, _) = compile_to_bytecode(source).unwrap();
assert!(bytecode
.iter()
.any(|b| matches!(b, Bytecode::Activate { .. })));
}
#[test]
fn test_serialize_deserialize_activate() {
let source = r#"activate User where "query""#;
let bytes = compile_source_inner(source).unwrap();
let restored = deserialize_bytecode(&bytes).unwrap();
assert!(restored
.iter()
.any(|b| matches!(b, Bytecode::Activate { .. })));
}
#[test]
fn test_compile_sealed_block() {
let source = "sealed { let x = 1 }";
let (bytecode, _) = compile_to_bytecode(source).unwrap();
assert!(bytecode
.iter()
.any(|b| matches!(b, Bytecode::SealedBegin)));
}
#[test]
fn test_empty_program_compiles() {
let source = "";
let (bytecode, _) = compile_to_bytecode(source).unwrap();
assert!(matches!(bytecode.last(), Some(Bytecode::Halt)));
}
#[test]
fn test_complex_program_compiles() {
let source = r#"
let x = 10
let y = 20
let z = x + y
"#;
let (bytecode, _) = compile_to_bytecode(source).unwrap();
assert!(!bytecode.is_empty());
}
#[test]
fn test_bytecode_json_is_valid() {
let bytes = compile_source_inner("1 + 2").unwrap();
let json: serde_json::Value = serde_json::from_slice(&bytes).unwrap();
assert!(json.is_array());
}
#[test]
fn test_version_string() {
assert!(!env!("CARGO_PKG_VERSION").is_empty());
}
// ── VM execution tests ────────────────────────────────────────────────────
#[test]
fn test_run_integer_literal() {
let result = load_and_run_inner(&compile_source_inner("42").unwrap()).unwrap();
// The final value on the stack is the integer 42.
assert_eq!(result, "42");
}
#[test]
fn test_run_addition() {
let result = load_and_run_inner(&compile_source_inner("1 + 2").unwrap()).unwrap();
assert_eq!(result, "3");
}
#[test]
fn test_run_string_literal() {
let result =
load_and_run_inner(&compile_source_inner(r#""hello""#).unwrap()).unwrap();
assert_eq!(result, r#""hello""#);
}
#[test]
fn test_run_boolean() {
let result = load_and_run_inner(&compile_source_inner("true").unwrap()).unwrap();
assert_eq!(result, "true");
}
#[test]
fn test_run_let_binding_and_use() {
let source = "let x = 10\nx";
let result = load_and_run_inner(&compile_source_inner(source).unwrap()).unwrap();
assert_eq!(result, "10");
}
#[test]
fn test_run_arithmetic_chain() {
// 2 * 3 + 4 should be 10 (if parsed left-to-right)
let source = "2 * 3";
let result = load_and_run_inner(&compile_source_inner(source).unwrap()).unwrap();
assert_eq!(result, "6");
}
#[test]
fn test_run_activate_returns_list() {
let source = r#"activate User where "all""#;
let result = load_and_run_inner(&compile_source_inner(source).unwrap()).unwrap();
// Activate returns an empty list placeholder in the pure VM.
assert_eq!(result, "[]");
}
#[test]
fn test_run_if_true_branch() {
let source = "if true { 1 } else { 2 }";
let result = load_and_run_inner(&compile_source_inner(source).unwrap()).unwrap();
assert_eq!(result, "1");
}
#[test]
fn test_run_if_false_branch() {
let source = "if false { 1 } else { 2 }";
let result = load_and_run_inner(&compile_source_inner(source).unwrap()).unwrap();
assert_eq!(result, "2");
}
#[test]
fn test_run_comparison_eq() {
let result = load_and_run_inner(&compile_source_inner("1 == 1").unwrap()).unwrap();
assert_eq!(result, "true");
}
#[test]
fn test_run_comparison_neq() {
let result = load_and_run_inner(&compile_source_inner("1 != 2").unwrap()).unwrap();
assert_eq!(result, "true");
}
#[test]
fn test_direct_run_empty_bytecode() {
let result = run_bytecode(&[]).unwrap();
assert_eq!(result, Value::Nil);
}
#[test]
fn test_direct_run_halt_only() {
let result = run_bytecode(&[Bytecode::Halt]).unwrap();
assert_eq!(result, Value::Nil);
}
#[test]
fn test_direct_run_push_halt() {
let result = run_bytecode(&[Bytecode::Push(Value::Int(99)), Bytecode::Halt]).unwrap();
assert_eq!(result, Value::Int(99));
}
#[test]
fn test_direct_run_add() {
let bc = [
Bytecode::Push(Value::Int(3)),
Bytecode::Push(Value::Int(4)),
Bytecode::Add,
Bytecode::Halt,
];
let result = run_bytecode(&bc).unwrap();
assert_eq!(result, Value::Int(7));
}
#[test]
fn test_direct_run_string_concat() {
let bc = [
Bytecode::Push(Value::Str("hello ".to_string())),
Bytecode::Push(Value::Str("world".to_string())),
Bytecode::Add,
Bytecode::Halt,
];
let result = run_bytecode(&bc).unwrap();
assert_eq!(result, Value::Str("hello world".to_string()));
}
#[test]
fn test_direct_run_jump() {
// Jump over a push, land on the second push.
let bc = [
Bytecode::Jump(1), // ip=0 → skip 1 → ip becomes 2
Bytecode::Push(Value::Int(0)), // ip=1 — skipped
Bytecode::Push(Value::Int(42)), // ip=2
Bytecode::Halt,
];
let result = run_bytecode(&bc).unwrap();
assert_eq!(result, Value::Int(42));
}
}