refactor: VM-only architecture — remove tree-walking interpreter
Eliminate dual-engine architecture (Interpreter + Vm) in favor of a single bytecode VM executor. - Extract shared types (Value, FunctionProto, Closure, UpvalueObj, Runtime trait, NativeFn) into new module - Consolidate builtin registration in — single used by VM, eliminates 40-line duplicate - Delete tree-walker: exec.rs, eval.rs, interpreter.rs, env.rs, module.rs - Change Value::Function(Rc<Function>) → Value::Function(Rc<Closure>) eliminating the closures HashMap pointer-key hack - Fix VM semantic gaps found during migration: * Structural equality for Object/Array in is_equal * CompoundAssignGlobal opcode (global compound assigns were broken) * 9 string methods added to VM get_property * ForInInit error on non-iterable values - Switch run_file/run_repl to VM; remove --vm CLI flag - Move 327 tests from interpreter/ to vm/ — all pass Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -7,67 +7,12 @@
|
||||
use crate::ast::*;
|
||||
use crate::ast::expr::{Literal, UnaryOp, BinaryOp, LogicalOp, AssignOp};
|
||||
use crate::error::RuntimeError;
|
||||
use crate::interpreter::Value;
|
||||
use crate::runtime::{Value, FunctionProto};
|
||||
use super::opcode::*;
|
||||
|
||||
use std::rc::Rc;
|
||||
use std::cell::RefCell;
|
||||
|
||||
// ============================================================================
|
||||
// FunctionProto — compiled function blueprint
|
||||
// ============================================================================
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FunctionProto {
|
||||
pub name: Option<String>,
|
||||
pub arity: u8,
|
||||
pub code: Vec<u8>,
|
||||
pub constants: Vec<Value>,
|
||||
/// Nested function protos (for closures and function declarations)
|
||||
pub protos: Vec<Rc<FunctionProto>>,
|
||||
pub upvalue_count: u8,
|
||||
/// Upvalue descriptors for this function (for Closure opcode emission)
|
||||
pub upvalues: Vec<(bool, u8)>, // (is_local, index)
|
||||
pub lines: Vec<(usize, usize)>, // (bytecode_offset, source_line)
|
||||
}
|
||||
|
||||
impl FunctionProto {
|
||||
pub fn new(name: Option<String>) -> Self {
|
||||
Self {
|
||||
name,
|
||||
arity: 0,
|
||||
code: Vec::new(),
|
||||
constants: Vec::new(),
|
||||
protos: Vec::new(),
|
||||
upvalue_count: 0,
|
||||
upvalues: Vec::new(),
|
||||
lines: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn add_constant(&mut self, val: Value) -> u16 {
|
||||
// Check for existing identical constant
|
||||
for (i, c) in self.constants.iter().enumerate() {
|
||||
if values_eq(c, &val) {
|
||||
return i as u16;
|
||||
}
|
||||
}
|
||||
let idx = self.constants.len();
|
||||
self.constants.push(val);
|
||||
idx as u16
|
||||
}
|
||||
}
|
||||
|
||||
fn values_eq(a: &Value, b: &Value) -> bool {
|
||||
match (a, b) {
|
||||
(Value::Number(x), Value::Number(y)) => (x - y).abs() < f64::EPSILON,
|
||||
(Value::String(x), Value::String(y)) => x == y,
|
||||
(Value::Bool(x), Value::Bool(y)) => x == y,
|
||||
(Value::Nil, Value::Nil) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Compiler
|
||||
// ============================================================================
|
||||
@@ -536,7 +481,7 @@ impl Compiler {
|
||||
self.function.code.push(compound_op as u8);
|
||||
} else {
|
||||
let name_idx = self.add_string_constant(name);
|
||||
emit_u16(&mut self.function.code, OpCode::CompoundAssignProp, name_idx);
|
||||
emit_u16(&mut self.function.code, OpCode::CompoundAssignGlobal, name_idx);
|
||||
self.function.code.push(compound_op as u8);
|
||||
}
|
||||
}
|
||||
@@ -558,16 +503,11 @@ impl Compiler {
|
||||
emit_u16(&mut self.function.code, OpCode::SetProperty, name_idx);
|
||||
} else {
|
||||
// Compound property set: object.name op= value
|
||||
// Strategy: load object, dup, get property as current value,
|
||||
// load rhs, apply op, set property
|
||||
// CompoundAssignProp handler does get_property internally
|
||||
self.compile_expr(object)?;
|
||||
self.emit_op(OpCode::Dup);
|
||||
let name_idx = self.add_string_constant(name);
|
||||
emit_u16(&mut self.function.code, OpCode::GetProperty, name_idx); // current value
|
||||
self.compile_expr(value)?; // rhs
|
||||
let compound_op = assign_op_to_compound(op);
|
||||
self.function.code.push(compound_op as u8);
|
||||
// Now stack: object, current_val, rhs → set property
|
||||
let name_idx = self.add_string_constant(name);
|
||||
emit_u16(&mut self.function.code, OpCode::CompoundAssignProp, name_idx);
|
||||
self.function.code.push(compound_op as u8);
|
||||
}
|
||||
|
||||
@@ -79,12 +79,13 @@ pub enum OpCode {
|
||||
CompoundAssignProp = 40, // operand: u16 name idx + u8 compound-op tag
|
||||
CompoundAssignIndex = 41, // operand: u8 compound-op tag
|
||||
CompoundAssignUpvalue = 44, // operand: u8 upvalue idx + u8 compound-op tag
|
||||
CompoundAssignGlobal = 45, // operand: u16 name idx + u8 compound-op tag
|
||||
}
|
||||
|
||||
impl OpCode {
|
||||
pub fn from_u8(byte: u8) -> Option<Self> {
|
||||
match byte {
|
||||
0..=41 | 44 => Some(unsafe { std::mem::transmute::<u8, OpCode>(byte) }),
|
||||
0..=41 | 44..=45 => Some(unsafe { std::mem::transmute::<u8, OpCode>(byte) }),
|
||||
42 => Some(OpCode::LoadUpvalue),
|
||||
43 => Some(OpCode::StoreUpvalue),
|
||||
_ => None,
|
||||
@@ -92,7 +93,7 @@ impl OpCode {
|
||||
}
|
||||
|
||||
/// Total number of distinct opcodes
|
||||
pub const COUNT: usize = 45;
|
||||
pub const COUNT: usize = 46;
|
||||
}
|
||||
|
||||
/// Compound assignment operator tags (used as operand byte after
|
||||
@@ -171,3 +172,5 @@ pub const SIZE_OP: usize = 1;
|
||||
pub const SIZE_U8: usize = 2;
|
||||
/// Size in bytes of an opcode with a u16/i16 operand.
|
||||
pub const SIZE_U16: usize = 3;
|
||||
/// Size in bytes of an opcode with a u16 + u8 operand.
|
||||
pub const SIZE_U16_PLUS1: usize = 4;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+146
-94
@@ -4,11 +4,11 @@
|
||||
//! call frames. Implements the `Runtime` trait for native function support.
|
||||
|
||||
use crate::error::RuntimeError;
|
||||
use crate::interpreter::{Value, Runtime};
|
||||
use crate::runtime::{Value, Runtime, FunctionProto, Closure, UpvalueObj};
|
||||
use crate::lexer::Lexer;
|
||||
use crate::parser::Parser;
|
||||
use super::opcode::*;
|
||||
use super::compiler::{Compiler, FunctionProto};
|
||||
use super::compiler::Compiler;
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
@@ -18,22 +18,6 @@ use std::rc::Rc;
|
||||
// VM data structures
|
||||
// ============================================================================
|
||||
|
||||
/// An upvalue — a reference to a local variable in an enclosing function.
|
||||
#[derive(Debug, Clone)]
|
||||
struct UpvalueObj {
|
||||
/// Stack index where the value lives, or usize::MAX if closed
|
||||
location: usize,
|
||||
/// The value, if it has been moved off the stack (closed)
|
||||
closed: Option<Value>,
|
||||
}
|
||||
|
||||
/// A runtime closure: compiled function proto + captured upvalues.
|
||||
#[derive(Debug, Clone)]
|
||||
struct Closure {
|
||||
proto: Rc<FunctionProto>,
|
||||
upvalues: Vec<Rc<RefCell<UpvalueObj>>>,
|
||||
}
|
||||
|
||||
/// A call frame on the VM stack.
|
||||
struct CallFrame {
|
||||
closure: Rc<Closure>,
|
||||
@@ -62,9 +46,6 @@ pub struct Vm {
|
||||
|
||||
/// Open upvalues (tracked so closures share the same upvalue object)
|
||||
open_upvalues: Vec<Rc<RefCell<UpvalueObj>>>,
|
||||
|
||||
/// VM-compiled closures: maps Function Rc pointer → Closure data
|
||||
closures: RefCell<HashMap<*const crate::interpreter::Function, Rc<Closure>>>,
|
||||
}
|
||||
|
||||
impl Vm {
|
||||
@@ -85,7 +66,6 @@ impl Vm {
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|_| ".".to_string()),
|
||||
open_upvalues: Vec::new(),
|
||||
closures: RefCell::new(HashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,6 +81,12 @@ impl Vm {
|
||||
self.run(Rc::new(proto))
|
||||
}
|
||||
|
||||
/// Look up a global variable by name (for test inspection).
|
||||
pub fn get_global(&self, name: &str) -> Option<Value> {
|
||||
self.globals.borrow().get(name).map(|(v, _)| v.clone())
|
||||
.or_else(|| self.builtins.borrow().get(name).cloned())
|
||||
}
|
||||
|
||||
/// Execute a compiled FunctionProto.
|
||||
pub fn run(&mut self, proto: Rc<FunctionProto>) -> Result<(), RuntimeError> {
|
||||
let closure = Rc::new(Closure {
|
||||
@@ -496,15 +482,7 @@ impl Vm {
|
||||
}
|
||||
|
||||
let closure = Rc::new(Closure { proto, upvalues });
|
||||
let func = Rc::new(crate::interpreter::Function {
|
||||
params: Vec::new(),
|
||||
body: Vec::new(),
|
||||
env: Rc::new(RefCell::new(crate::interpreter::Env::new(None))),
|
||||
name: None,
|
||||
});
|
||||
let key = Rc::as_ptr(&func) as *const crate::interpreter::Function;
|
||||
self.closures.borrow_mut().insert(key, closure);
|
||||
self.stack.push(Value::Function(func));
|
||||
self.stack.push(Value::Function(closure));
|
||||
self.advance_ip_to(off);
|
||||
}
|
||||
|
||||
@@ -527,7 +505,15 @@ impl Vm {
|
||||
// --- For-in ---
|
||||
OpCode::ForInInit => {
|
||||
let iterable = self.stack.pop().unwrap();
|
||||
let items = self.for_in_items(&iterable);
|
||||
let items = match &iterable {
|
||||
Value::Array(arr) => arr.borrow().clone(),
|
||||
Value::Object(obj) => obj.borrow().keys().map(|k| Value::String(k.clone())).collect(),
|
||||
Value::String(s) => s.chars().map(|c| Value::String(c.to_string())).collect(),
|
||||
_ => return Err(RuntimeError::RuntimeError {
|
||||
message: format!("for-in requires an array, object, or string, got {}", iterable),
|
||||
token: None,
|
||||
}),
|
||||
};
|
||||
self.stack.push(Value::Array(Rc::new(RefCell::new(items))));
|
||||
self.advance_ip(SIZE_OP);
|
||||
}
|
||||
@@ -632,6 +618,33 @@ impl Vm {
|
||||
self.stack.push(result);
|
||||
self.advance_ip(SIZE_U8 + 1);
|
||||
}
|
||||
OpCode::CompoundAssignGlobal => {
|
||||
let name_idx = read_u16(code, ip) as usize;
|
||||
let op_tag = code[ip + 3];
|
||||
let compound_op = CompoundOp::from_u8(op_tag).unwrap();
|
||||
let rhs = self.stack.pop().unwrap();
|
||||
let name = proto_string(&proto, name_idx)?;
|
||||
let lhs = self.globals.borrow().get(&name)
|
||||
.map(|(v, _)| v.clone())
|
||||
.or_else(|| self.builtins.borrow().get(&name).cloned())
|
||||
.ok_or_else(|| RuntimeError::RuntimeError {
|
||||
message: format!("Undefined variable '{}'", name),
|
||||
token: None,
|
||||
})?;
|
||||
let result = self.apply_compound_op(lhs, rhs, compound_op)?;
|
||||
// Check const flag (globals tuple stores (value, mutable))
|
||||
if let Some((_, mutable)) = self.globals.borrow().get(&name) {
|
||||
if !*mutable {
|
||||
return Err(RuntimeError::RuntimeError {
|
||||
message: format!("Cannot reassign constant '{}'", name),
|
||||
token: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
self.globals.borrow_mut().insert(name, (result.clone(), true));
|
||||
self.stack.push(result);
|
||||
self.advance_ip(SIZE_U16_PLUS1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -678,22 +691,13 @@ impl Vm {
|
||||
self.stack.push(result);
|
||||
self.advance_ip(SIZE_U8);
|
||||
}
|
||||
Value::Function(_) => {
|
||||
// Look up the VM-compiled closure
|
||||
let func_ptr = match &self.stack[callee_idx] {
|
||||
Value::Function(f) => Rc::as_ptr(f) as *const crate::interpreter::Function,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let closure = self.closures.borrow().get(&func_ptr).cloned().ok_or_else(|| RuntimeError::RuntimeError {
|
||||
message: "VM: call to non-VM function (tree-walker function not supported in VM)".into(),
|
||||
token: None,
|
||||
})?;
|
||||
|
||||
Value::Function(closure) => {
|
||||
let closure = Rc::clone(closure);
|
||||
// Advance caller's IP past the Call instruction before pushing new frame
|
||||
self.frame_mut().ip += SIZE_U8;
|
||||
|
||||
// Callee is at callee_idx, args start at callee_idx+1
|
||||
let base = callee_idx + 1; // first param is here
|
||||
let base = callee_idx + 1;
|
||||
self.frames.push(CallFrame {
|
||||
closure,
|
||||
ip: 0,
|
||||
@@ -748,6 +752,78 @@ impl Vm {
|
||||
},
|
||||
Value::String(s) => match name {
|
||||
"length" => Ok(Value::Number(s.chars().count() as f64)),
|
||||
"upper" => {
|
||||
let s = s.clone();
|
||||
Ok(Value::NativeFunction(Rc::new(move |runtime: &mut dyn Runtime, args: Vec<Value>| {
|
||||
let mut all_args = vec![Value::String(s.clone())];
|
||||
all_args.extend(args);
|
||||
crate::runtime::builtins::string::upper(runtime, all_args)
|
||||
})))
|
||||
}
|
||||
"lower" => {
|
||||
let s = s.clone();
|
||||
Ok(Value::NativeFunction(Rc::new(move |runtime: &mut dyn Runtime, args: Vec<Value>| {
|
||||
let mut all_args = vec![Value::String(s.clone())];
|
||||
all_args.extend(args);
|
||||
crate::runtime::builtins::string::lower(runtime, all_args)
|
||||
})))
|
||||
}
|
||||
"trim" => {
|
||||
let s = s.clone();
|
||||
Ok(Value::NativeFunction(Rc::new(move |runtime: &mut dyn Runtime, args: Vec<Value>| {
|
||||
let mut all_args = vec![Value::String(s.clone())];
|
||||
all_args.extend(args);
|
||||
crate::runtime::builtins::string::trim(runtime, all_args)
|
||||
})))
|
||||
}
|
||||
"substring" => {
|
||||
let s = s.clone();
|
||||
Ok(Value::NativeFunction(Rc::new(move |runtime: &mut dyn Runtime, args: Vec<Value>| {
|
||||
let mut all_args = vec![Value::String(s.clone())];
|
||||
all_args.extend(args);
|
||||
crate::runtime::builtins::string::substring(runtime, all_args)
|
||||
})))
|
||||
}
|
||||
"replace" => {
|
||||
let s = s.clone();
|
||||
Ok(Value::NativeFunction(Rc::new(move |runtime: &mut dyn Runtime, args: Vec<Value>| {
|
||||
let mut all_args = vec![Value::String(s.clone())];
|
||||
all_args.extend(args);
|
||||
crate::runtime::builtins::string::replace(runtime, all_args)
|
||||
})))
|
||||
}
|
||||
"contains" => {
|
||||
let s = s.clone();
|
||||
Ok(Value::NativeFunction(Rc::new(move |runtime: &mut dyn Runtime, args: Vec<Value>| {
|
||||
let mut all_args = vec![Value::String(s.clone())];
|
||||
all_args.extend(args);
|
||||
crate::runtime::builtins::string::contains(runtime, all_args)
|
||||
})))
|
||||
}
|
||||
"starts_with" => {
|
||||
let s = s.clone();
|
||||
Ok(Value::NativeFunction(Rc::new(move |runtime: &mut dyn Runtime, args: Vec<Value>| {
|
||||
let mut all_args = vec![Value::String(s.clone())];
|
||||
all_args.extend(args);
|
||||
crate::runtime::builtins::string::starts_with(runtime, all_args)
|
||||
})))
|
||||
}
|
||||
"ends_with" => {
|
||||
let s = s.clone();
|
||||
Ok(Value::NativeFunction(Rc::new(move |runtime: &mut dyn Runtime, args: Vec<Value>| {
|
||||
let mut all_args = vec![Value::String(s.clone())];
|
||||
all_args.extend(args);
|
||||
crate::runtime::builtins::string::ends_with(runtime, all_args)
|
||||
})))
|
||||
}
|
||||
"split" => {
|
||||
let s = s.clone();
|
||||
Ok(Value::NativeFunction(Rc::new(move |runtime: &mut dyn Runtime, args: Vec<Value>| {
|
||||
let mut all_args = vec![Value::String(s.clone())];
|
||||
all_args.extend(args);
|
||||
crate::runtime::builtins::string::split(runtime, all_args)
|
||||
})))
|
||||
}
|
||||
_ => Err(RuntimeError::RuntimeError {
|
||||
message: format!("String has no property '{}'", name),
|
||||
token: None,
|
||||
@@ -922,7 +998,23 @@ impl Vm {
|
||||
(Value::Bool(x), Value::Bool(y)) => x == y,
|
||||
(Value::Number(x), Value::Number(y)) => x == y,
|
||||
(Value::String(x), Value::String(y)) => x == y,
|
||||
_ => false, // Simplified — full structural equality omitted for now
|
||||
(Value::Array(x), Value::Array(y)) => {
|
||||
let x = x.borrow();
|
||||
let y = y.borrow();
|
||||
if x.len() != y.len() { return false; }
|
||||
x.iter().zip(y.iter()).all(|(a, b)| self.is_equal(a, b))
|
||||
}
|
||||
(Value::Object(x), Value::Object(y)) => {
|
||||
let x = x.borrow();
|
||||
let y = y.borrow();
|
||||
if x.len() != y.len() { return false; }
|
||||
x.iter().all(|(k, v)| {
|
||||
y.get(k).map_or(false, |yv| self.is_equal(v, yv))
|
||||
})
|
||||
}
|
||||
(Value::Function(x), Value::Function(y)) => Rc::ptr_eq(x, y),
|
||||
(Value::NativeFunction(x), Value::NativeFunction(y)) => Rc::ptr_eq(x, y),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -988,14 +1080,6 @@ impl Vm {
|
||||
// For-in helper
|
||||
// ========================================================================
|
||||
|
||||
fn for_in_items(&self, collection: &Value) -> Vec<Value> {
|
||||
match collection {
|
||||
Value::Array(arr) => arr.borrow().clone(),
|
||||
Value::Object(obj) => obj.borrow().keys().map(|k| Value::String(k.clone())).collect(),
|
||||
Value::String(s) => s.chars().map(|c| Value::String(c.to_string())).collect(),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Runtime for Vm {
|
||||
@@ -1080,12 +1164,7 @@ impl Runtime for Vm {
|
||||
}
|
||||
}
|
||||
|
||||
// 11. Transfer closures from module VM to parent (so exported fns are callable)
|
||||
for (key, closure) in module_vm.closures.borrow().iter() {
|
||||
self.closures.borrow_mut().insert(*key, Rc::clone(closure));
|
||||
}
|
||||
|
||||
// 12. Cache in parent
|
||||
// 11. Cache in parent
|
||||
self.module_cache.borrow_mut().insert(resolved, exports_obj.clone());
|
||||
|
||||
Ok(exports_obj)
|
||||
@@ -1107,43 +1186,16 @@ fn proto_string(proto: &FunctionProto, idx: usize) -> Result<String, RuntimeErro
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Builtin registration (reuses tree-walker's builtins)
|
||||
// Builtin registration (delegates to runtime::builtins)
|
||||
// ============================================================================
|
||||
|
||||
fn register_builtins(map: &Rc<RefCell<HashMap<String, Value>>>) {
|
||||
let mut m = map.borrow_mut();
|
||||
|
||||
// io
|
||||
let mut io = HashMap::new();
|
||||
io.insert("print".into(), Value::NativeFunction(Rc::new(crate::interpreter::builtins::io::print)));
|
||||
io.insert("input".into(), Value::NativeFunction(Rc::new(crate::interpreter::builtins::io::input)));
|
||||
m.insert("io".into(), Value::Object(Rc::new(RefCell::new(io))));
|
||||
m.insert("print".into(), Value::NativeFunction(Rc::new(crate::interpreter::builtins::io::print)));
|
||||
m.insert("input".into(), Value::NativeFunction(Rc::new(crate::interpreter::builtins::io::input)));
|
||||
|
||||
// os
|
||||
let mut os = HashMap::new();
|
||||
os.insert("clock".into(), Value::NativeFunction(Rc::new(crate::interpreter::builtins::os::clock)));
|
||||
m.insert("os".into(), Value::Object(Rc::new(RefCell::new(os))));
|
||||
m.insert("clock".into(), Value::NativeFunction(Rc::new(crate::interpreter::builtins::os::clock)));
|
||||
|
||||
// core
|
||||
m.insert("len".into(), Value::NativeFunction(Rc::new(crate::interpreter::builtins::core::len)));
|
||||
m.insert("typeof".into(), Value::NativeFunction(Rc::new(crate::interpreter::builtins::core::typeof_fn)));
|
||||
m.insert("push".into(), Value::NativeFunction(Rc::new(crate::interpreter::builtins::core::push)));
|
||||
m.insert("pop".into(), Value::NativeFunction(Rc::new(crate::interpreter::builtins::core::pop)));
|
||||
|
||||
// require (VM version)
|
||||
m.insert("require".into(), Value::NativeFunction(Rc::new(crate::interpreter::module::require_fn)));
|
||||
|
||||
// string
|
||||
m.insert("split".into(), Value::NativeFunction(Rc::new(crate::interpreter::builtins::string::split)));
|
||||
m.insert("trim".into(), Value::NativeFunction(Rc::new(crate::interpreter::builtins::string::trim)));
|
||||
m.insert("substring".into(), Value::NativeFunction(Rc::new(crate::interpreter::builtins::string::substring)));
|
||||
m.insert("replace".into(), Value::NativeFunction(Rc::new(crate::interpreter::builtins::string::replace)));
|
||||
m.insert("contains".into(), Value::NativeFunction(Rc::new(crate::interpreter::builtins::string::contains)));
|
||||
m.insert("upper".into(), Value::NativeFunction(Rc::new(crate::interpreter::builtins::string::upper)));
|
||||
m.insert("lower".into(), Value::NativeFunction(Rc::new(crate::interpreter::builtins::string::lower)));
|
||||
m.insert("starts_with".into(), Value::NativeFunction(Rc::new(crate::interpreter::builtins::string::starts_with)));
|
||||
m.insert("ends_with".into(), Value::NativeFunction(Rc::new(crate::interpreter::builtins::string::ends_with)));
|
||||
crate::runtime::builtins::register_all(&mut *m);
|
||||
// require is VM-specific
|
||||
m.insert("require".into(), Value::NativeFunction(Rc::new(crate::runtime::require_fn)));
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "tests.rs"]
|
||||
mod tests;
|
||||
|
||||
Reference in New Issue
Block a user