mirror of
https://github.com/duhanbalci/dexpr.git
synced 2026-07-01 16:19:16 +00:00
performance improvements
This commit is contained in:
@@ -1,11 +1,29 @@
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
use dexpr::{ast::value::Value, compiler::Compiler, parser, vm::VM};
|
||||
use indexmap::IndexMap;
|
||||
use rust_decimal::Decimal;
|
||||
use rust_decimal_macros::dec;
|
||||
use smol_str::SmolStr;
|
||||
|
||||
/// Helper: compile source to bytecode
|
||||
fn compile(source: &str) -> Vec<u8> {
|
||||
let ast = parser::program(source).unwrap();
|
||||
let mut compiler = Compiler::new();
|
||||
compiler.compile(ast).unwrap()
|
||||
}
|
||||
|
||||
/// Helper: build a sample object with N fields
|
||||
fn sample_object(n: usize) -> Value {
|
||||
let mut map = IndexMap::new();
|
||||
for i in 0..n {
|
||||
map.insert(SmolStr::new(format!("field{}", i)), Value::Number(Decimal::from(i)));
|
||||
}
|
||||
Value::Object(Box::new(map))
|
||||
}
|
||||
|
||||
pub fn criterion_benchmark(c: &mut Criterion) {
|
||||
// 1. Parser Benchmark
|
||||
// ── Existing benchmarks ──────────────────────────────────────────────
|
||||
|
||||
c.bench_function("parser_long", |b| {
|
||||
let input = include_str!("../examples/bench_long.dexpr");
|
||||
b.iter(|| {
|
||||
@@ -13,7 +31,6 @@ pub fn criterion_benchmark(c: &mut Criterion) {
|
||||
})
|
||||
});
|
||||
|
||||
// 2. Compiler Benchmark
|
||||
c.bench_function("compiler_long", |b| {
|
||||
let input = include_str!("../examples/bench_long.dexpr");
|
||||
let ast = parser::program(input).unwrap();
|
||||
@@ -23,9 +40,6 @@ pub fn criterion_benchmark(c: &mut Criterion) {
|
||||
})
|
||||
});
|
||||
|
||||
// 3. VM Benchmarks
|
||||
|
||||
// basic_long.dexpr benchmark
|
||||
c.bench_function("vm_basic_long", |b| {
|
||||
let input = include_str!("../examples/basic_long.dexpr");
|
||||
let ast = parser::program(input).unwrap();
|
||||
@@ -38,7 +52,6 @@ pub fn criterion_benchmark(c: &mut Criterion) {
|
||||
})
|
||||
});
|
||||
|
||||
// Long code benchmark (using bench_long.dexpr)
|
||||
c.bench_function("vm_long", |b| {
|
||||
let input = include_str!("../examples/bench_long.dexpr");
|
||||
let ast = parser::program(input).unwrap();
|
||||
@@ -50,12 +63,9 @@ pub fn criterion_benchmark(c: &mut Criterion) {
|
||||
})
|
||||
});
|
||||
|
||||
// Short code benchmark
|
||||
c.bench_function("vm_short", |b| {
|
||||
let input = "5.12 + test * 1.5";
|
||||
let ast = parser::program(input).unwrap();
|
||||
let mut compiler = Compiler::new();
|
||||
let bytecode = compiler.compile(ast).unwrap();
|
||||
let bytecode = compile(input);
|
||||
let test_val = dec!(100);
|
||||
b.iter(|| {
|
||||
let mut vm = VM::new(&bytecode);
|
||||
@@ -63,6 +73,220 @@ pub fn criterion_benchmark(c: &mut Criterion) {
|
||||
let _ = vm.execute().unwrap();
|
||||
})
|
||||
});
|
||||
|
||||
// ── #1: Method dispatch clone overhead ───────────────────────────────
|
||||
|
||||
// Object method — clone entire IndexMap per call
|
||||
c.bench_function("vm_object_method_keys", |b| {
|
||||
let bytecode = compile("obj.keys()");
|
||||
let obj = sample_object(20);
|
||||
b.iter(|| {
|
||||
let mut vm = VM::new(&bytecode);
|
||||
vm.set_global("obj", obj.clone());
|
||||
let _ = vm.execute().unwrap();
|
||||
})
|
||||
});
|
||||
|
||||
c.bench_function("vm_object_method_length", |b| {
|
||||
let bytecode = compile("obj.length()");
|
||||
let obj = sample_object(20);
|
||||
b.iter(|| {
|
||||
let mut vm = VM::new(&bytecode);
|
||||
vm.set_global("obj", obj.clone());
|
||||
let _ = vm.execute().unwrap();
|
||||
})
|
||||
});
|
||||
|
||||
c.bench_function("vm_object_method_contains", |b| {
|
||||
let bytecode = compile(r#"obj.contains("field10")"#);
|
||||
let obj = sample_object(20);
|
||||
b.iter(|| {
|
||||
let mut vm = VM::new(&bytecode);
|
||||
vm.set_global("obj", obj.clone());
|
||||
let _ = vm.execute().unwrap();
|
||||
})
|
||||
});
|
||||
|
||||
// StringList method — clone entire Vec per call
|
||||
c.bench_function("vm_strlist_method_length", |b| {
|
||||
let bytecode = compile("items.length()");
|
||||
let items = Value::StringList(Box::new((0..50).map(|i| SmolStr::new(format!("item{}", i))).collect()));
|
||||
b.iter(|| {
|
||||
let mut vm = VM::new(&bytecode);
|
||||
vm.set_global("items", items.clone());
|
||||
let _ = vm.execute().unwrap();
|
||||
})
|
||||
});
|
||||
|
||||
c.bench_function("vm_numlist_method_sum", |b| {
|
||||
let bytecode = compile("nums.sum()");
|
||||
let nums = Value::NumberList(Box::new((0..100).map(Decimal::from).collect()));
|
||||
b.iter(|| {
|
||||
let mut vm = VM::new(&bytecode);
|
||||
vm.set_global("nums", nums.clone());
|
||||
let _ = vm.execute().unwrap();
|
||||
})
|
||||
});
|
||||
|
||||
// String method — lighter clone (SmolStr)
|
||||
c.bench_function("vm_string_method_upper", |b| {
|
||||
let bytecode = compile(r#"s.upper()"#);
|
||||
b.iter(|| {
|
||||
let mut vm = VM::new(&bytecode);
|
||||
vm.set_global("s", Value::String(SmolStr::new("hello world this is a test string")));
|
||||
let _ = vm.execute().unwrap();
|
||||
})
|
||||
});
|
||||
|
||||
// ── #2 & #3: Vec alloc in method/external calls ─────────────────────
|
||||
|
||||
c.bench_function("vm_method_call_with_args", |b| {
|
||||
let bytecode = compile(r#"s.replace("hello", "world")"#);
|
||||
b.iter(|| {
|
||||
let mut vm = VM::new(&bytecode);
|
||||
vm.set_global("s", Value::String(SmolStr::new("hello world hello")));
|
||||
let _ = vm.execute().unwrap();
|
||||
})
|
||||
});
|
||||
|
||||
c.bench_function("vm_external_fn_call", |b| {
|
||||
let bytecode = compile("getRate(a, b)");
|
||||
b.iter(|| {
|
||||
let mut vm = VM::new(&bytecode);
|
||||
vm.set_global("a", Value::Number(dec!(10)));
|
||||
vm.set_global("b", Value::Number(dec!(20)));
|
||||
vm.register_function("getRate", |_args| Ok(Value::Number(dec!(34.5))));
|
||||
let _ = vm.execute().unwrap();
|
||||
})
|
||||
});
|
||||
|
||||
// ── #4: Value enum size (cache pressure on register ops) ─────────────
|
||||
|
||||
c.bench_function("vm_arithmetic_chain", |b| {
|
||||
// Pure arithmetic — measures register read/write cache performance
|
||||
let bytecode = compile(
|
||||
"a = 1.5\nb = 2.3\nc = a + b\nd = c * a\ne = d - b\nf = e / c\ng = f + d\ng * 2.0",
|
||||
);
|
||||
b.iter(|| {
|
||||
let mut vm = VM::new(&bytecode);
|
||||
let _ = vm.execute().unwrap();
|
||||
})
|
||||
});
|
||||
|
||||
c.bench_function("vm_comparison_chain", |b| {
|
||||
let bytecode = compile(
|
||||
"a = 10\nb = 20\nc = a < b\nd = b >= a\ne = a == 10\nf = b != 15\nc && d && e && f",
|
||||
);
|
||||
b.iter(|| {
|
||||
let mut vm = VM::new(&bytecode);
|
||||
let _ = vm.execute().unwrap();
|
||||
})
|
||||
});
|
||||
|
||||
// ── #5: SmolStr alloc per opcode (string table missing) ──────────────
|
||||
|
||||
c.bench_function("vm_global_read_heavy", |b| {
|
||||
// Many LoadGlobal ops → SmolStr alloc per read (split to stay within register limit)
|
||||
let bytecode = compile(
|
||||
"r1 = x1 + x2 + x3\nr2 = x4 + x5 + x6\nr = r1 + r2\nr",
|
||||
);
|
||||
b.iter(|| {
|
||||
let mut vm = VM::new(&bytecode);
|
||||
for i in 1..=6 {
|
||||
vm.set_global(&format!("x{}", i), Value::Number(Decimal::from(i)));
|
||||
}
|
||||
let _ = vm.execute().unwrap();
|
||||
})
|
||||
});
|
||||
|
||||
c.bench_function("vm_global_write_heavy", |b| {
|
||||
// Many StoreGlobal ops
|
||||
let bytecode = compile(
|
||||
"a = 1\nb = 2\nc = 3\nd = 4\ne = 5\nf = 6\nr = a + b + c\ns = d + e + f\nr + s",
|
||||
);
|
||||
b.iter(|| {
|
||||
let mut vm = VM::new(&bytecode);
|
||||
let _ = vm.execute().unwrap();
|
||||
})
|
||||
});
|
||||
|
||||
c.bench_function("vm_property_access_chain", |b| {
|
||||
// GetProperty → read_string per access
|
||||
let bytecode = compile("obj.field0 + obj.field1 + obj.field2 + obj.field3 + obj.field4");
|
||||
let obj = sample_object(10);
|
||||
b.iter(|| {
|
||||
let mut vm = VM::new(&bytecode);
|
||||
vm.set_global("obj", obj.clone());
|
||||
let _ = vm.execute().unwrap();
|
||||
})
|
||||
});
|
||||
|
||||
// ── #8: String concat with format! ───────────────────────────────────
|
||||
|
||||
c.bench_function("vm_string_concat", |b| {
|
||||
let bytecode = compile(
|
||||
r#"a = "hello" + " " + "world"
|
||||
b = a + " " + "this"
|
||||
c = b + " " + "test"
|
||||
c"#,
|
||||
);
|
||||
b.iter(|| {
|
||||
let mut vm = VM::new(&bytecode);
|
||||
let _ = vm.execute().unwrap();
|
||||
})
|
||||
});
|
||||
|
||||
c.bench_function("vm_string_number_coerce", |b| {
|
||||
let bytecode = compile(
|
||||
r#"a = "value: " + 42
|
||||
b = a + " and " + 3.14
|
||||
b"#,
|
||||
);
|
||||
b.iter(|| {
|
||||
let mut vm = VM::new(&bytecode);
|
||||
let _ = vm.execute().unwrap();
|
||||
})
|
||||
});
|
||||
|
||||
// ── #6: Opcode dispatch (overall loop throughput) ────────────────────
|
||||
|
||||
c.bench_function("vm_opcode_throughput", |b| {
|
||||
// Many simple ops to stress the dispatch loop
|
||||
let bytecode = compile(
|
||||
"a = 1\nb = 2\nc = a + b\nd = c * 2\ne = d - 1\nf = e / 3\n\
|
||||
g = f + a\nh = g * b\ni = h - c\nj = i + d\n\
|
||||
k = j * 2\nl = k - 1\nm = l + 3\nn = m / 2\n\
|
||||
o = n + a\np = o * b\np",
|
||||
);
|
||||
b.iter(|| {
|
||||
let mut vm = VM::new(&bytecode);
|
||||
let _ = vm.execute().unwrap();
|
||||
})
|
||||
});
|
||||
|
||||
// ── Combined: realistic expression with multiple issue areas ─────────
|
||||
|
||||
c.bench_function("vm_realistic_mixed", |b| {
|
||||
let bytecode = compile(
|
||||
r#"
|
||||
price = 100.50
|
||||
tax = 18
|
||||
discount = 5.5
|
||||
net = price * (1 + tax / 100) - discount
|
||||
label = "Total: " + net
|
||||
if net > 100 then
|
||||
result = label + " (high)"
|
||||
else
|
||||
result = label + " (low)"
|
||||
end
|
||||
result
|
||||
"#,
|
||||
);
|
||||
b.iter(|| {
|
||||
let mut vm = VM::new(&bytecode);
|
||||
let _ = vm.execute().unwrap();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group!(benches, criterion_benchmark);
|
||||
|
||||
@@ -11,9 +11,9 @@ pub enum Value {
|
||||
Number(Decimal),
|
||||
String(SmolStr),
|
||||
Boolean(bool),
|
||||
NumberList(Vec<Decimal>),
|
||||
StringList(Vec<SmolStr>),
|
||||
Object(IndexMap<SmolStr, Value>),
|
||||
NumberList(Box<Vec<Decimal>>),
|
||||
StringList(Box<Vec<SmolStr>>),
|
||||
Object(Box<IndexMap<SmolStr, Value>>),
|
||||
}
|
||||
|
||||
/// Type tag constants for serialization
|
||||
@@ -120,7 +120,7 @@ impl Value {
|
||||
bytes.push((list.len() >> 8) as u8);
|
||||
bytes.push(list.len() as u8);
|
||||
// List items
|
||||
for n in list {
|
||||
for n in list.iter() {
|
||||
bytes.extend_from_slice(&n.serialize());
|
||||
}
|
||||
}
|
||||
@@ -129,7 +129,7 @@ impl Value {
|
||||
bytes.push((list.len() >> 8) as u8);
|
||||
bytes.push(list.len() as u8);
|
||||
// List items
|
||||
for s in list {
|
||||
for s in list.iter() {
|
||||
// String length (2 bytes)
|
||||
bytes.push((s.len() >> 8) as u8);
|
||||
bytes.push(s.len() as u8);
|
||||
@@ -142,7 +142,7 @@ impl Value {
|
||||
bytes.push((map.len() >> 8) as u8);
|
||||
bytes.push(map.len() as u8);
|
||||
// Entries: key (string) + value (recursive)
|
||||
for (key, val) in map {
|
||||
for (key, val) in map.iter() {
|
||||
bytes.push((key.len() >> 8) as u8);
|
||||
bytes.push(key.len() as u8);
|
||||
bytes.extend_from_slice(key.as_bytes());
|
||||
@@ -209,19 +209,19 @@ impl From<SmolStr> for Value {
|
||||
|
||||
impl From<Vec<Decimal>> for Value {
|
||||
fn from(v: Vec<Decimal>) -> Self {
|
||||
Value::NumberList(v)
|
||||
Value::NumberList(Box::new(v))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<SmolStr>> for Value {
|
||||
fn from(v: Vec<SmolStr>) -> Self {
|
||||
Value::StringList(v)
|
||||
Value::StringList(Box::new(v))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IndexMap<SmolStr, Value>> for Value {
|
||||
fn from(m: IndexMap<SmolStr, Value>) -> Self {
|
||||
Value::Object(m)
|
||||
Value::Object(Box::new(m))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,7 +292,7 @@ impl Value {
|
||||
list.push(Decimal::deserialize(decimal_bytes));
|
||||
}
|
||||
|
||||
Ok((Value::NumberList(list), pos))
|
||||
Ok((Value::NumberList(Box::new(list)), pos))
|
||||
}
|
||||
TYPE_STRING_LIST => {
|
||||
if bytes.len() < pos + 2 {
|
||||
@@ -321,7 +321,7 @@ impl Value {
|
||||
list.push(s.into());
|
||||
}
|
||||
|
||||
Ok((Value::StringList(list), pos))
|
||||
Ok((Value::StringList(Box::new(list)), pos))
|
||||
}
|
||||
TYPE_OBJECT => {
|
||||
if bytes.len() < pos + 2 {
|
||||
@@ -354,7 +354,7 @@ impl Value {
|
||||
map.insert(key.into(), val);
|
||||
}
|
||||
|
||||
Ok((Value::Object(map), pos))
|
||||
Ok((Value::Object(Box::new(map)), pos))
|
||||
}
|
||||
_ => Err(format!("Unknown type tag: {}", type_tag)),
|
||||
}
|
||||
@@ -407,7 +407,7 @@ impl Value {
|
||||
serde_json::Value::String(s) => Ok(Value::String(SmolStr::new(s))),
|
||||
serde_json::Value::Array(arr) => {
|
||||
if arr.is_empty() {
|
||||
return Ok(Value::StringList(Vec::new()));
|
||||
return Ok(Value::StringList(Box::new(Vec::new())));
|
||||
}
|
||||
// Check if all elements are the same type
|
||||
let first = &arr[0];
|
||||
@@ -418,12 +418,12 @@ impl Value {
|
||||
nums.push(n);
|
||||
}
|
||||
}
|
||||
Ok(Value::NumberList(nums))
|
||||
Ok(Value::NumberList(Box::new(nums)))
|
||||
} else if first.is_string() && arr.iter().all(|v| v.is_string()) {
|
||||
let strings: Vec<SmolStr> = arr.iter()
|
||||
.filter_map(|v| v.as_str().map(SmolStr::new))
|
||||
.collect();
|
||||
Ok(Value::StringList(strings))
|
||||
Ok(Value::StringList(Box::new(strings)))
|
||||
} else {
|
||||
Err("Arrays must contain all numbers or all strings".to_string())
|
||||
}
|
||||
@@ -433,7 +433,7 @@ impl Value {
|
||||
for (k, v) in obj {
|
||||
map.insert(SmolStr::new(k), Self::from_json_value(v)?);
|
||||
}
|
||||
Ok(Value::Object(map))
|
||||
Ok(Value::Object(Box::new(map)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,9 +126,9 @@ impl<'a> BytecodeReader<'a> {
|
||||
self.read_byte()
|
||||
}
|
||||
|
||||
/// Read a string
|
||||
/// Read a string as a borrowed slice (zero-copy from bytecode)
|
||||
#[inline(always)]
|
||||
pub fn read_string(&mut self) -> Result<SmolStr, String> {
|
||||
pub fn read_str(&mut self) -> Result<&'a str, String> {
|
||||
let length = self.read_u16()? as usize;
|
||||
|
||||
if self.position + length > self.bytecode.len() {
|
||||
@@ -139,7 +139,13 @@ impl<'a> BytecodeReader<'a> {
|
||||
.map_err(|_| "Invalid UTF-8 string".to_string())?;
|
||||
self.position += length;
|
||||
|
||||
Ok(s.into())
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
/// Read a string as SmolStr (allocating — use read_str when possible)
|
||||
#[inline(always)]
|
||||
pub fn read_string(&mut self) -> Result<SmolStr, String> {
|
||||
self.read_str().map(SmolStr::from)
|
||||
}
|
||||
|
||||
/// Read a value
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
//! let mut customer = IndexMap::new();
|
||||
//! customer.insert(SmolStr::new("name"), Value::String("Alice".into()));
|
||||
//! customer.insert(SmolStr::new("age"), Value::Number(dec!(30)));
|
||||
//! info.add_value("customer", &Value::Object(customer), None);
|
||||
//! info.add_value("customer", &Value::Object(Box::new(customer)), None);
|
||||
//! info.add_value("price", &Value::Number(dec!(100)), None);
|
||||
//!
|
||||
//! let json = info.to_json();
|
||||
|
||||
@@ -6,66 +6,63 @@ use super::error::VMError;
|
||||
use super::vm::VM;
|
||||
|
||||
impl<'a> VM<'a> {
|
||||
/// Dispatch a method call on a value
|
||||
/// Dispatch a method call on a value.
|
||||
///
|
||||
/// Uses `std::mem::take` to temporarily move the value out of the register,
|
||||
/// avoiding clones when dispatching to type-specific handlers.
|
||||
pub(super) fn dispatch_method(
|
||||
&mut self,
|
||||
dest: usize,
|
||||
obj: usize,
|
||||
method: &SmolStr,
|
||||
method: &str,
|
||||
args: &[Value],
|
||||
) -> Result<(), VMError> {
|
||||
match &self.registers[obj] {
|
||||
Value::String(_) => self.dispatch_string_method(dest, obj, method, args),
|
||||
Value::StringList(_) => self.dispatch_string_list_method(dest, obj, method, args),
|
||||
Value::NumberList(_) => self.dispatch_number_list_method(dest, obj, method, args),
|
||||
Value::Object(_) => self.dispatch_object_method(dest, obj, method, args),
|
||||
// Take the value out to avoid borrow conflicts (register read + write).
|
||||
let obj_val = std::mem::take(&mut self.registers[obj]);
|
||||
|
||||
let result = match &obj_val {
|
||||
Value::String(_) => self.dispatch_string_method_inner(&obj_val, method, args),
|
||||
Value::StringList(_) => self.dispatch_string_list_method_inner(&obj_val, method, args),
|
||||
Value::NumberList(_) => self.dispatch_number_list_method_inner(&obj_val, method, args),
|
||||
Value::Object(_) => self.dispatch_object_method_inner(&obj_val, method, args),
|
||||
_ => {
|
||||
// Try external methods for any type
|
||||
let obj_val = &self.registers[obj];
|
||||
let type_name: SmolStr = obj_val.type_name().into();
|
||||
let key = (type_name.clone(), method.clone());
|
||||
let key = (type_name, SmolStr::from(method));
|
||||
if let Some(ext_method) = self.external_methods.as_ref().and_then(|m| m.get(&key)) {
|
||||
let result = ext_method(obj_val, args).map_err(VMError::RuntimeError)?;
|
||||
self.registers[dest] = result;
|
||||
Ok(())
|
||||
ext_method(&obj_val, args).map_err(VMError::RuntimeError)
|
||||
} else {
|
||||
Err(VMError::MethodNotFound {
|
||||
type_name: obj_val.type_name(),
|
||||
method: method.clone(),
|
||||
method: SmolStr::from(method),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Put the object back, then set dest (if dest == obj, result overwrites it — that's fine).
|
||||
self.registers[obj] = obj_val;
|
||||
self.registers[dest] = result?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn dispatch_string_method(
|
||||
&mut self,
|
||||
dest: usize,
|
||||
obj: usize,
|
||||
method: &SmolStr,
|
||||
fn dispatch_string_method_inner(
|
||||
&self,
|
||||
obj_val: &Value,
|
||||
method: &str,
|
||||
args: &[Value],
|
||||
) -> Result<(), VMError> {
|
||||
let s = match &self.registers[obj] {
|
||||
Value::String(s) => s.clone(),
|
||||
) -> Result<Value, VMError> {
|
||||
let s = match obj_val {
|
||||
Value::String(s) => s,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
match method.as_str() {
|
||||
"upper" => {
|
||||
self.registers[dest] = Value::String(s.to_uppercase_smolstr());
|
||||
}
|
||||
"lower" => {
|
||||
self.registers[dest] = Value::String(s.to_lowercase_smolstr());
|
||||
}
|
||||
"trim" => {
|
||||
self.registers[dest] = Value::String(SmolStr::new(s.trim()));
|
||||
}
|
||||
"trimStart" => {
|
||||
self.registers[dest] = Value::String(SmolStr::new(s.trim_start()));
|
||||
}
|
||||
"trimEnd" => {
|
||||
self.registers[dest] = Value::String(SmolStr::new(s.trim_end()));
|
||||
}
|
||||
match method {
|
||||
"upper" => Ok(Value::String(s.to_uppercase_smolstr())),
|
||||
"lower" => Ok(Value::String(s.to_lowercase_smolstr())),
|
||||
"trim" => Ok(Value::String(SmolStr::new(s.trim()))),
|
||||
"trimStart" => Ok(Value::String(SmolStr::new(s.trim_start()))),
|
||||
"trimEnd" => Ok(Value::String(SmolStr::new(s.trim_end()))),
|
||||
"split" => {
|
||||
if args.is_empty() {
|
||||
return Err(VMError::RuntimeError(
|
||||
@@ -75,13 +72,11 @@ impl<'a> VM<'a> {
|
||||
match &args[0] {
|
||||
Value::String(delim) => {
|
||||
let parts: Vec<SmolStr> = s.split(delim.as_str()).map(SmolStr::new).collect();
|
||||
self.registers[dest] = Value::StringList(parts);
|
||||
}
|
||||
_ => {
|
||||
return Err(VMError::RuntimeError(
|
||||
"split() requires a string delimiter".to_string(),
|
||||
));
|
||||
Ok(Value::StringList(Box::new(parts)))
|
||||
}
|
||||
_ => Err(VMError::RuntimeError(
|
||||
"split() requires a string delimiter".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
"replace" => {
|
||||
@@ -92,14 +87,11 @@ impl<'a> VM<'a> {
|
||||
}
|
||||
match (&args[0], &args[1]) {
|
||||
(Value::String(old), Value::String(new)) => {
|
||||
let result = SmolStr::new(s.replace(old.as_str(), new.as_str()));
|
||||
self.registers[dest] = Value::String(result);
|
||||
}
|
||||
_ => {
|
||||
return Err(VMError::RuntimeError(
|
||||
"replace() requires string arguments".to_string(),
|
||||
));
|
||||
Ok(Value::String(SmolStr::new(s.replace(old.as_str(), new.as_str()))))
|
||||
}
|
||||
_ => Err(VMError::RuntimeError(
|
||||
"replace() requires string arguments".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
"startsWith" => {
|
||||
@@ -109,14 +101,10 @@ impl<'a> VM<'a> {
|
||||
));
|
||||
}
|
||||
match &args[0] {
|
||||
Value::String(prefix) => {
|
||||
self.registers[dest] = Value::Boolean(s.starts_with(prefix.as_str()));
|
||||
}
|
||||
_ => {
|
||||
return Err(VMError::RuntimeError(
|
||||
"startsWith() requires a string prefix".to_string(),
|
||||
));
|
||||
}
|
||||
Value::String(prefix) => Ok(Value::Boolean(s.starts_with(prefix.as_str()))),
|
||||
_ => Err(VMError::RuntimeError(
|
||||
"startsWith() requires a string prefix".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
"endsWith" => {
|
||||
@@ -126,14 +114,10 @@ impl<'a> VM<'a> {
|
||||
));
|
||||
}
|
||||
match &args[0] {
|
||||
Value::String(suffix) => {
|
||||
self.registers[dest] = Value::Boolean(s.ends_with(suffix.as_str()));
|
||||
}
|
||||
_ => {
|
||||
return Err(VMError::RuntimeError(
|
||||
"endsWith() requires a string suffix".to_string(),
|
||||
));
|
||||
}
|
||||
Value::String(suffix) => Ok(Value::Boolean(s.ends_with(suffix.as_str()))),
|
||||
_ => Err(VMError::RuntimeError(
|
||||
"endsWith() requires a string suffix".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
"contains" => {
|
||||
@@ -143,19 +127,13 @@ impl<'a> VM<'a> {
|
||||
));
|
||||
}
|
||||
match &args[0] {
|
||||
Value::String(substr) => {
|
||||
self.registers[dest] = Value::Boolean(s.contains(substr.as_str()));
|
||||
}
|
||||
_ => {
|
||||
return Err(VMError::RuntimeError(
|
||||
"contains() requires a string substring".to_string(),
|
||||
));
|
||||
}
|
||||
Value::String(substr) => Ok(Value::Boolean(s.contains(substr.as_str()))),
|
||||
_ => Err(VMError::RuntimeError(
|
||||
"contains() requires a string substring".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
"length" => {
|
||||
self.registers[dest] = Value::Number(Decimal::from(s.len()));
|
||||
}
|
||||
"length" => Ok(Value::Number(Decimal::from(s.len()))),
|
||||
"charAt" => {
|
||||
if args.is_empty() {
|
||||
return Err(VMError::RuntimeError(
|
||||
@@ -166,19 +144,13 @@ impl<'a> VM<'a> {
|
||||
Value::Number(idx) => {
|
||||
let index = idx.to_u64().unwrap_or(u64::MAX) as usize;
|
||||
match s.chars().nth(index) {
|
||||
Some(c) => {
|
||||
self.registers[dest] = Value::String(SmolStr::new(c.to_string()));
|
||||
}
|
||||
None => {
|
||||
self.registers[dest] = Value::Null;
|
||||
}
|
||||
Some(c) => Ok(Value::String(SmolStr::new(c.to_string()))),
|
||||
None => Ok(Value::Null),
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(VMError::RuntimeError(
|
||||
"charAt() requires a number index".to_string(),
|
||||
));
|
||||
}
|
||||
_ => Err(VMError::RuntimeError(
|
||||
"charAt() requires a number index".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
"substring" => {
|
||||
@@ -201,68 +173,48 @@ impl<'a> VM<'a> {
|
||||
};
|
||||
|
||||
if start >= chars.len() || start >= end {
|
||||
self.registers[dest] = Value::String(SmolStr::new(""));
|
||||
Ok(Value::String(SmolStr::new("")))
|
||||
} else {
|
||||
let end = end.min(chars.len());
|
||||
let result: String = chars[start..end].iter().collect();
|
||||
self.registers[dest] = Value::String(SmolStr::new(result));
|
||||
Ok(Value::String(SmolStr::new(result)))
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
return Err(VMError::RuntimeError(
|
||||
"substring() requires a number start index".to_string(),
|
||||
));
|
||||
}
|
||||
_ => Err(VMError::RuntimeError(
|
||||
"substring() requires a number start index".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let key = (SmolStr::new_static("String"), method.clone());
|
||||
let key = (SmolStr::new_static("String"), SmolStr::from(method));
|
||||
if let Some(ext_method) = self.external_methods.as_ref().and_then(|m| m.get(&key)) {
|
||||
let obj_val = &self.registers[obj];
|
||||
let result = ext_method(obj_val, args).map_err(VMError::RuntimeError)?;
|
||||
self.registers[dest] = result;
|
||||
ext_method(obj_val, args).map_err(VMError::RuntimeError)
|
||||
} else {
|
||||
return Err(VMError::MethodNotFound {
|
||||
Err(VMError::MethodNotFound {
|
||||
type_name: "String",
|
||||
method: method.clone(),
|
||||
});
|
||||
method: SmolStr::from(method),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn dispatch_string_list_method(
|
||||
&mut self,
|
||||
dest: usize,
|
||||
obj: usize,
|
||||
method: &SmolStr,
|
||||
fn dispatch_string_list_method_inner(
|
||||
&self,
|
||||
obj_val: &Value,
|
||||
method: &str,
|
||||
args: &[Value],
|
||||
) -> Result<(), VMError> {
|
||||
let list = match &self.registers[obj] {
|
||||
Value::StringList(l) => l.clone(),
|
||||
) -> Result<Value, VMError> {
|
||||
let list = match obj_val {
|
||||
Value::StringList(l) => l,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
match method.as_str() {
|
||||
"length" | "len" => {
|
||||
self.registers[dest] = Value::Number(Decimal::from(list.len()));
|
||||
}
|
||||
"isEmpty" => {
|
||||
self.registers[dest] = Value::Boolean(list.is_empty());
|
||||
}
|
||||
"first" => {
|
||||
self.registers[dest] = list
|
||||
.first()
|
||||
.map(|s| Value::String(s.clone()))
|
||||
.unwrap_or(Value::Null);
|
||||
}
|
||||
"last" => {
|
||||
self.registers[dest] = list
|
||||
.last()
|
||||
.map(|s| Value::String(s.clone()))
|
||||
.unwrap_or(Value::Null);
|
||||
}
|
||||
match method {
|
||||
"length" | "len" => Ok(Value::Number(Decimal::from(list.len()))),
|
||||
"isEmpty" => Ok(Value::Boolean(list.is_empty())),
|
||||
"first" => Ok(list.first().map(|s| Value::String(s.clone())).unwrap_or(Value::Null)),
|
||||
"last" => Ok(list.last().map(|s| Value::String(s.clone())).unwrap_or(Value::Null)),
|
||||
"get" => {
|
||||
if args.is_empty() {
|
||||
return Err(VMError::RuntimeError("get() requires an index".to_string()));
|
||||
@@ -270,12 +222,9 @@ impl<'a> VM<'a> {
|
||||
match &args[0] {
|
||||
Value::Number(idx) => {
|
||||
let index = idx.to_usize().unwrap_or(usize::MAX);
|
||||
self.registers[dest] = list
|
||||
.get(index)
|
||||
.map(|s| Value::String(s.clone()))
|
||||
.unwrap_or(Value::Null);
|
||||
Ok(list.get(index).map(|s| Value::String(s.clone())).unwrap_or(Value::Null))
|
||||
}
|
||||
_ => return Err(VMError::RuntimeError("get() requires a number index".to_string())),
|
||||
_ => Err(VMError::RuntimeError("get() requires a number index".to_string())),
|
||||
}
|
||||
}
|
||||
"contains" => {
|
||||
@@ -283,10 +232,8 @@ impl<'a> VM<'a> {
|
||||
return Err(VMError::RuntimeError("contains() requires an argument".to_string()));
|
||||
}
|
||||
match &args[0] {
|
||||
Value::String(s) => {
|
||||
self.registers[dest] = Value::Boolean(list.contains(s));
|
||||
}
|
||||
_ => return Err(VMError::RuntimeError("contains() requires a string argument".to_string())),
|
||||
Value::String(s) => Ok(Value::Boolean(list.contains(s))),
|
||||
_ => Err(VMError::RuntimeError("contains() requires a string argument".to_string())),
|
||||
}
|
||||
}
|
||||
"indexOf" => {
|
||||
@@ -296,11 +243,9 @@ impl<'a> VM<'a> {
|
||||
match &args[0] {
|
||||
Value::String(s) => {
|
||||
let idx = list.iter().position(|item| item == s);
|
||||
self.registers[dest] = idx
|
||||
.map(|i| Value::Number(Decimal::from(i)))
|
||||
.unwrap_or(Value::Number(Decimal::from(-1)));
|
||||
Ok(idx.map(|i| Value::Number(Decimal::from(i))).unwrap_or(Value::Number(Decimal::from(-1))))
|
||||
}
|
||||
_ => return Err(VMError::RuntimeError("indexOf() requires a string argument".to_string())),
|
||||
_ => Err(VMError::RuntimeError("indexOf() requires a string argument".to_string())),
|
||||
}
|
||||
}
|
||||
"slice" => {
|
||||
@@ -318,20 +263,20 @@ impl<'a> VM<'a> {
|
||||
} else {
|
||||
list.len()
|
||||
};
|
||||
self.registers[dest] = Value::StringList(list[start..end].to_vec());
|
||||
Ok(Value::StringList(Box::new(list[start..end].to_vec())))
|
||||
}
|
||||
_ => return Err(VMError::RuntimeError("slice() requires a number index".to_string())),
|
||||
_ => Err(VMError::RuntimeError("slice() requires a number index".to_string())),
|
||||
}
|
||||
}
|
||||
"reverse" => {
|
||||
let mut reversed = list;
|
||||
let mut reversed = list.to_vec();
|
||||
reversed.reverse();
|
||||
self.registers[dest] = Value::StringList(reversed);
|
||||
Ok(Value::StringList(Box::new(reversed)))
|
||||
}
|
||||
"sort" => {
|
||||
let mut sorted = list;
|
||||
let mut sorted = list.to_vec();
|
||||
sorted.sort();
|
||||
self.registers[dest] = Value::StringList(sorted);
|
||||
Ok(Value::StringList(Box::new(sorted)))
|
||||
}
|
||||
"join" => {
|
||||
let delim = if args.is_empty() {
|
||||
@@ -343,56 +288,38 @@ impl<'a> VM<'a> {
|
||||
}
|
||||
};
|
||||
let result: String = list.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(delim);
|
||||
self.registers[dest] = Value::String(SmolStr::new(result));
|
||||
Ok(Value::String(SmolStr::new(result)))
|
||||
}
|
||||
_ => {
|
||||
let key = (SmolStr::new_static("StringList"), method.clone());
|
||||
let key = (SmolStr::new_static("StringList"), SmolStr::from(method));
|
||||
if let Some(ext_method) = self.external_methods.as_ref().and_then(|m| m.get(&key)) {
|
||||
let obj_val = &self.registers[obj];
|
||||
let result = ext_method(obj_val, args).map_err(VMError::RuntimeError)?;
|
||||
self.registers[dest] = result;
|
||||
ext_method(obj_val, args).map_err(VMError::RuntimeError)
|
||||
} else {
|
||||
return Err(VMError::MethodNotFound {
|
||||
Err(VMError::MethodNotFound {
|
||||
type_name: "StringList",
|
||||
method: method.clone(),
|
||||
});
|
||||
method: SmolStr::from(method),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn dispatch_number_list_method(
|
||||
&mut self,
|
||||
dest: usize,
|
||||
obj: usize,
|
||||
method: &SmolStr,
|
||||
fn dispatch_number_list_method_inner(
|
||||
&self,
|
||||
obj_val: &Value,
|
||||
method: &str,
|
||||
args: &[Value],
|
||||
) -> Result<(), VMError> {
|
||||
let list = match &self.registers[obj] {
|
||||
Value::NumberList(l) => l.clone(),
|
||||
) -> Result<Value, VMError> {
|
||||
let list = match obj_val {
|
||||
Value::NumberList(l) => l,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
match method.as_str() {
|
||||
"length" | "len" => {
|
||||
self.registers[dest] = Value::Number(Decimal::from(list.len()));
|
||||
}
|
||||
"isEmpty" => {
|
||||
self.registers[dest] = Value::Boolean(list.is_empty());
|
||||
}
|
||||
"first" => {
|
||||
self.registers[dest] = list
|
||||
.first()
|
||||
.map(|n| Value::Number(*n))
|
||||
.unwrap_or(Value::Null);
|
||||
}
|
||||
"last" => {
|
||||
self.registers[dest] = list
|
||||
.last()
|
||||
.map(|n| Value::Number(*n))
|
||||
.unwrap_or(Value::Null);
|
||||
}
|
||||
match method {
|
||||
"length" | "len" => Ok(Value::Number(Decimal::from(list.len()))),
|
||||
"isEmpty" => Ok(Value::Boolean(list.is_empty())),
|
||||
"first" => Ok(list.first().map(|n| Value::Number(*n)).unwrap_or(Value::Null)),
|
||||
"last" => Ok(list.last().map(|n| Value::Number(*n)).unwrap_or(Value::Null)),
|
||||
"get" => {
|
||||
if args.is_empty() {
|
||||
return Err(VMError::RuntimeError("get() requires an index".to_string()));
|
||||
@@ -400,12 +327,9 @@ impl<'a> VM<'a> {
|
||||
match &args[0] {
|
||||
Value::Number(idx) => {
|
||||
let index = idx.to_usize().unwrap_or(usize::MAX);
|
||||
self.registers[dest] = list
|
||||
.get(index)
|
||||
.map(|n| Value::Number(*n))
|
||||
.unwrap_or(Value::Null);
|
||||
Ok(list.get(index).map(|n| Value::Number(*n)).unwrap_or(Value::Null))
|
||||
}
|
||||
_ => return Err(VMError::RuntimeError("get() requires a number index".to_string())),
|
||||
_ => Err(VMError::RuntimeError("get() requires a number index".to_string())),
|
||||
}
|
||||
}
|
||||
"contains" => {
|
||||
@@ -413,10 +337,8 @@ impl<'a> VM<'a> {
|
||||
return Err(VMError::RuntimeError("contains() requires an argument".to_string()));
|
||||
}
|
||||
match &args[0] {
|
||||
Value::Number(n) => {
|
||||
self.registers[dest] = Value::Boolean(list.contains(n));
|
||||
}
|
||||
_ => return Err(VMError::RuntimeError("contains() requires a number argument".to_string())),
|
||||
Value::Number(n) => Ok(Value::Boolean(list.contains(n))),
|
||||
_ => Err(VMError::RuntimeError("contains() requires a number argument".to_string())),
|
||||
}
|
||||
}
|
||||
"indexOf" => {
|
||||
@@ -426,11 +348,9 @@ impl<'a> VM<'a> {
|
||||
match &args[0] {
|
||||
Value::Number(n) => {
|
||||
let idx = list.iter().position(|item| item == n);
|
||||
self.registers[dest] = idx
|
||||
.map(|i| Value::Number(Decimal::from(i)))
|
||||
.unwrap_or(Value::Number(Decimal::from(-1)));
|
||||
Ok(idx.map(|i| Value::Number(Decimal::from(i))).unwrap_or(Value::Number(Decimal::from(-1))))
|
||||
}
|
||||
_ => return Err(VMError::RuntimeError("indexOf() requires a number argument".to_string())),
|
||||
_ => Err(VMError::RuntimeError("indexOf() requires a number argument".to_string())),
|
||||
}
|
||||
}
|
||||
"slice" => {
|
||||
@@ -448,86 +368,70 @@ impl<'a> VM<'a> {
|
||||
} else {
|
||||
list.len()
|
||||
};
|
||||
self.registers[dest] = Value::NumberList(list[start..end].to_vec());
|
||||
Ok(Value::NumberList(Box::new(list[start..end].to_vec())))
|
||||
}
|
||||
_ => return Err(VMError::RuntimeError("slice() requires a number index".to_string())),
|
||||
_ => Err(VMError::RuntimeError("slice() requires a number index".to_string())),
|
||||
}
|
||||
}
|
||||
"reverse" => {
|
||||
let mut reversed = list;
|
||||
let mut reversed = list.to_vec();
|
||||
reversed.reverse();
|
||||
self.registers[dest] = Value::NumberList(reversed);
|
||||
Ok(Value::NumberList(Box::new(reversed)))
|
||||
}
|
||||
"sort" => {
|
||||
let mut sorted = list;
|
||||
let mut sorted = list.to_vec();
|
||||
sorted.sort();
|
||||
self.registers[dest] = Value::NumberList(sorted);
|
||||
Ok(Value::NumberList(Box::new(sorted)))
|
||||
}
|
||||
"sum" => {
|
||||
let sum: Decimal = list.iter().sum();
|
||||
self.registers[dest] = Value::Number(sum);
|
||||
Ok(Value::Number(sum))
|
||||
}
|
||||
"avg" => {
|
||||
if list.is_empty() {
|
||||
self.registers[dest] = Value::Null;
|
||||
Ok(Value::Null)
|
||||
} else {
|
||||
let sum: Decimal = list.iter().sum();
|
||||
let avg = sum / Decimal::from(list.len());
|
||||
self.registers[dest] = Value::Number(avg);
|
||||
Ok(Value::Number(avg))
|
||||
}
|
||||
}
|
||||
"min" => {
|
||||
self.registers[dest] = list
|
||||
.iter()
|
||||
.min()
|
||||
.map(|n| Value::Number(*n))
|
||||
.unwrap_or(Value::Null);
|
||||
}
|
||||
"max" => {
|
||||
self.registers[dest] = list
|
||||
.iter()
|
||||
.max()
|
||||
.map(|n| Value::Number(*n))
|
||||
.unwrap_or(Value::Null);
|
||||
}
|
||||
"min" => Ok(list.iter().min().map(|n| Value::Number(*n)).unwrap_or(Value::Null)),
|
||||
"max" => Ok(list.iter().max().map(|n| Value::Number(*n)).unwrap_or(Value::Null)),
|
||||
_ => {
|
||||
let key = (SmolStr::new_static("NumberList"), method.clone());
|
||||
let key = (SmolStr::new_static("NumberList"), SmolStr::from(method));
|
||||
if let Some(ext_method) = self.external_methods.as_ref().and_then(|m| m.get(&key)) {
|
||||
let obj_val = &self.registers[obj];
|
||||
let result = ext_method(obj_val, args).map_err(VMError::RuntimeError)?;
|
||||
self.registers[dest] = result;
|
||||
ext_method(obj_val, args).map_err(VMError::RuntimeError)
|
||||
} else {
|
||||
return Err(VMError::MethodNotFound {
|
||||
Err(VMError::MethodNotFound {
|
||||
type_name: "NumberList",
|
||||
method: method.clone(),
|
||||
});
|
||||
method: SmolStr::from(method),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn dispatch_object_method(
|
||||
&mut self,
|
||||
dest: usize,
|
||||
obj: usize,
|
||||
method: &SmolStr,
|
||||
fn dispatch_object_method_inner(
|
||||
&self,
|
||||
obj_val: &Value,
|
||||
method: &str,
|
||||
args: &[Value],
|
||||
) -> Result<(), VMError> {
|
||||
let map = match &self.registers[obj] {
|
||||
Value::Object(m) => m.clone(),
|
||||
) -> Result<Value, VMError> {
|
||||
let map = match obj_val {
|
||||
Value::Object(m) => m,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
match method.as_str() {
|
||||
match method {
|
||||
"keys" => {
|
||||
let keys: Vec<SmolStr> = map.keys().cloned().collect();
|
||||
self.registers[dest] = Value::StringList(keys);
|
||||
Ok(Value::StringList(Box::new(keys)))
|
||||
}
|
||||
"values" => {
|
||||
let vals: Vec<Value> = map.values().cloned().collect();
|
||||
if vals.is_empty() {
|
||||
self.registers[dest] = Value::StringList(Vec::new());
|
||||
Ok(Value::StringList(Box::new(Vec::new())))
|
||||
} else if vals.iter().all(|v| matches!(v, Value::String(_))) {
|
||||
let strings: Vec<SmolStr> = vals
|
||||
.into_iter()
|
||||
@@ -536,7 +440,7 @@ impl<'a> VM<'a> {
|
||||
_ => unreachable!(),
|
||||
})
|
||||
.collect();
|
||||
self.registers[dest] = Value::StringList(strings);
|
||||
Ok(Value::StringList(Box::new(strings)))
|
||||
} else if vals.iter().all(|v| matches!(v, Value::Number(_))) {
|
||||
let numbers: Vec<Decimal> = vals
|
||||
.into_iter()
|
||||
@@ -545,16 +449,14 @@ impl<'a> VM<'a> {
|
||||
_ => unreachable!(),
|
||||
})
|
||||
.collect();
|
||||
self.registers[dest] = Value::NumberList(numbers);
|
||||
Ok(Value::NumberList(Box::new(numbers)))
|
||||
} else {
|
||||
return Err(VMError::RuntimeError(
|
||||
Err(VMError::RuntimeError(
|
||||
"values() only works when all values are the same type (String or Number)".to_string(),
|
||||
));
|
||||
))
|
||||
}
|
||||
}
|
||||
"length" | "len" => {
|
||||
self.registers[dest] = Value::Number(Decimal::from(map.len()));
|
||||
}
|
||||
"length" | "len" => Ok(Value::Number(Decimal::from(map.len()))),
|
||||
"contains" => {
|
||||
if args.is_empty() {
|
||||
return Err(VMError::RuntimeError(
|
||||
@@ -562,14 +464,10 @@ impl<'a> VM<'a> {
|
||||
));
|
||||
}
|
||||
match &args[0] {
|
||||
Value::String(key) => {
|
||||
self.registers[dest] = Value::Boolean(map.contains_key(key));
|
||||
}
|
||||
_ => {
|
||||
return Err(VMError::RuntimeError(
|
||||
"contains() requires a string key".to_string(),
|
||||
))
|
||||
}
|
||||
Value::String(key) => Ok(Value::Boolean(map.contains_key(key))),
|
||||
_ => Err(VMError::RuntimeError(
|
||||
"contains() requires a string key".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
"get" => {
|
||||
@@ -579,30 +477,23 @@ impl<'a> VM<'a> {
|
||||
));
|
||||
}
|
||||
match &args[0] {
|
||||
Value::String(key) => {
|
||||
self.registers[dest] = map.get(key).cloned().unwrap_or(Value::Null);
|
||||
}
|
||||
_ => {
|
||||
return Err(VMError::RuntimeError(
|
||||
"get() requires a string key".to_string(),
|
||||
))
|
||||
}
|
||||
Value::String(key) => Ok(map.get(key).cloned().unwrap_or(Value::Null)),
|
||||
_ => Err(VMError::RuntimeError(
|
||||
"get() requires a string key".to_string(),
|
||||
)),
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
let key = (SmolStr::new_static("Object"), method.clone());
|
||||
let key = (SmolStr::new_static("Object"), SmolStr::from(method));
|
||||
if let Some(ext_method) = self.external_methods.as_ref().and_then(|m| m.get(&key)) {
|
||||
let obj_val = &self.registers[obj];
|
||||
let result = ext_method(obj_val, args).map_err(VMError::RuntimeError)?;
|
||||
self.registers[dest] = result;
|
||||
ext_method(obj_val, args).map_err(VMError::RuntimeError)
|
||||
} else {
|
||||
return Err(VMError::MethodNotFound {
|
||||
Err(VMError::MethodNotFound {
|
||||
type_name: "Object",
|
||||
method: method.clone(),
|
||||
});
|
||||
method: SmolStr::from(method),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
65
src/vm/vm.rs
65
src/vm/vm.rs
@@ -1,8 +1,8 @@
|
||||
use crate::{ast::value::Value, bytecode::BytecodeReader, opcodes::OpCodeByte};
|
||||
use bumpalo::Bump;
|
||||
use micromap::Map;
|
||||
use rust_decimal::{Decimal, MathematicalOps};
|
||||
use rustc_hash::FxHashMap;
|
||||
use smallvec::SmallVec;
|
||||
use smol_str::SmolStr;
|
||||
|
||||
/// Type alias for external (host) functions
|
||||
@@ -47,8 +47,6 @@ pub struct VM<'a> {
|
||||
|
||||
pub(super) external_methods: Option<FxHashMap<(SmolStr, SmolStr), ExternalMethod>>,
|
||||
|
||||
heap: Bump,
|
||||
|
||||
debug_info: Option<&'a DebugInfo>,
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
@@ -70,7 +68,6 @@ impl<'a> VM<'a> {
|
||||
last_result: Value::Null,
|
||||
external_functions: None,
|
||||
external_methods: None,
|
||||
heap: Bump::new(),
|
||||
debug_info: None,
|
||||
#[cfg(debug_assertions)]
|
||||
debug: false,
|
||||
@@ -144,7 +141,6 @@ impl<'a> VM<'a> {
|
||||
self.registers = [const { Value::Null }; MAX_REGISTERS];
|
||||
self.last_result = Value::Null;
|
||||
// Preserve globals
|
||||
self.heap = Bump::new();
|
||||
}
|
||||
|
||||
/// Execute the bytecode program and return the last expression result
|
||||
@@ -402,12 +398,12 @@ impl<'a> VM<'a> {
|
||||
#[inline]
|
||||
fn handle_load_global(&mut self) -> Result<(), VMError> {
|
||||
let reg = self.read_register_checked()?;
|
||||
let name = self.reader.read_string().map_err(VMError::BytecodeError)?;
|
||||
let name = self.reader.read_str().map_err(VMError::BytecodeError)?;
|
||||
|
||||
let value = self
|
||||
.globals
|
||||
.get(&name)
|
||||
.ok_or_else(|| VMError::UndefinedVariable(name.clone()))?;
|
||||
.get(name)
|
||||
.ok_or_else(|| VMError::UndefinedVariable(SmolStr::from(name)))?;
|
||||
self.registers[reg] = value.clone();
|
||||
|
||||
log_debug!(self,
|
||||
@@ -420,12 +416,12 @@ impl<'a> VM<'a> {
|
||||
/// Handle StoreGlobal opcode - store register to global variable
|
||||
#[inline]
|
||||
fn handle_store_global(&mut self) -> Result<(), VMError> {
|
||||
let name = self.reader.read_string().map_err(VMError::BytecodeError)?;
|
||||
let name = self.reader.read_str().map_err(VMError::BytecodeError)?;
|
||||
let reg = self.read_register_checked()?;
|
||||
|
||||
self
|
||||
.globals
|
||||
.insert(name.clone(), self.registers[reg].clone());
|
||||
.insert(SmolStr::from(name), self.registers[reg].clone());
|
||||
|
||||
log_debug!(self,
|
||||
"StoreGlobal global.{} = r{} ({})",
|
||||
@@ -476,15 +472,23 @@ impl<'a> VM<'a> {
|
||||
self.registers[dest] = Value::Number(*a_num + *b_num);
|
||||
}
|
||||
(Value::String(a_str), Value::String(b_str)) => {
|
||||
let result = format!("{}{}", a_str, b_str);
|
||||
let mut result = String::with_capacity(a_str.len() + b_str.len());
|
||||
result.push_str(a_str);
|
||||
result.push_str(b_str);
|
||||
self.registers[dest] = Value::String(result.into());
|
||||
}
|
||||
(Value::String(a_str), other) => {
|
||||
let result = format!("{}{}", a_str, value_to_string(other));
|
||||
let b_cow = value_to_string(other);
|
||||
let mut result = String::with_capacity(a_str.len() + b_cow.len());
|
||||
result.push_str(a_str);
|
||||
result.push_str(&b_cow);
|
||||
self.registers[dest] = Value::String(result.into());
|
||||
}
|
||||
(other, Value::String(b_str)) => {
|
||||
let result = format!("{}{}", value_to_string(other), b_str);
|
||||
let a_cow = value_to_string(other);
|
||||
let mut result = String::with_capacity(a_cow.len() + b_str.len());
|
||||
result.push_str(&a_cow);
|
||||
result.push_str(b_str);
|
||||
self.registers[dest] = Value::String(result.into());
|
||||
}
|
||||
(a_val, b_val) => {
|
||||
@@ -657,11 +661,11 @@ impl<'a> VM<'a> {
|
||||
let a = self.read_register_checked()?;
|
||||
let b = self.read_register_checked()?;
|
||||
|
||||
let result = format!(
|
||||
"{}{}",
|
||||
value_to_string(&self.registers[a]),
|
||||
value_to_string(&self.registers[b])
|
||||
);
|
||||
let a_cow = value_to_string(&self.registers[a]);
|
||||
let b_cow = value_to_string(&self.registers[b]);
|
||||
let mut result = String::with_capacity(a_cow.len() + b_cow.len());
|
||||
result.push_str(&a_cow);
|
||||
result.push_str(&b_cow);
|
||||
self.registers[dest] = Value::String(result.into());
|
||||
|
||||
log_debug!(self, "Concat r{} = r{} + r{}", dest, a, b);
|
||||
@@ -672,11 +676,11 @@ impl<'a> VM<'a> {
|
||||
fn handle_get_property(&mut self) -> Result<(), VMError> {
|
||||
let dest = self.read_register_checked()?;
|
||||
let obj = self.read_register_checked()?;
|
||||
let prop = self.reader.read_string().map_err(VMError::BytecodeError)?;
|
||||
let prop = self.reader.read_str().map_err(VMError::BytecodeError)?;
|
||||
|
||||
match &self.registers[obj] {
|
||||
Value::Object(map) => {
|
||||
let value = map.get(&prop).cloned().unwrap_or(Value::Null);
|
||||
let value = map.get(prop).cloned().unwrap_or(Value::Null);
|
||||
self.registers[dest] = value;
|
||||
}
|
||||
other => {
|
||||
@@ -694,13 +698,13 @@ impl<'a> VM<'a> {
|
||||
/// Handle SetProperty opcode - set a field on an Object (in-place on register)
|
||||
fn handle_set_property(&mut self) -> Result<(), VMError> {
|
||||
let obj = self.read_register_checked()?;
|
||||
let prop = self.reader.read_string().map_err(VMError::BytecodeError)?;
|
||||
let prop = self.reader.read_str().map_err(VMError::BytecodeError)?;
|
||||
let val = self.read_register_checked()?;
|
||||
|
||||
let value = self.registers[val].clone();
|
||||
match &mut self.registers[obj] {
|
||||
Value::Object(map) => {
|
||||
map.insert(prop.clone(), value);
|
||||
map.insert(SmolStr::from(prop), value);
|
||||
}
|
||||
other => {
|
||||
return Err(VMError::RuntimeError(format!(
|
||||
@@ -718,21 +722,21 @@ impl<'a> VM<'a> {
|
||||
fn handle_method_call(&mut self) -> Result<(), VMError> {
|
||||
let dest = self.read_register_checked()?;
|
||||
let obj = self.read_register_checked()?;
|
||||
let method = self.reader.read_string().map_err(VMError::BytecodeError)?;
|
||||
let method = self.reader.read_str().map_err(VMError::BytecodeError)?;
|
||||
|
||||
// Read argument registers
|
||||
let arg_count = self
|
||||
.reader
|
||||
.read_byte()
|
||||
.map_err(VMError::BytecodeError)? as usize;
|
||||
let mut args = Vec::with_capacity(arg_count);
|
||||
let mut args: SmallVec<[Value; 4]> = SmallVec::with_capacity(arg_count);
|
||||
|
||||
for _ in 0..arg_count {
|
||||
let reg = self.read_register_checked()?;
|
||||
args.push(self.registers[reg].clone());
|
||||
}
|
||||
|
||||
self.dispatch_method(dest, obj, &method, &args)?;
|
||||
self.dispatch_method(dest, obj, method, &args)?;
|
||||
|
||||
log_debug!(self, "MethodCall r{} = r{}.{}(...)", dest, obj, method);
|
||||
Ok(())
|
||||
@@ -748,7 +752,7 @@ impl<'a> VM<'a> {
|
||||
let fn_id = self.reader.read_byte().map_err(VMError::BytecodeError)?;
|
||||
let arg_count = self.reader.read_byte().map_err(VMError::BytecodeError)? as usize;
|
||||
|
||||
let mut arg_regs = Vec::with_capacity(arg_count);
|
||||
let mut arg_regs: SmallVec<[usize; 4]> = SmallVec::with_capacity(arg_count);
|
||||
for _ in 0..arg_count {
|
||||
arg_regs.push(self.read_register_checked()?);
|
||||
}
|
||||
@@ -762,20 +766,21 @@ impl<'a> VM<'a> {
|
||||
/// Handle CallExternal opcode - call host function by name
|
||||
fn handle_call_external(&mut self) -> Result<(), VMError> {
|
||||
let dest = self.read_register_checked()?;
|
||||
let name = self.reader.read_string().map_err(VMError::BytecodeError)?;
|
||||
let name = self.reader.read_str().map_err(VMError::BytecodeError)?;
|
||||
let arg_count = self.reader.read_byte().map_err(VMError::BytecodeError)? as usize;
|
||||
|
||||
let mut args = Vec::with_capacity(arg_count);
|
||||
let mut args: SmallVec<[Value; 4]> = SmallVec::with_capacity(arg_count);
|
||||
for _ in 0..arg_count {
|
||||
let reg = self.read_register_checked()?;
|
||||
args.push(self.registers[reg].clone());
|
||||
}
|
||||
|
||||
let name_key = SmolStr::from(name);
|
||||
let func = self
|
||||
.external_functions
|
||||
.as_ref()
|
||||
.and_then(|m| m.get(&name))
|
||||
.ok_or_else(|| VMError::RuntimeError(format!("Undefined function: {}", name)))?;
|
||||
.and_then(|m| m.get(&name_key))
|
||||
.ok_or_else(|| VMError::RuntimeError(format!("Undefined function: {}", name_key)))?;
|
||||
|
||||
let result = func(&args).map_err(VMError::RuntimeError)?;
|
||||
self.registers[dest] = result;
|
||||
|
||||
@@ -55,7 +55,7 @@ fn value_def_to_value(def: &ValueDef) -> Value {
|
||||
};
|
||||
map.insert(SmolStr::from(k.as_str()), val);
|
||||
}
|
||||
Value::Object(map)
|
||||
Value::Object(Box::new(map))
|
||||
}
|
||||
let obj = def.value.as_ref().unwrap().as_object().unwrap();
|
||||
json_obj_to_value(obj)
|
||||
|
||||
@@ -628,7 +628,7 @@ fn test_numberlist_contains() {
|
||||
let mut compiler = Compiler::new();
|
||||
let bytecode = compiler.compile(ast).expect("compile");
|
||||
let mut vm = VM::new(&bytecode);
|
||||
vm.set_global("nums", Value::NumberList(vec![dec!(1), dec!(2), dec!(3), dec!(4)]));
|
||||
vm.set_global("nums", Value::NumberList(Box::new(vec![dec!(1), dec!(2), dec!(3), dec!(4)])));
|
||||
vm.execute().expect("execute");
|
||||
assert_eq!(vm.get_global("result").unwrap(), &Value::Boolean(true));
|
||||
}
|
||||
@@ -639,7 +639,7 @@ fn test_numberlist_indexof() {
|
||||
let mut compiler = Compiler::new();
|
||||
let bytecode = compiler.compile(ast).expect("compile");
|
||||
let mut vm = VM::new(&bytecode);
|
||||
vm.set_global("nums", Value::NumberList(vec![dec!(1), dec!(2), dec!(3)]));
|
||||
vm.set_global("nums", Value::NumberList(Box::new(vec![dec!(1), dec!(2), dec!(3)])));
|
||||
vm.execute().expect("execute");
|
||||
assert_eq!(vm.get_global("result").unwrap(), &Value::Number(dec!(2)));
|
||||
}
|
||||
@@ -650,7 +650,7 @@ fn test_numberlist_slice() {
|
||||
let mut compiler = Compiler::new();
|
||||
let bytecode = compiler.compile(ast).expect("compile");
|
||||
let mut vm = VM::new(&bytecode);
|
||||
vm.set_global("nums", Value::NumberList(vec![dec!(10), dec!(20), dec!(30), dec!(40)]));
|
||||
vm.set_global("nums", Value::NumberList(Box::new(vec![dec!(10), dec!(20), dec!(30), dec!(40)])));
|
||||
vm.execute().expect("execute");
|
||||
// slice(1,3) = [20, 30], sum = 50
|
||||
assert_eq!(vm.get_global("result").unwrap(), &Value::Number(dec!(50)));
|
||||
@@ -662,7 +662,7 @@ fn test_numberlist_reverse() {
|
||||
let mut compiler = Compiler::new();
|
||||
let bytecode = compiler.compile(ast).expect("compile");
|
||||
let mut vm = VM::new(&bytecode);
|
||||
vm.set_global("nums", Value::NumberList(vec![dec!(1), dec!(2), dec!(3)]));
|
||||
vm.set_global("nums", Value::NumberList(Box::new(vec![dec!(1), dec!(2), dec!(3)])));
|
||||
vm.execute().expect("execute");
|
||||
assert_eq!(vm.get_global("result").unwrap(), &Value::Number(dec!(3)));
|
||||
}
|
||||
@@ -673,7 +673,7 @@ fn test_numberlist_sort() {
|
||||
let mut compiler = Compiler::new();
|
||||
let bytecode = compiler.compile(ast).expect("compile");
|
||||
let mut vm = VM::new(&bytecode);
|
||||
vm.set_global("nums", Value::NumberList(vec![dec!(30), dec!(10), dec!(20)]));
|
||||
vm.set_global("nums", Value::NumberList(Box::new(vec![dec!(30), dec!(10), dec!(20)])));
|
||||
vm.execute().expect("execute");
|
||||
assert_eq!(vm.get_global("result").unwrap(), &Value::Number(dec!(10)));
|
||||
}
|
||||
@@ -684,7 +684,7 @@ fn test_numberlist_isempty() {
|
||||
let mut compiler = Compiler::new();
|
||||
let bytecode = compiler.compile(ast).expect("compile");
|
||||
let mut vm = VM::new(&bytecode);
|
||||
vm.set_global("nums", Value::NumberList(vec![]));
|
||||
vm.set_global("nums", Value::NumberList(Box::new(vec![])));
|
||||
vm.execute().expect("execute");
|
||||
assert_eq!(vm.get_global("result").unwrap(), &Value::Boolean(true));
|
||||
}
|
||||
@@ -1028,7 +1028,7 @@ fn make_object(entries: Vec<(&str, Value)>) -> Value {
|
||||
for (k, v) in entries {
|
||||
map.insert(SmolStr::new(k), v);
|
||||
}
|
||||
Value::Object(map)
|
||||
Value::Object(Box::new(map))
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1113,7 +1113,7 @@ fn test_object_method_keys() {
|
||||
let result = run_expr_with_globals("person.keys()", vec![("person", obj)]);
|
||||
assert_eq!(
|
||||
result,
|
||||
Value::StringList(vec![SmolStr::new("name"), SmolStr::new("age")])
|
||||
Value::StringList(Box::new(vec![SmolStr::new("name"), SmolStr::new("age")]))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1177,7 +1177,7 @@ fn test_object_method_values_strings() {
|
||||
let result = run_expr_with_globals("obj.values()", vec![("obj", obj)]);
|
||||
assert_eq!(
|
||||
result,
|
||||
Value::StringList(vec![SmolStr::new("x"), SmolStr::new("y")])
|
||||
Value::StringList(Box::new(vec![SmolStr::new("x"), SmolStr::new("y")]))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1188,7 +1188,7 @@ fn test_object_method_values_numbers() {
|
||||
("b", Value::Number(dec!(2))),
|
||||
]);
|
||||
let result = run_expr_with_globals("obj.values()", vec![("obj", obj)]);
|
||||
assert_eq!(result, Value::NumberList(vec![dec!(1), dec!(2)]));
|
||||
assert_eq!(result, Value::NumberList(Box::new(vec![dec!(1), dec!(2)])));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user