Add browser REPL and MCP framing fixes
ober
69ae1ad7edfd9bdd01cae29eb01b79b37d784e50
--- a/.gitignore +++ b/.gitignore @@ -71,6 +71,7 @@ # Rust build artifacts jerboa-native-rs/target/ +browser-repl/target/ # Claude Code exports new.txt --- a/Makefile +++ b/Makefile @@ -1799,3 +1799,47 @@ docker-push: docker-build @echo "=== Pushing $(DOCKER_IMAGE) to Docker Hub ===" docker push $(DOCKER_IMAGE) @echo "Pushed $(DOCKER_IMAGE)" + +# ── Browser REPL WASM engine ──────────────────────────────────────────────── +BROWSER_REPL_MANIFEST := browser-repl/Cargo.toml +BROWSER_REPL_WASM := browser-repl/target/wasm32-unknown-unknown/release/jerboa_browser_repl.wasm +BROWSER_REPL_DIST := dist/browser-repl +BROWSER_REPL_CARGO := RUSTC=$$(rustup which rustc --toolchain 1.94.1) rustup run 1.94.1 cargo +BROWSER_REPL_RUSTFLAGS := -C link-arg=--max-memory=33554432 + +.PHONY: browser-repl-check browser-repl-wasm browser-repl-test browser-repl-artifact + +browser-repl-check: + $(BROWSER_REPL_CARGO) fmt --manifest-path $(BROWSER_REPL_MANIFEST) -- --check + $(BROWSER_REPL_CARGO) test --manifest-path $(BROWSER_REPL_MANIFEST) + +browser-repl-wasm: + RUSTFLAGS='$(BROWSER_REPL_RUSTFLAGS)' $(BROWSER_REPL_CARGO) build --manifest-path $(BROWSER_REPL_MANIFEST) --target wasm32-unknown-unknown --release + +browser-repl-test: browser-repl-check browser-repl-wasm + node tests/browser-repl-node-smoke.mjs $(BROWSER_REPL_WASM) + @test $$(wc -c < $(BROWSER_REPL_WASM)) -le 786432 + +browser-repl-artifact: browser-repl-test + @mkdir -p $(BROWSER_REPL_DIST) + @cp $(BROWSER_REPL_WASM) $(BROWSER_REPL_DIST)/jerboa-repl.wasm + @sha=$$(shasum -a 256 $(BROWSER_REPL_DIST)/jerboa-repl.wasm | awk '{print $$1}'); \ + bytes=$$(wc -c < $(BROWSER_REPL_DIST)/jerboa-repl.wasm | tr -d ' '); \ + commit=$$(git rev-parse HEAD); \ + version=$$(tr -d '[:space:]' < VERSION); \ + printf '%s\n' \ + '{' \ + ' "schema_version": 1,' \ + ' "abi_version": 1,' \ + ' "subset_revision": "browser-subset-1",' \ + " \"jerboa_version\": \"$$version\"," \ + " \"jerboa_commit\": \"$$commit\"," \ + ' "rust_toolchain": "1.94.1",' \ + ' "rust_target": "wasm32-unknown-unknown",' \ + ' "wasm_filename": "jerboa-repl.wasm",' \ + " \"wasm_sha256\": \"$$sha\"," \ + " \"wasm_bytes\": $$bytes," \ + ' "wasm_import_count": 0,' \ + ' "wasm_memory_max_bytes": 33554432' \ + '}' > $(BROWSER_REPL_DIST)/manifest.json; \ + printf '%s %s\n' "$$sha" jerboa-repl.wasm > $(BROWSER_REPL_DIST)/SHA256SUMS new file mode 100644 --- /dev/null +++ b/browser-repl/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "jerboa-browser-repl" +version = "0.1.0" new file mode 100644 --- /dev/null +++ b/browser-repl/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "jerboa-browser-repl" +version = "0.1.0" +edition = "2021" +publish = false + +[lib] +crate-type = ["cdylib", "rlib"] + +[profile.release] +opt-level = "s" +lto = true +codegen-units = 1 +panic = "abort" +strip = true + new file mode 100644 --- /dev/null +++ b/browser-repl/src/lib.rs @@ -0,0 +1,1614 @@ +use std::cell::{Cell, RefCell}; +use std::collections::BTreeMap; +use std::fmt::Write as _; +use std::rc::Rc; + +const ABI_VERSION: u32 = 1; +const MAX_SOURCE_BYTES: usize = 65_536; +const DEFAULT_STEPS: u32 = 250_000; +const HARD_MAX_STEPS: u32 = 1_000_000; +const MAX_DEPTH: u32 = 256; +const MAX_OUTPUT_BYTES: usize = 131_072; +const SUBSET_REVISION: &str = "browser-subset-1"; +const ENGINE_VERSION: &str = env!("CARGO_PKG_VERSION"); + +thread_local! { + static STATE: RefCell<State> = RefCell::new(State::new()); +} + +struct State { + input: Vec<u8>, + result: Vec<u8>, + session: Session, +} + +impl State { + fn new() -> Self { + Self { + input: Vec::new(), + result: Vec::new(), + session: Session::new(), + } + } +} + +#[no_mangle] +pub extern "C" fn repl_abi_version() -> u32 { + ABI_VERSION +} + +#[no_mangle] +pub extern "C" fn repl_input_alloc(len: u32) -> u32 { + let len = len as usize; + if len == 0 || len > MAX_SOURCE_BYTES { + return 0; + } + STATE.with(|state| { + let mut state = state.borrow_mut(); + state.result.clear(); + state.input.resize(len, 0); + state.input.as_mut_ptr() as u32 + }) +} + +#[no_mangle] +pub extern "C" fn repl_eval(len: u32, step_budget: u32) -> u32 { + STATE.with(|state| { + let mut state = state.borrow_mut(); + let len = len as usize; + if len == 0 || len > state.input.len() || len > MAX_SOURCE_BYTES { + state.result = envelope( + "reader-error", + &[], + 0, + Some(Error::new("reader", "invalid input length", None)), + ) + .into_bytes(); + return 2; + } + let budget = if step_budget == 0 { + DEFAULT_STEPS + } else { + step_budget.min(HARD_MAX_STEPS) + }; + let source = match std::str::from_utf8(&state.input[..len]) { + Ok(source) => source.to_owned(), + Err(_) => { + state.result = envelope( + "reader-error", + &[], + 0, + Some(Error::new("reader", "input is not valid UTF-8", None)), + ) + .into_bytes(); + return 2; + } + }; + let response = state.session.eval_source(&source, budget); + let code = response.code(); + state.result = response.to_json().into_bytes(); + code + }) +} + +#[no_mangle] +pub extern "C" fn repl_result_ptr() -> u32 { + STATE.with(|state| state.borrow().result.as_ptr() as u32) +} + +#[no_mangle] +pub extern "C" fn repl_result_len() -> u32 { + STATE.with(|state| state.borrow().result.len() as u32) +} + +#[no_mangle] +pub extern "C" fn repl_engine_info() -> u32 { + STATE.with(|state| { + let mut state = state.borrow_mut(); + let text = format!( + "{{\"abi\":{},\"status\":\"ok\",\"events\":[{{\"kind\":\"value\",\"text\":\"engine {} subset {} source_bytes {} default_steps {} max_steps {} depth {} output_bytes {}\"}}],\"steps\":0,\"error\":null}}", + ABI_VERSION, + json_text(ENGINE_VERSION), + json_text(SUBSET_REVISION), + MAX_SOURCE_BYTES, + DEFAULT_STEPS, + HARD_MAX_STEPS, + MAX_DEPTH, + MAX_OUTPUT_BYTES + ); + state.result = text.into_bytes(); + 0 + }) +} + +#[derive(Clone, Debug, PartialEq, Eq)] +enum Expr { + Bool(bool), + Int(i64), + Str(String), + Symbol(String), + List(Vec<Expr>), +} + +#[derive(Clone)] +enum Value { + Nil, + Bool(bool), + Int(i64), + Str(String), + Symbol(String), + Pair(Rc<(Value, Value)>), + Prim(fn(&[Value], &mut EvalContext) -> Result<Value, Error>), + Closure(Rc<Closure>), + Void, +} + +#[derive(Clone)] +struct Closure { + params: Vec<String>, + body: Vec<Expr>, + env: Env, +} + +type Env = Rc<Frame>; + +struct Frame { + parent: Option<Env>, + values: RefCell<BTreeMap<String, Value>>, + read_only: Cell<bool>, +} + +impl Frame { + fn new(parent: Option<Env>, read_only: bool) -> Env { + Rc::new(Self { + parent, + values: RefCell::new(BTreeMap::new()), + read_only: Cell::new(read_only), + }) + } + + fn define(&self, name: String, value: Value) -> Result<(), Error> { + if self.read_only.get() { + return Err(Error::eval("cannot modify read-only environment")); + } + self.values.borrow_mut().insert(name, value); + Ok(()) + } + + fn set(&self, name: &str, value: Value) -> Result<(), Error> { + if self.values.borrow().contains_key(name) { + if self.read_only.get() { + return Err(Error::eval("cannot modify read-only binding")); + } + self.values.borrow_mut().insert(name.to_owned(), value); + return Ok(()); + } + match &self.parent { + Some(parent) => parent.set(name, value), + None => Err(Error::eval(format!("unbound variable `{name}`"))), + } + } + + fn get(&self, name: &str) -> Option<Value> { + self.values + .borrow() + .get(name) + .cloned() + .or_else(|| self.parent.as_ref().and_then(|parent| parent.get(name))) + } +} + +struct Session { + env: Env, +} + +impl Session { + fn new() -> Self { + let prim = Frame::new(None, false); + install_primitives(&prim); + prim.read_only_set(); + Self { + env: Frame::new(Some(prim), false), + } + } + + fn eval_source(&mut self, source: &str, step_budget: u32) -> Response { + let forms = match Reader::new(source).read_all() { + Ok(forms) => forms, + Err(err) => return Response::new("reader-error", vec![], 0, Some(err)), + }; + let mut cx = EvalContext { + steps_left: step_budget, + steps_used: 0, + depth: 0, + events: Vec::new(), + stdout: String::new(), + }; + for form in forms { + match eval(&form, self.env.clone(), &mut cx) { + Ok(value) => { + if !cx.stdout.is_empty() { + cx.push_stdout_event(); + } + if !matches!(value, Value::Void) { + cx.events.push(Event::value(print_value(&value))); + } + } + Err(err) => { + if !cx.stdout.is_empty() { + cx.push_stdout_event(); + } + let status = if err.kind == "limit" { + "limit" + } else { + "eval-error" + }; + return Response::new(status, cx.events, cx.steps_used, Some(err)); + } + } + } + Response::new("ok", cx.events, cx.steps_used, None) + } +} + +impl Frame { + fn read_only_set(self: &Rc<Self>) { + self.read_only.set(true); + } +} + +struct EvalContext { + steps_left: u32, + steps_used: u32, + depth: u32, + events: Vec<Event>, + stdout: String, +} + +impl EvalContext { + fn step(&mut self) -> Result<(), Error> { + if self.steps_left == 0 { + return Err(Error::limit("evaluation step limit exceeded")); + } + self.steps_left -= 1; + self.steps_used += 1; + Ok(()) + } + + fn enter(&mut self) -> Result<(), Error> { + if self.depth >= MAX_DEPTH { + return Err(Error::limit("evaluation depth limit exceeded")); + } + self.depth += 1; + Ok(()) + } + + fn leave(&mut self) { + self.depth -= 1; + } + + fn write_stdout(&mut self, text: &str) -> Result<(), Error> { + if self.stdout.len() + text.len() > MAX_OUTPUT_BYTES { + return Err(Error::limit("captured stdout limit exceeded")); + } + self.stdout.push_str(text); + Ok(()) + } + + fn push_stdout_event(&mut self) { + let text = std::mem::take(&mut self.stdout); + self.events.push(Event::stdout(text)); + } +} + +fn eval(expr: &Expr, env: Env, cx: &mut EvalContext) -> Result<Value, Error> { + cx.step()?; + cx.enter()?; + let result = match expr { + Expr::Bool(value) => Ok(Value::Bool(*value)), + Expr::Int(value) => Ok(Value::Int(*value)), + Expr::Str(value) => Ok(Value::Str(value.clone())), + Expr::Symbol(name) => env + .get(name) + .ok_or_else(|| Error::eval(format!("unbound variable `{name}`"))), + Expr::List(items) => eval_list(items, env, cx), + }; + cx.leave(); + result +} + +fn eval_list(items: &[Expr], env: Env, cx: &mut EvalContext) -> Result<Value, Error> { + if items.is_empty() { + return Ok(Value::Nil); + } + let head = match &items[0] { + Expr::Symbol(name) => name.as_str(), + _ => "", + }; + match head { + "quote" => { + if items.len() != 2 { + return Err(Error::eval("quote expects one argument")); + } + datum_to_value(&items[1]) + } + "if" => { + if !(3..=4).contains(&items.len()) { + return Err(Error::eval("if expects two or three arguments")); + } + if truthy(&eval(&items[1], env.clone(), cx)?) { + eval(&items[2], env, cx) + } else if items.len() == 4 { + eval(&items[3], env, cx) + } else { + Ok(Value::Void) + } + } + "begin" => eval_sequence(&items[1..], env, cx), + "lambda" => { + if items.len() < 3 { + return Err(Error::eval("lambda expects parameters and body")); + } + let params = parse_params(&items[1])?; + Ok(Value::Closure(Rc::new(Closure { + params, + body: items[2..].to_vec(), + env, + }))) + } + "def" | "define" => eval_define(items, env, cx), + "defn" => eval_defn(items, env), + "set!" => { + if items.len() != 3 { + return Err(Error::eval("set! expects a name and value")); + } + let name = expect_symbol(&items[1], "set!")?; + let value = eval(&items[2], env.clone(), cx)?; + env.set(name, value)?; + Ok(Value::Void) + } + "let" => eval_let(items, env, cx, false), + "let*" => eval_let(items, env, cx, true), + "cond" => eval_cond(&items[1..], env, cx), + "and" => { + let mut last = Value::Bool(true); + for expr in &items[1..] { + last = eval(expr, env.clone(), cx)?; + if !truthy(&last) { + return Ok(Value::Bool(false)); + } + } + Ok(last) + } + "or" => { + for expr in &items[1..] { + let value = eval(expr, env.clone(), cx)?; + if truthy(&value) { + return Ok(value); + } + } + Ok(Value::Bool(false)) + } + "when" => { + if items.len() < 3 { + return Err(Error::eval("when expects a test and body")); + } + if truthy(&eval(&items[1], env.clone(), cx)?) { + eval_sequence(&items[2..], env, cx) + } else { + Ok(Value::Void) + } + } + "unless" => { + if items.len() < 3 { + return Err(Error::eval("unless expects a test and body")); + } + if !truthy(&eval(&items[1], env.clone(), cx)?) { + eval_sequence(&items[2..], env, cx) + } else { + Ok(Value::Void) + } + } + "import" => { + if items.len() >= 2 && items[1..].iter().all(matches_browser_subset_import) { + Ok(Value::Void) + } else { + Err(Error::eval( + "unsupported in browser subset: only (jerboa prelude) and (std clojure) imports are available", + )) + } + } + _ => { + let op = eval(&items[0], env.clone(), cx)?; + let mut args = Vec::with_capacity(items.len().saturating_sub(1)); + for item in &items[1..] { + args.push(eval(item, env.clone(), cx)?); + } + apply(&op, &args, cx) + } + } +} + +fn eval_sequence(body: &[Expr], env: Env, cx: &mut EvalContext) -> Result<Value, Error> { + let mut value = Value::Void; + for expr in body { + value = eval(expr, env.clone(), cx)?; + } + Ok(value) +} + +fn eval_define(items: &[Expr], env: Env, cx: &mut EvalContext) -> Result<Value, Error> { + if items.len() < 3 { + return Err(Error::eval("def expects a name and value or function body")); + } + match &items[1] { + Expr::Symbol(name) => { + if items.len() != 3 { + return Err(Error::eval("variable def expects exactly one value")); + } + let value = eval(&items[2], env.clone(), cx)?; + env.define(name.clone(), value)?; + Ok(Value::Void) + } + Expr::List(parts) if !parts.is_empty() => { + let name = expect_symbol(&parts[0], "def")?.to_owned(); + let mut params = Vec::new(); + for part in &parts[1..] { + params.push(expect_symbol(part, "def parameter")?.to_owned()); + } + reject_duplicate_params(¶ms)?; + let closure = Value::Closure(Rc::new(Closure { + params, + body: items[2..].to_vec(), + env: env.clone(), + })); + env.define(name, closure)?; + Ok(Value::Void) + } + _ => Err(Error::eval("malformed def")), + } +} + +fn eval_defn(items: &[Expr], env: Env) -> Result<Value, Error> { + if items.len() < 4 { + return Err(Error::eval("defn expects a name, parameters, and body")); + } + let name = expect_symbol(&items[1], "defn")?.to_owned(); + let params = parse_params(&items[2])?; + reject_duplicate_params(¶ms)?; + let closure = Value::Closure(Rc::new(Closure { + params, + body: items[3..].to_vec(), + env: env.clone(), + })); + env.define(name, closure)?; + Ok(Value::Void) +} + +fn eval_let( + items: &[Expr], + env: Env, + cx: &mut EvalContext, + sequential: bool, +) -> Result<Value, Error> { + if items.len() < 3 { + return Err(Error::eval("let expects bindings and body")); + } + let bindings = match &items[1] { + Expr::List(bindings) => bindings, + _ => return Err(Error::eval("let bindings must be a list")), + }; + let child = Frame::new(Some(env.clone()), false); + for binding in bindings { + let pair = match binding { + Expr::List(pair) if pair.len() == 2 => pair, + _ => return Err(Error::eval("let binding must be (name value)")), + }; + let name = expect_symbol(&pair[0], "let binding")?.to_owned(); + let source_env = if sequential { + child.clone() + } else { + env.clone() + }; + let value = eval(&pair[1], source_env, cx)?; + child.define(name, value)?; + } + eval_sequence(&items[2..], child, cx) +} + +fn eval_cond(clauses: &[Expr], env: Env, cx: &mut EvalContext) -> Result<Value, Error> { + for clause in clauses { + let parts = match clause { + Expr::List(parts) if !parts.is_empty() => parts, + _ => return Err(Error::eval("cond clause must be a non-empty list")), + }; + if matches!(&parts[0], Expr::Symbol(s) if s == "else") { + return eval_sequence(&parts[1..], env, cx); + } + let test = eval(&parts[0], env.clone(), cx)?; + if truthy(&test) { + return if parts.len() == 1 { + Ok(test) + } else { + eval_sequence(&parts[1..], env, cx) + }; + } + } + Ok(Value::Void) +} + +fn apply(op: &Value, args: &[Value], cx: &mut EvalContext) -> Result<Value, Error> { + cx.step()?; + match op { + Value::Prim(f) => f(args, cx), + Value::Closure(closure) => { + if closure.params.len() != args.len() { + return Err(Error::eval(format!( + "arity mismatch: expected {}, got {}", + closure.params.len(), + args.len() + ))); + } + let frame = Frame::new(Some(closure.env.clone()), false); + for (param, arg) in closure.params.iter().zip(args.iter()) { + frame.define(param.clone(), arg.clone())?; + } + eval_sequence(&closure.body, frame, cx) + } + _ => Err(Error::eval("attempted to call a non-procedure")), + } +} + +fn install_primitives(env: &Env) { + let mut map = env.values.borrow_mut(); + macro_rules! prim { + ($name:literal, $func:ident) => { + map.insert($name.to_owned(), Value::Prim($func)); + }; + } + prim!("+", prim_add); + prim!("-", prim_sub); + prim!("*", prim_mul); + prim!("quotient", prim_quotient); + prim!("remainder", prim_remainder); + prim!("modulo", prim_modulo); + prim!("abs", prim_abs); + prim!("zero?", prim_zero); + prim!("even?", prim_even); + prim!("odd?", prim_odd); + prim!("inc", prim_inc); + prim!("dec", prim_dec); + prim!("=", prim_num_eq); + prim!("<", prim_lt); + prim!("<=", prim_lte); + prim!(">", prim_gt); + prim!(">=", prim_gte); + prim!("boolean?", prim_boolean_p); + prim!("number?", prim_number_p); + prim!("pair?", prim_pair_p); + prim!("null?", prim_null_p); + prim!("list?", prim_list_p); + prim!("string?", prim_string_p); + prim!("symbol?", prim_symbol_p); + prim!("procedure?", prim_procedure_p); + prim!("not", prim_not); + prim!("cons", prim_cons); + prim!("car", prim_car); + prim!("cdr", prim_cdr); + prim!("first", prim_car); + prim!("rest", prim_cdr); + prim!("list", prim_list); + prim!("length", prim_length); + prim!("count", prim_length); + prim!("append", prim_append); + prim!("reverse", prim_reverse); + prim!("list-ref", prim_list_ref); + prim!("map", prim_map); + prim!("filter", prim_filter); + prim!("foldl", prim_foldl); + prim!("reduce", prim_reduce); + prim!("eq?", prim_equal); + prim!("eqv?", prim_equal); + prim!("equal?", prim_equal); + prim!("str", prim_str); + prim!("string-append", prim_string_append); + prim!("string-length", prim_string_length); + prim!("string=?", prim_string_eq); + prim!("string<?", prim_string_lt); + prim!("substring", prim_substring); + prim!("number->string", prim_number_to_string); + prim!("symbol->string", prim_symbol_to_string); + prim!("string->symbol", prim_string_to_symbol); + prim!("display", prim_display); + prim!("write", prim_write); + prim!("displayln", prim_displayln); + prim!("newline", prim_newline); + prim!("error", prim_error); + prim!("assert!", prim_assert); +} + +macro_rules! arity { + ($args:expr, $n:expr, $name:literal) => { + if $args.len() != $n { + return Err(Error::eval(format!("{} expects {} arguments", $name, $n))); + } + }; +} + +fn expect_int(value: &Value, name: &str) -> Result<i64, Error> { + match value { + Value::Int(n) => Ok(*n), + _ => Err(Error::eval(format!("{name} expects integers"))), + } +} + +fn checked_bin( + args: &[Value], + name: &str, + mut f: impl FnMut(i64, i64) -> Option<i64>, +) -> Result<Value, Error> { + if args.is_empty() { + return Err(Error::eval(format!("{name} expects at least one argument"))); + } + let mut acc = expect_int(&args[0], name)?; + for arg in &args[1..] { + acc = f(acc, expect_int(arg, name)?).ok_or_else(|| Error::eval("integer overflow"))?; + } + Ok(Value::Int(acc)) +} + +fn prim_add(args: &[Value], _: &mut EvalContext) -> Result<Value, Error> { + let mut acc = 0i64; + for arg in args { + acc = acc + .checked_add(expect_int(arg, "+")?) + .ok_or_else(|| Error::eval("integer overflow"))?; + } + Ok(Value::Int(acc)) +} + +fn prim_sub(args: &[Value], _: &mut EvalContext) -> Result<Value, Error> { + if args.len() == 1 { + return Ok(Value::Int( + expect_int(&args[0], "-")? + .checked_neg() + .ok_or_else(|| Error::eval("integer overflow"))?, + )); + } + checked_bin(args, "-", i64::checked_sub) +} + +fn prim_mul(args: &[Value], _: &mut EvalContext) -> Result<Value, Error> { + let mut acc = 1i64; + for arg in args { + acc = acc + .checked_mul(expect_int(arg, "*")?) + .ok_or_else(|| Error::eval("integer overflow"))?; + } + Ok(Value::Int(acc)) +} + +fn prim_quotient(args: &[Value], _: &mut EvalContext) -> Result<Value, Error> { + arity!(args, 2, "quotient"); + let rhs = expect_int(&args[1], "quotient")?; + if rhs == 0 { + return Err(Error::eval("division by zero")); + } + Ok(Value::Int(expect_int(&args[0], "quotient")? / rhs)) +} + +fn prim_remainder(args: &[Value], _: &mut EvalContext) -> Result<Value, Error> { + arity!(args, 2, "remainder"); + let rhs = expect_int(&args[1], "remainder")?; + if rhs == 0 { + return Err(Error::eval("division by zero")); + } + Ok(Value::Int(expect_int(&args[0], "remainder")? % rhs)) +} + +fn prim_modulo(args: &[Value], cx: &mut EvalContext) -> Result<Value, Error> { + prim_remainder(args, cx) +} + +fn prim_abs(args: &[Value], _: &mut EvalContext) -> Result<Value, Error> { + arity!(args, 1, "abs"); + Ok(Value::Int( + expect_int(&args[0], "abs")? + .checked_abs() + .ok_or_else(|| Error::eval("integer overflow"))?, + )) +} + +fn prim_zero(args: &[Value], _: &mut EvalContext) -> Result<Value, Error> { + arity!(args, 1, "zero?"); + Ok(Value::Bool(expect_int(&args[0], "zero?")? == 0)) +} + +fn prim_even(args: &[Value], _: &mut EvalContext) -> Result<Value, Error> { + arity!(args, 1, "even?"); + Ok(Value::Bool(expect_int(&args[0], "even?")? % 2 == 0)) +} + +fn prim_odd(args: &[Value], _: &mut EvalContext) -> Result<Value, Error> { + arity!(args, 1, "odd?"); + Ok(Value::Bool(expect_int(&args[0], "odd?")? % 2 != 0)) +} + +fn prim_inc(args: &[Value], _: &mut EvalContext) -> Result<Value, Error> { + arity!(args, 1, "inc"); + Ok(Value::Int( + expect_int(&args[0], "inc")? + .checked_add(1) + .ok_or_else(|| Error::eval("integer overflow"))?, + )) +} + +fn prim_dec(args: &[Value], _: &mut EvalContext) -> Result<Value, Error> { + arity!(args, 1, "dec"); + Ok(Value::Int( + expect_int(&args[0], "dec")? + .checked_sub(1) + .ok_or_else(|| Error::eval("integer overflow"))?, + )) +} + +fn cmp(args: &[Value], name: &str, f: impl Fn(i64, i64) -> bool) -> Result<Value, Error> { + if args.len() < 2 { + return Ok(Value::Bool(true)); + } + for pair in args.windows(2) { + if !f(expect_int(&pair[0], name)?, expect_int(&pair[1], name)?) { + return Ok(Value::Bool(false)); + } + } + Ok(Value::Bool(true)) +} + +fn prim_num_eq(args: &[Value], _: &mut EvalContext) -> Result<Value, Error> { + cmp(args, "=", |a, b| a == b) +} +fn prim_lt(args: &[Value], _: &mut EvalContext) -> Result<Value, Error> { + cmp(args, "<", |a, b| a < b) +} +fn prim_lte(args: &[Value], _: &mut EvalContext) -> Result<Value, Error> { + cmp(args, "<=", |a, b| a <= b) +} +fn prim_gt(args: &[Value], _: &mut EvalContext) -> Result<Value, Error> { + cmp(args, ">", |a, b| a > b) +} +fn prim_gte(args: &[Value], _: &mut EvalContext) -> Result<Value, Error> { + cmp(args, ">=", |a, b| a >= b) +} + +fn prim_boolean_p(args: &[Value], _: &mut EvalContext) -> Result<Value, Error> { + arity!(args, 1, "boolean?"); + Ok(Value::Bool(matches!(args[0], Value::Bool(_)))) +} +fn prim_number_p(args: &[Value], _: &mut EvalContext) -> Result<Value, Error> { + arity!(args, 1, "number?"); + Ok(Value::Bool(matches!(args[0], Value::Int(_)))) +} +fn prim_pair_p(args: &[Value], _: &mut EvalContext) -> Result<Value, Error> { + arity!(args, 1, "pair?"); + Ok(Value::Bool(matches!(args[0], Value::Pair(_)))) +} +fn prim_null_p(args: &[Value], _: &mut EvalContext) -> Result<Value, Error> { + arity!(args, 1, "null?"); + Ok(Value::Bool(matches!(args[0], Value::Nil))) +} +fn prim_list_p(args: &[Value], _: &mut EvalContext) -> Result<Value, Error> { + arity!(args, 1, "list?"); + Ok(Value::Bool(list_to_vec(&args[0]).is_ok())) +} +fn prim_string_p(args: &[Value], _: &mut EvalContext) -> Result<Value, Error> { + arity!(args, 1, "string?"); + Ok(Value::Bool(matches!(args[0], Value::Str(_)))) +} +fn prim_symbol_p(args: &[Value], _: &mut EvalContext) -> Result<Value, Error> { + arity!(args, 1, "symbol?"); + Ok(Value::Bool(matches!(args[0], Value::Symbol(_)))) +} +fn prim_procedure_p(args: &[Value], _: &mut EvalContext) -> Result<Value, Error> { + arity!(args, 1, "procedure?"); + Ok(Value::Bool(matches!( + args[0], + Value::Prim(_) | Value::Closure(_) + ))) +} +fn prim_not(args: &[Value], _: &mut EvalContext) -> Result<Value, Error> { + arity!(args, 1, "not"); + Ok(Value::Bool(!truthy(&args[0]))) +} + +fn prim_cons(args: &[Value], _: &mut EvalContext) -> Result<Value, Error> { + arity!(args, 2, "cons"); + Ok(Value::Pair(Rc::new((args[0].clone(), args[1].clone())))) +} + +fn prim_car(args: &[Value], _: &mut EvalContext) -> Result<Value, Error> { + arity!(args, 1, "car"); + match &args[0] { + Value::Pair(pair) => Ok(pair.0.clone()), + _ => Err(Error::eval("car expects a pair")), + } +} + +fn prim_cdr(args: &[Value], _: &mut EvalContext) -> Result<Value, Error> { + arity!(args, 1, "cdr"); + match &args[0] { + Value::Pair(pair) => Ok(pair.1.clone()), + _ => Err(Error::eval("cdr expects a pair")), + } +} + +fn prim_list(args: &[Value], _: &mut EvalContext) -> Result<Value, Error> { + Ok(vec_to_list(args)) +} + +fn prim_length(args: &[Value], _: &mut EvalContext) -> Result<Value, Error> { + arity!(args, 1, "length"); + Ok(Value::Int(list_to_vec(&args[0])?.len() as i64)) +} + +fn prim_append(args: &[Value], _: &mut EvalContext) -> Result<Value, Error> { + let mut out = Vec::new(); + for arg in args { + out.extend(list_to_vec(arg)?); + } + Ok(vec_to_list(&out)) +} + +fn prim_reverse(args: &[Value], _: &mut EvalContext) -> Result<Value, Error> { + arity!(args, 1, "reverse"); + let mut values = list_to_vec(&args[0])?; + values.reverse(); + Ok(vec_to_list(&values)) +} + +fn prim_list_ref(args: &[Value], _: &mut EvalContext) -> Result<Value, Error> { + arity!(args, 2, "list-ref"); + let idx = expect_int(&args[1], "list-ref")?; + if idx < 0 { + return Err(Error::eval("list-ref index is negative")); + } + list_to_vec(&args[0])? + .get(idx as usize) + .cloned() + .ok_or_else(|| Error::eval("list-ref index out of range")) +} + +fn prim_map(args: &[Value], cx: &mut EvalContext) -> Result<Value, Error> { + arity!(args, 2, "map"); + let proc = args[0].clone(); + let values = list_to_vec(&args[1])?; + let mut out = Vec::with_capacity(values.len()); + for value in values { + cx.step()?; + out.push(apply(&proc, &[value], cx)?); + } + Ok(vec_to_list(&out)) +} + +fn prim_filter(args: &[Value], cx: &mut EvalContext) -> Result<Value, Error> { + arity!(args, 2, "filter"); + let proc = args[0].clone(); + let mut out = Vec::new(); + for value in list_to_vec(&args[1])? { + cx.step()?; + if truthy(&apply(&proc, &[value.clone()], cx)?) { + out.push(value); + } + } + Ok(vec_to_list(&out)) +} + +fn prim_foldl(args: &[Value], cx: &mut EvalContext) -> Result<Value, Error> { + arity!(args, 3, "foldl"); + let proc = args[0].clone(); + let mut acc = args[1].clone(); + for value in list_to_vec(&args[2])? { + cx.step()?; + acc = apply(&proc, &[value, acc], cx)?; + }