pub(crate) mod budget; pub(crate) mod cek; pub(crate) mod convert; pub(crate) mod ctx; pub(crate) mod data; pub mod intern; pub mod module; pub mod nanvalue; pub(crate) mod number; pub mod parse; pub(crate) mod procs; mod scope; pub use scope::Scope; pub use budget::Budget; pub use data::{Array, HeapArr, HeapObj, NumTag, Object, String, Value, pattern_bound_names, pattern_bound_names_ordered}; pub use module::{Module, ModuleType, Engine, NumericTower}; pub use number::{Number, IntegerValue, FloatValue, NumFormat, set_numeric_format, IntegerModel, FloatModel, OverflowBehavior}; pub use procs::try_match_simple; #[cfg(test)] mod tests { use super::{Runtime, RuntimeError}; fn run(src: &str) -> String { let mut rt = Runtime::new(1024 * 1024, 100_000); rt.run_json(src).expect("run failed").to_string() } #[test] fn runtime_arithmetic_addition() { assert_eq!(run(r#"{"+":[1,2,3]}"#), "6"); } #[test] fn runtime_if_true_branch() { assert_eq!(run(r#"{"if":[true,"yes","no"]}"#), r#""yes""#); } #[test] fn runtime_if_false_branch() { assert_eq!(run(r#"{"if":[false,"yes","no"]}"#), r#""no""#); } #[test] fn runtime_not() { assert_eq!(run(r#"{"not":[false]}"#), "true"); } #[test] fn runtime_string_length() { assert_eq!(run(r#"{"str-length":"hello"}"#), "5"); } #[test] fn runtime_anonymous_function_call() { assert_eq!( run(r#"{"apply":[{"fn":[[{"var":"x"}],{"+": [{"var":"x"},1]}]},[41]]}"#), "42" ); } #[test] fn runtime_named_recursive_factorial() { assert_eq!( run(r#"{"letrec":[{"fact":{"fn":[[{"var":"n"}],{"if":[{"==":[{"var":"n"},0]},1,{"*":[{"var":"n"},{"fact":[{"-":[{"var":"n"},1]}]}]}]}]}},[{"fact":[5]}]]}"#), "[120]" ); } #[test] fn runtime_let_star_binding() { assert_eq!(run(r#"{"let*":[[["x",7]],{"+":[{"var":"x"},3]}]}"#), "10"); } #[test] fn runtime_eval_limit_exceeded() { let err = Runtime::new(1024 * 1024, 100) .run_json(r#"{"letrec":[{"loop":{"fn":[[],{"loop":[]}]}},[{"loop":[]}]]}"#) .unwrap_err(); assert!(matches!( err, crate::Error::Runtime(RuntimeError::EvalLimit) )); } #[test] fn runtime_memory_limit_exceeded() { let err = Runtime::new(4, 10_000) .run_json(r#"{"str-join":[["aaaaaaaaaaaaaaaaaaaaaaaaa","bbbbbbbbbbbbbbbbbbbbbbbbb"],"xxxxxxxxxxxxxxxxxxxxxxxxx"]}"#) .unwrap_err(); assert!(matches!( err, crate::Error::Runtime(RuntimeError::MemoryLimit) )); } #[test] fn runtime_run_with_args() { let budget = super::Budget::new(usize::MAX / 2); let prog_val = super::parse::parse_json( r#"{"+":[{"nth":[{"var":"args"},0]},{"nth":[{"var":"args"},1]}]}"#, &budget, ) .unwrap(); let args_val = super::parse::parse_json("[10,32]", &budget).unwrap(); let extra = [("args".into(), args_val)].into_iter().collect(); let result = Runtime::new(1024 * 1024, 10_000) .run_value_with_extra(prog_val, extra) .expect("run_with_args failed"); assert_eq!(result.to_string(), "42"); } // ── Arithmetic ──────────────────────────────────────────────────────────── #[test] fn runtime_arithmetic_subtraction() { assert_eq!(run(r#"{"-":[10,3]}"#), "7"); } #[test] fn runtime_arithmetic_multiplication() { assert_eq!(run(r#"{"*":[3,4]}"#), "12"); } #[test] fn runtime_arithmetic_division() { // Division always returns float (not closed over integers) assert_eq!(run(r#"{"/":[10,2]}"#), "5.0"); } #[test] fn runtime_arithmetic_modulus() { assert_eq!(run(r#"{"%":[10,3]}"#), "1"); } // ── Comparison ──────────────────────────────────────────────────────────── #[test] fn runtime_eq_neq() { assert_eq!(run(r#"{"==":[1,1]}"#), "true"); assert_eq!(run(r#"{"==":[1,2]}"#), "false"); assert_eq!(run(r#"{"!=":[1,2]}"#), "true"); assert_eq!(run(r#"{"!=":[1,1]}"#), "false"); } #[test] fn runtime_ordering() { assert_eq!(run(r#"{"<":[1,2]}"#), "true"); assert_eq!(run(r#"{"<":[2,2]}"#), "false"); assert_eq!(run(r#"{"<=":[2,2]}"#), "true"); assert_eq!(run(r#"{">":[3,2]}"#), "true"); assert_eq!(run(r#"{">=":[2,2]}"#), "true"); } // ── Math ────────────────────────────────────────────────────────────────── #[test] fn runtime_abs() { assert_eq!(run(r#"{"abs":[{"-":[0,5]}]}"#), "5"); assert_eq!(run(r#"{"abs":[3]}"#), "3"); } #[test] fn runtime_floor_ceil() { assert_eq!(run(r#"{"floor":[3.7]}"#), "3"); assert_eq!(run(r#"{"ceil":[3.2]}"#), "4"); } #[test] fn runtime_round() { assert_eq!(run(r#"{"round":[2.5]}"#), "3"); assert_eq!(run(r#"{"round":[2.4]}"#), "2"); } #[test] fn runtime_sqrt() { assert_eq!(run(r#"{"sqrt":[4.0]}"#), "2.0"); } #[test] fn runtime_pow() { assert_eq!(run(r#"{"pow":[2,10]}"#), "1024"); } #[test] fn runtime_min_max() { assert_eq!(run(r#"{"min":[3,1,4,1,5]}"#), "1"); assert_eq!(run(r#"{"max":[3,1,4,1,5]}"#), "5"); } // ── Type predicates ─────────────────────────────────────────────────────── #[test] fn runtime_null_pred() { assert_eq!(run(r#"{"null?":null}"#), "true"); assert_eq!(run(r#"{"null?":false}"#), "false"); } #[test] fn runtime_bool_pred() { assert_eq!(run(r#"{"bool?":true}"#), "true"); assert_eq!(run(r#"{"bool?":42}"#), "false"); } #[test] fn runtime_number_pred() { assert_eq!(run(r#"{"number?":42}"#), "true"); assert_eq!(run(r#"{"number?":"hello"}"#), "false"); } #[test] fn runtime_string_pred() { assert_eq!(run(r#"{"string?":"hi"}"#), "true"); assert_eq!(run(r#"{"string?":0}"#), "false"); } #[test] fn runtime_array_pred() { assert_eq!(run(r#"{"array?":[[1,2]]}"#), "true"); assert_eq!(run(r#"{"array?":42}"#), "false"); } #[test] fn runtime_object_pred() { assert_eq!( run(r#"{"let*":[[["obj",{"a":1,"b":2}]],{"object?":[{"var":"obj"}]}]}"#), "true" ); assert_eq!(run(r#"{"object?":42}"#), "false"); } #[test] fn runtime_proc_pred() { assert_eq!(run(r#"{"proc?":[{"var":"+"}]}"#), "true"); assert_eq!(run(r#"{"proc?":42}"#), "false"); } // ── Array ops ───────────────────────────────────────────────────────────── #[test] fn runtime_count() { assert_eq!(run(r#"{"count":[[1,2,3]]}"#), "3"); assert_eq!(run(r#"{"count":[[]]}"#), "0"); } #[test] fn runtime_nth() { assert_eq!(run(r#"{"nth":[[10,20,30],0]}"#), "10"); assert_eq!(run(r#"{"nth":[[10,20,30],2]}"#), "30"); } #[test] fn runtime_first_rest() { assert_eq!(run(r#"{"first":[[1,2,3]]}"#), "1"); assert_eq!(run(r#"{"rest":[[1,2,3]]}"#), "[2,3]"); assert_eq!(run(r#"{"rest":[[42]]}"#), "[]"); } #[test] fn runtime_cons() { assert_eq!(run(r#"{"cons":[0,[1,2]]}"#), "[0,1,2]"); assert_eq!(run(r#"{"cons":[99,[]]}"#), "[99]"); } #[test] fn runtime_concat() { assert_eq!(run(r#"{"concat":[[1,2],[3,4]]}"#), "[1,2,3,4]"); assert_eq!(run(r#"{"concat":[[],[1]]}"#), "[1]"); } #[test] fn runtime_slice() { assert_eq!(run(r#"{"slice":[[0,1,2,3],2]}"#), "[2,3]"); assert_eq!(run(r#"{"slice":[[0,1,2,3],1,3]}"#), "[1,2]"); } // ── Object ops ──────────────────────────────────────────────────────────── #[test] fn runtime_keys_values() { assert_eq!(run(r#"{"count":[{"keys":[{"a":1,"b":2}]}]}"#), "2"); assert_eq!(run(r#"{"count":[{"values":[{"a":1,"b":2}]}]}"#), "2"); } #[test] fn runtime_has() { assert_eq!(run(r#"{"has?":[{"a":1,"b":2},"a"]}"#), "true"); assert_eq!(run(r#"{"has?":[{"a":1,"b":2},"z"]}"#), "false"); } #[test] fn runtime_assoc() { assert_eq!(run(r#"{"has?":[{"assoc":[{"a":1,"b":2},"c",3]},"c"]}"#), "true"); } #[test] fn runtime_dissoc() { assert_eq!(run(r#"{"has?":[{"dissoc":[{"a":1,"b":2},"a"]},"a"]}"#), "false"); } #[test] fn runtime_dissoc_last_key_produces_empty_object() { // Removing the only key from a 2-key object leaves a 1-key object, // which round-trips through to_output as a single-pair object. // Removing the last key from that should yield an empty object {}. assert_eq!(run(r#"{"dissoc":[{"dissoc":[{"a":1,"b":2},"a"]},"b"]}"#), "{}"); } #[test] fn runtime_merge() { assert_eq!( run(r#"{"count":[{"keys":[{"merge":[{"a":1,"b":2},{"c":3,"d":4}]}]}]}"#), "4" ); } // ── String ops ──────────────────────────────────────────────────────────── #[test] fn runtime_str_upper_lower() { assert_eq!(run(r#"{"str-upper":"hello"}"#), r#""HELLO""#); assert_eq!(run(r#"{"str-lower":"WORLD"}"#), r#""world""#); } #[test] fn runtime_str_trim() { assert_eq!(run(r#"{"str-trim":" hi "}"#), r#""hi""#); } #[test] fn runtime_str_contains() { assert_eq!(run(r#"{"str-contains?":["hello world","world"]}"#), "true"); assert_eq!(run(r#"{"str-contains?":["hello world","xyz"]}"#), "false"); } #[test] fn runtime_str_starts_ends_with() { assert_eq!(run(r#"{"str-starts-with?":["hello","he"]}"#), "true"); assert_eq!(run(r#"{"str-ends-with?":["hello","lo"]}"#), "true"); } #[test] fn runtime_str_slice() { assert_eq!(run(r#"{"str-slice":["hello",1,3]}"#), r#""el""#); } #[test] fn runtime_str_split_join() { assert_eq!(run(r#"{"str-split":["a,b,c",","]}"#), r#"["a","b","c"]"#); assert_eq!(run(r#"{"str-join":[["x","y","z"],"-"]}"#), r#""x-y-z""#); } #[test] fn runtime_str_replace() { assert_eq!(run(r#"{"str-replace":["hello world","world","Rust"]}"#), r#""hello Rust""#); } #[test] fn runtime_str_index_of() { assert_eq!(run(r#"{"str-index-of":["hello","ll"]}"#), "2"); assert_eq!(run(r#"{"str-index-of":["hello","xyz"]}"#), "null"); } // ── Control flow ────────────────────────────────────────────────────────── #[test] fn runtime_eval() { assert_eq!(run(r#"{"eval":[{"if":[true,42,0]}]}"#), "42"); } #[test] fn runtime_let_two_bindings() { assert_eq!( run(r#"{"let":[{"x":3,"y":4},{"*":[{"var":"x"},{"var":"y"}]}]}"#), "12" ); } #[test] fn runtime_let_single_binding() { // {"x": 42} is a singleton object in the AST; let treats it as one binding. assert_eq!(run(r#"{"let":[{"x":42},{"var":"x"}]}"#), "42"); } #[test] fn runtime_letrec_mutual_recursion() { assert_eq!( run(r#"{"letrec":[{"even":{"fn":[[{"var":"n"}],{"if":[{"==":[{"var":"n"},0]},true,{"odd":[{"-":[{"var":"n"},1]}]}]}]},"odd":{"fn":[[{"var":"n"}],{"if":[{"==":[{"var":"n"},0]},false,{"even":[{"-":[{"var":"n"},1]}]}]}]}},[{"apply":[{"var":"even"},[4]]},{"apply":[{"var":"odd"},[7]]}]]}"#), "[true,true]" ); } #[test] fn runtime_vau_receives_unevaluated_arg() { // vau (operative) returns args unevaluated assert_eq!( run(r#"{"apply":[{"vau":[[{"var":"x"}],{"var":"x"}]},[{"not":[false]}]]}"#), r#"{"not":[false]}"# ); } #[test] fn runtime_macro_auto_evals_return() { // macro (= vau + eval return) auto-evaluates the return in caller's env assert_eq!( run(r#"{"apply":[{"macro":[[{"var":"x"}],{"var":"x"}]},[{"not":[false]}]]}"#), "true" ); } // ── var path traversal ──────────────────────────────────────────────────── #[test] fn runtime_var_path_object() { assert_eq!( run(r#"{"let*":[[["obj",{"a":1,"b":99}]],{"var":["obj","b"]}]}"#), "99" ); } #[test] fn runtime_var_path_array() { assert_eq!( run(r#"{"let*":[[["arr",[10,20,30]]],{"var":["arr",2]}]}"#), "30" ); } } use std::fmt; // ── Eval enum ───────────────────────────────────────────────────────────────── /// Result of a single evaluation step. Either a final value or a deferred /// tail call that the trampoline loop will resolve. pub(crate) enum Eval { /// Evaluation complete. Done(Value), /// Tail call: evaluate this value in this scope. TailEval { value: Value, scope: Scope }, /// Yield to the CEK machine: evaluate `expr` in `scope`, then call `resume` /// with the result. Builtins return this instead of calling `eval_trampoline`. Yield { expr: Value, scope: Scope, resume: Box crate::Result>, }, } impl Eval { pub(crate) fn done(v: Value) -> crate::Result { Ok(Eval::Done(v)) } } // ── eval_trampoline ────────────────────────────────────────────────────────── /// Primary entry point for sub-expression evaluation within builtins. /// /// Delegates to the CEK machine (`cek::cek_eval`), which evaluates iteratively /// using a heap-allocated continuation stack instead of growing the Rust stack. pub(crate) fn eval_trampoline( budget: &Budget, eval_limit: &mut usize, value: Value, scope: Scope, ) -> crate::Result { cek::cek_eval(budget, eval_limit, value, scope) } // ── Self-evaluating tags ───────────────────────────────────────────────────── /// Singleton objects with these keys pass through eval unchanged. /// These are data/control markers, not procedure calls. /// /// NOT listed: `vau`, `wrap` (must go through normal dispatch for scope capture). /// Hardcoded self-evaluating tags — the minimum set needed before stdlib loads. /// Additional tags can be registered via `deftype` with `{"self-eval": true, "raw": true}`. /// miniKanren tags (runtime/lvar, sigma, runtime/stream-cons, runtime/stream-thunk) are /// deftype'd in kanren.jst. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum SelfEvalTag { Builtins, // compiled builtin reference DefTypeDescriptor, // lexical type descriptor (self-eval type metadata) Error, // reified error value (from try) Mu, // recursive operative value (carries defs + identity + scope) Pattern, // first-class pattern value (from pat) Quoted, // monadic wrapper: value survives evaluation unchanged (for qmap) Scope, // captured scope in vau/fn value forms Type, // first-class type predicate (callable) } impl SelfEvalTag { /// Try to parse a string tag into a `SelfEvalTag`. Returns `None` for non-self-eval tags. pub(crate) fn from_str(s: &str) -> Option { match s { "builtins" => Some(SelfEvalTag::Builtins), "deftype-descriptor" => Some(SelfEvalTag::DefTypeDescriptor), "error" => Some(SelfEvalTag::Error), "mu" => Some(SelfEvalTag::Mu), "pattern" => Some(SelfEvalTag::Pattern), "quoted" => Some(SelfEvalTag::Quoted), "scope" => Some(SelfEvalTag::Scope), "type" => Some(SelfEvalTag::Type), _ => None, } } /// The canonical string representation of this tag (matches Jest value keys). #[allow(dead_code)] pub(crate) fn as_str(&self) -> &'static str { match self { SelfEvalTag::Builtins => "builtins", SelfEvalTag::DefTypeDescriptor => "deftype-descriptor", SelfEvalTag::Error => "error", SelfEvalTag::Mu => "mu", SelfEvalTag::Pattern => "pattern", SelfEvalTag::Quoted => "quoted", SelfEvalTag::Scope => "scope", SelfEvalTag::Type => "type", } } } /// Check whether a tag string is a hardcoded self-evaluating tag. #[allow(dead_code)] pub(crate) fn is_self_eval_tag(tag: &str) -> bool { SelfEvalTag::from_str(tag).is_some() } /// Check if a type descriptor has `"raw": true` (skip payload evaluation). pub(crate) fn is_raw_self_eval(descriptor: &Value) -> bool { use nanvalue::NanView; match descriptor.view() { NanView::Object(ho) => match ho.get("raw") { Some(v) => matches!(v.view(), NanView::Bool(true)), None => false, }, _ => false, } } /// Run the type's refiner on the payload (validates + normalizes in one step). /// If no refiner is present, returns the payload unchanged. pub(crate) fn validate_and_normalize( tag: &str, payload: Value, descriptor: &Value, budget: &Budget, eval_limit: &mut usize, scope: &Scope, ) -> crate::Result { let desc_obj = match descriptor.view() { nanvalue::NanView::Object(ho) => ho, _ => return Ok(payload), }; // Run refiner if present and non-null if let Some(refiner_fn) = desc_obj.get("refiner") && !matches!(refiner_fn.view(), nanvalue::NanView::Null) { let result = procs::dispatch_call( refiner_fn.clone(), &[payload], budget, eval_limit, scope.clone(), tag, )?; return match result { Eval::Done(v) => Ok(v), Eval::TailEval { value, scope: s } => eval_trampoline(budget, eval_limit, value, s), Eval::Yield { expr, scope: s, resume } => { let val = eval_trampoline(budget, eval_limit, expr, s)?; match resume(val, budget, eval_limit)? { Eval::Done(v) => Ok(v), Eval::TailEval { value, scope: s2 } => eval_trampoline(budget, eval_limit, value, s2), Eval::Yield { .. } => unreachable!("nested Yield in validate_and_normalize"), } } }; } Ok(payload) } // ── Runtime ─────────────────────────────────────────────────────────────────── pub struct Runtime { budget: Budget, eval_limit: usize, engine: Engine, } impl Runtime { pub fn new(mem_limit: usize, eval_limit: usize) -> Self { Self { budget: Budget::new(mem_limit), eval_limit, engine: Engine::standard(), } } /// Construct a Runtime with effectively unlimited memory and evaluation steps. pub fn unlimited() -> Self { Self::new(usize::MAX / 2, usize::MAX / 2) } /// Construct a fast Runtime (i64/f64, wrapping overflow, no arbitrary precision). pub fn fast(mem_limit: usize, eval_limit: usize) -> Self { Self { budget: Budget::new(mem_limit), eval_limit, engine: Engine::fast(), } } /// Set the engine (module configuration) for this runtime. pub fn with_engine(mut self, engine: Engine) -> Self { self.engine = engine; self } /// Set the numeric format for this runtime (and thread). /// Deprecated: use `with_engine(Engine::fast())` instead. pub fn with_numeric_format(self, fmt: number::NumFormat) -> Self { number::set_numeric_format(fmt); self } /// Get remaining eval_limit (for profiling/debugging). pub fn remaining_eval_limit(&self) -> usize { self.eval_limit } /// Attach a file loader, enabling `import` in `do` blocks. pub fn with_file_loader( mut self, f: impl Fn(&str) -> std::io::Result + 'static, ) -> Self { let ctx = ctx::EvalCtx::with_loader(f); self.budget = Budget::with_ctx(self.budget.remaining(), ctx); self } pub fn run( &mut self, program: &dyn crate::JestInput, ) -> crate::Result { let scope = self.build_scope(std::collections::HashMap::new()); let val = convert::from_input(program, &self.budget)?; let result = eval_trampoline(&self.budget, &mut self.eval_limit, val, scope)?; Ok(convert::to_output(result)) } /// Parse JSON source and evaluate it in one step. pub fn run_json(&mut self, source: &str) -> crate::Result { let val = parse::parse_json(source, &self.budget)?; let scope = self.build_scope(std::collections::HashMap::new()); eval_trampoline(&self.budget, &mut self.eval_limit, val, scope) } /// Evaluate a pre-parsed `Value`. pub fn run_value(&mut self, val: Value) -> crate::Result { let scope = self.build_scope(std::collections::HashMap::new()); eval_trampoline(&self.budget, &mut self.eval_limit, val, scope) } /// Evaluate a pre-parsed `Value` with extra scope bindings. pub fn run_value_with_extra( &mut self, val: Value, extra: std::collections::HashMap, ) -> crate::Result { let scope = self.build_scope(extra); eval_trampoline(&self.budget, &mut self.eval_limit, val, scope) } /// Compile to bytecode and execute in the VM. pub fn run_bytecode(&mut self, val: Value) -> crate::Result { let scope = self.build_scope(std::collections::HashMap::new()); let (chunk, ctx) = crate::compile::compile_bytecode_with_scope(&val, Some(&scope)); crate::compile::vm::vm_execute_with_ctx( &chunk, &ctx, &self.budget, &mut self.eval_limit, scope, ) } /// Build a runtime Scope from the cached global map plus extra bindings. /// /// The global scope is cached as `GlobalEntry` (Send+Sync) and converted to /// runtime Values here because `runtime::Value` is !Send (Rc-based). /// A private unlimited budget is used for global entries so that a small user budget /// cannot prevent scope initialization. pub fn build_scope(&self, extra: std::collections::HashMap) -> Scope { // Activate engine's thread-locals before building scope self.engine.activate(); let base = cached_global_scope_for_engine(&self.engine); if extra.is_empty() { base } else { base.local(extra) } } } // ── Cached global scope ────────────────────────────────────────────────────── // // The global scope (builtins + stdlib bootstrap) is expensive to build: // procs::init_for_engine() returns GlobalEntry values that must be converted to // runtime Values. Since Value is Rc-based (!Send), we cache per-thread per-engine. // Scope::clone() is a cheap Rc bump, so all Runtimes with the same engine on the // same thread share one base scope. thread_local! { static SCOPE_CACHE: std::cell::RefCell> = std::cell::RefCell::new(std::collections::HashMap::new()); } /// Check if the scope cache RefCell is available (not currently borrowed). /// Used to guard JIT compilation which needs to build a scope. pub fn scope_cache_available() -> bool { SCOPE_CACHE.with(|cache| cache.try_borrow().is_ok()) } fn cached_global_scope_for_engine(engine: &Engine) -> Scope { let hash = engine.config_hash(); SCOPE_CACHE.with(|cache| { let mut map = cache.borrow_mut(); if let Some(scope) = map.get(&hash) { return scope.clone(); } let global_budget = Budget::new(usize::MAX / 2); let global = procs::init_for_engine(engine); let rt_map: std::collections::HashMap = global .iter() .map(|(k, entry)| (k.clone(), entry.to_value(&global_budget))) .collect(); let scope = Scope::new_from_map(rt_map); map.insert(hash, scope.clone()); scope }) } // ── RuntimeError ────────────────────────────────────────────────────────────── #[derive(Debug)] pub enum RuntimeError { MemoryLimit, EvalLimit, } pub type Result = std::result::Result; impl fmt::Display for RuntimeError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { RuntimeError::MemoryLimit => write!(f, "memory limit exceeded"), RuntimeError::EvalLimit => write!(f, "evaluation step limit exceeded"), } } } impl std::error::Error for RuntimeError {} impl From for crate::Error { fn from(e: RuntimeError) -> Self { crate::Error::Runtime(e) } } #[cfg(test)] mod profiling_tests { use super::*; #[test] fn compare_interpreter_vs_bytecode_steps() { let src = r#"{"do": [ {"defrec": [[ "fib", [{"var": "n"}, {"var": "a"}, {"var": "b"}], {"if": [ {"<": [{"var": "n"}, 1]}, {"var": "a"}, {"fib": [{"-": [{"var": "n"}, 1]}, {"var": "b"}, {"+": [{"var": "a"}, {"var": "b"}]}]} ]} ]]}, {"fib": [3, 0, 1]} ]}"#; let budget = Budget::new(10_000_000); let program = parse::parse_json(src, &budget).unwrap(); // Test interpreter first to see baseline let initial_limit = 10_000_000; let mut rt_interp = Runtime::new(10_000_000, initial_limit); let result_interp = rt_interp.run_value(program.clone()).unwrap(); let steps_interp = initial_limit - rt_interp.remaining_eval_limit(); println!("\n=== Fibonacci(3) Step Count Comparison ==="); println!("Result: {}", result_interp.to_string()); println!("Interpreter steps: {}", steps_interp); // Test bytecode with same limit let mut rt_bytecode = Runtime::new(10_000_000, initial_limit); match rt_bytecode.run_bytecode(program.clone()) { Ok(result_bytecode) => { let steps_bytecode = initial_limit - rt_bytecode.remaining_eval_limit(); println!("Bytecode steps: {}", steps_bytecode); println!("Ratio: {:.2}x", steps_bytecode as f64 / steps_interp as f64); assert_eq!(result_interp.to_string(), result_bytecode.to_string()); } Err(e) => { let steps_bytecode = initial_limit - rt_bytecode.remaining_eval_limit(); println!("Bytecode FAILED after {} steps: {:?}", steps_bytecode, e); panic!("Bytecode exceeded eval limit"); } } } }