Compare commits

...

3 Commits

Author SHA1 Message Date
953b39d433 perf improvements 2026-04-06 02:51:42 +03:00
b0ea71e104 performance improvements 2026-04-06 00:04:40 +03:00
7582c5aee7 refactor 2026-04-05 23:05:31 +03:00
31 changed files with 2633 additions and 1274 deletions

1
Cargo.lock generated
View File

@@ -315,6 +315,7 @@ dependencies = [
"rust_decimal", "rust_decimal",
"rust_decimal_macros", "rust_decimal_macros",
"rustc-hash", "rustc-hash",
"serde",
"serde_json", "serde_json",
"smallvec", "smallvec",
"smol_str", "smol_str",

View File

@@ -2,6 +2,9 @@
name = "dexpr" name = "dexpr"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2021"
description = "Embeddable expression evaluator and bytecode VM"
license = "MIT"
exclude = ["editor/", "wasm/", "docs/", ".vscode/", "benches/", "scripts/", "CLAUDE.md", "flamegraph.svg", "profile.json.gz", "gen.js", "*.dexpr", "*.txt", "src/main.rs"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
@@ -35,6 +38,7 @@ serde_json = "1"
[dev-dependencies] [dev-dependencies]
criterion = { version = "0.8.2", features = ["html_reports"] } criterion = { version = "0.8.2", features = ["html_reports"] }
serde = { version = "1", features = ["derive"] }
[[bench]] [[bench]]
name = "my_benchmark" name = "my_benchmark"

View File

@@ -1,21 +1,39 @@
use criterion::{criterion_group, criterion_main, Criterion}; use criterion::{criterion_group, criterion_main, Criterion};
use dexpr::{ast::value::Value, compiler::Compiler, parser, vm::VM}; use dexpr::{ast::value::Value, compiler::Compiler, parser, vm::VM};
use indexmap::IndexMap;
use rust_decimal::Decimal;
use rust_decimal_macros::dec; use rust_decimal_macros::dec;
use smol_str::SmolStr;
use std::rc::Rc;
/// 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(Rc::new(map))
}
pub fn criterion_benchmark(c: &mut Criterion) { pub fn criterion_benchmark(c: &mut Criterion) {
// 1. Parser Benchmark // ── Existing benchmarks ──────────────────────────────────────────────
c.bench_function("parser_long", |b| { c.bench_function("parser_long", |b| {
let input = include_str!("../src/bench_long.dexpr"); let input = include_str!("../examples/bench_long.dexpr");
b.iter(|| { b.iter(|| {
let _ = parser::program(input).unwrap(); let _ = parser::program(input).unwrap();
}) })
}); });
// 2. Compiler Benchmark
c.bench_function("compiler_long", |b| { c.bench_function("compiler_long", |b| {
let input = include_str!("../src/bench_long.dexpr"); let input = include_str!("../examples/bench_long.dexpr");
let ast = parser::program(input).unwrap(); let ast = parser::program(input).unwrap();
b.iter(|| { b.iter(|| {
let mut compiler = Compiler::new(); let mut compiler = Compiler::new();
@@ -23,11 +41,8 @@ pub fn criterion_benchmark(c: &mut Criterion) {
}) })
}); });
// 3. VM Benchmarks
// basic_long.dexpr benchmark
c.bench_function("vm_basic_long", |b| { c.bench_function("vm_basic_long", |b| {
let input = include_str!("../src/basic_long.dexpr"); let input = include_str!("../examples/basic_long.dexpr");
let ast = parser::program(input).unwrap(); let ast = parser::program(input).unwrap();
let mut compiler = Compiler::new(); let mut compiler = Compiler::new();
let bytecode = compiler.compile(ast).unwrap(); let bytecode = compiler.compile(ast).unwrap();
@@ -38,9 +53,8 @@ pub fn criterion_benchmark(c: &mut Criterion) {
}) })
}); });
// Long code benchmark (using bench_long.dexpr)
c.bench_function("vm_long", |b| { c.bench_function("vm_long", |b| {
let input = include_str!("../src/bench_long.dexpr"); let input = include_str!("../examples/bench_long.dexpr");
let ast = parser::program(input).unwrap(); let ast = parser::program(input).unwrap();
let mut compiler = Compiler::new(); let mut compiler = Compiler::new();
let bytecode = compiler.compile(ast).unwrap(); let bytecode = compiler.compile(ast).unwrap();
@@ -50,12 +64,9 @@ pub fn criterion_benchmark(c: &mut Criterion) {
}) })
}); });
// Short code benchmark
c.bench_function("vm_short", |b| { c.bench_function("vm_short", |b| {
let input = "5.12 + test * 1.5"; let input = "5.12 + test * 1.5";
let ast = parser::program(input).unwrap(); let bytecode = compile(input);
let mut compiler = Compiler::new();
let bytecode = compiler.compile(ast).unwrap();
let test_val = dec!(100); let test_val = dec!(100);
b.iter(|| { b.iter(|| {
let mut vm = VM::new(&bytecode); let mut vm = VM::new(&bytecode);
@@ -63,6 +74,220 @@ pub fn criterion_benchmark(c: &mut Criterion) {
let _ = vm.execute().unwrap(); 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(Rc::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(Rc::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); criterion_group!(benches, criterion_benchmark);

View File

@@ -96,8 +96,9 @@ Parse ile birlikte pozisyon bilgisi de toplar ve `DebugInfo` üretir.
2. Sağ operandı register'a derle 2. Sağ operandı register'a derle
3. Sonuç register'ı ayır 3. Sonuç register'ı ayır
4. Uygun opcode'u emit et (Add, Sub, Mul, vs.) 4. Uygun opcode'u emit et (Add, Sub, Mul, vs.)
5. **Özel durum:** String + String → `Concat` kullanılır 5. Operand register'ları serbest bırak
6. Operand register'ları serbest bırak
> **Not:** String birleştirme derleme zamanında ayırt edilmez. `Op::Add` her zaman `OpCodeByte::Add` emit eder; string birleştirme ve otomatik tip dönüşümü VM tarafından çalışma zamanında (runtime) ele alınır.
### UnaryOp (Tekli Operasyon) ### UnaryOp (Tekli Operasyon)
1. Operandı register'a derle 1. Operandı register'a derle

View File

@@ -85,6 +85,25 @@ Bytecode komut setini (instruction set) tanımlar. Her opcode bir `u8` değerine
--- ---
## Built-in Fonksiyon ID'leri
`default_fn` modülü, built-in fonksiyonlar için sabit ID'ler tanımlar. `CallExternal` opcode'u bu ID'leri kullanarak built-in fonksiyonları çağırır.
| Sabit | ID | Fonksiyon |
|-------|----|-----------|
| `ABS` | `1` | Mutlak değer |
| `MIN` | `2` | Minimum değer |
| `MAX` | `3` | Maksimum değer |
| `FLOOR` | `4` | Aşağı yuvarlama |
| `CEIL` | `5` | Yukarı yuvarlama |
| `ROUND` | `6` | Yuvarlama |
| `SQRT` | `7` | Karekök |
| `LEN` | `8` | Uzunluk |
| `TO_STRING` | `9` | String'e dönüştür |
| `TO_NUMBER` | `10` | Sayıya dönüştür |
---
## Hızlı Lookup Tablosu ## Hızlı Lookup Tablosu
`LOOKUP[256]` statik dizisi, O(1) karmaşıklıkta byte-to-opcode dönüşümü sağlar. `from_byte(u8)` metodu bu tabloyu kullanır. `LOOKUP[256]` statik dizisi, O(1) karmaşıklıkta byte-to-opcode dönüşümü sağlar. `from_byte(u8)` metodu bu tabloyu kullanır.

View File

@@ -11,7 +11,9 @@ Register tabanlı sanal makine. Bytecode'u çalıştırır, 8 register ve global
| Dosya | İçerik | | Dosya | İçerik |
|-------|--------| |-------|--------|
| `vm/mod.rs` | Modül export'ları | | `vm/mod.rs` | Modül export'ları |
| `vm/vm.rs` | Ana VM implementasyonu | | `vm/vm.rs` | Ana VM implementasyonu (core çalıştırma döngüsü) |
| `vm/methods.rs` | Metod dispatch (String, StringList, NumberList, Object metodları) |
| `vm/builtins.rs` | Built-in fonksiyon implementasyonları (abs, min, max, floor, ceil, round, sqrt, len, toString, toNumber) |
| `vm/error.rs` | Hata türleri (VMError) | | `vm/error.rs` | Hata türleri (VMError) |
| `vm/debug_info.rs` | Bytecode offset → kaynak konum eşleştirme | | `vm/debug_info.rs` | Bytecode offset → kaynak konum eşleştirme |
@@ -120,6 +122,7 @@ struct VM<'a> {
### Aritmetik ### Aritmetik
- **`binary_op(f, name)`** — Genel handler: iki operand register'ı oku, fonksiyonu uygula, sonucu kaydet - **`binary_op(f, name)`** — Genel handler: iki operand register'ı oku, fonksiyonu uygula, sonucu kaydet
- Sıfıra bölme kontrolü yapılır - Sıfıra bölme kontrolü yapılır
- **`Add` opcode:** Sayısal toplama yanında string birleştirmeyi de destekler. Otomatik tip dönüşümü (auto-coercion) yapılır: String+String, String+Number, Number+String, String+Boolean kombinasyonları birleştirme olarak çalışır
- **`handle_neg()`** — Sadece Number tipinde tekli negatif - **`handle_neg()`** — Sadece Number tipinde tekli negatif
### Karşılaştırma ### Karşılaştırma
@@ -133,7 +136,7 @@ struct VM<'a> {
- **`handle_jump_if_false()`** — Register `Boolean(false)` ise atla - **`handle_jump_if_false()`** — Register `Boolean(false)` ise atla
### String, Nesne ve Metodlar ### String, Nesne ve Metodlar
- **`handle_concat()`** — İki String register'ını birleştir - **`handle_concat()`** — İki register'ı birleştir (karışık tip dönüşümü destekler: String, Number, Boolean otomatik olarak String'e dönüştürülür)
- **`handle_get_property()`** — Object register'ından alan oku, alan yoksa `Null` döndür - **`handle_get_property()`** — Object register'ından alan oku, alan yoksa `Null` döndür
- **`handle_set_property()`** — Object register'ında alan değerini ayarla - **`handle_set_property()`** — Object register'ında alan değerini ayarla
- **`handle_method_call()`** — Nesne register'ı, metod adı, argümanlar - **`handle_method_call()`** — Nesne register'ı, metod adı, argümanlar
@@ -153,6 +156,18 @@ struct VM<'a> {
### Built-in ### Built-in
- **`handle_log()`** — Register değerini stdout'a yazdır - **`handle_log()`** — Register değerini stdout'a yazdır
- **`rand(min, max)`** — min ile max arasında rastgele tamsayı üret (varsayılan harici fonksiyon) - **`rand(min, max)`** — min ile max arasında rastgele tamsayı üret (varsayılan harici fonksiyon)
- **`abs(n)`** — Mutlak değer
- **`min(a, b, ...)`** — Verilen değerlerin minimumu
- **`max(a, b, ...)`** — Verilen değerlerin maksimumu
- **`floor(n)`** — Aşağı yuvarlama
- **`ceil(n)`** — Yukarı yuvarlama
- **`round(n[, places])`** — Yuvarlama (opsiyonel ondalık basamak sayısı)
- **`sqrt(n)`** — Karekök
- **`len(v)`** — Değerin uzunluğu (String, List, Object)
- **`toString(v)`** — Değeri String'e dönüştür
- **`toNumber(v)`** — Değeri Number'a dönüştür
> **Not:** Built-in fonksiyon implementasyonları `vm/builtins.rs` dosyasında, sabit ID tanımları `src/opcodes.rs` içindeki `default_fn` modülündedir.
--- ---

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 48 KiB

74
gen.js
View File

@@ -1,74 +0,0 @@
function randomBetween(min, max) {
min = Math.round(min);
max = Math.round(max);
return Math.floor(Math.random() * (max - min + 1) + min);
}
function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
}
function generateRandomArithmeticExpression() {
const operators = ['+', '-', '*', '/']; // Array of arithmetic operators
const maxDecimalPlaces = 5;
const numOperands = randomBetween(10, 20);
const numParentheses = randomBetween(4, 8);
// Generate random numbers with two decimal places
let nums = [];
for (let i = 0; i < numOperands; i++) {
nums.push((Math.random() * 100).toFixed(randomBetween(1, maxDecimalPlaces)));
}
let res = [];
shuffleArray(nums);
let numRemainingOperands = nums.length;
let openParentheses = 0;
for (let i = 0; i < nums.length; i++) {
// Open a parenthesis if there are enough operands remaining
if (openParentheses < numParentheses && numRemainingOperands > 1 && Math.random() < 0.5) {
res.push('(');
openParentheses++;
}
res.push(nums[i]);
// Close a parenthesis if there are enough operands preceding it
if (openParentheses > 0 && numRemainingOperands > 2 && Math.random() < 0.5) {
res.push(')');
openParentheses--;
}
if (i < nums.length - 1) {
res.push(operators[randomBetween(0, 3)]);
}
numRemainingOperands--;
}
// Close any remaining open parentheses
while (openParentheses > 0) {
res.push(')');
openParentheses--;
}
return res.join('');
}
let res = '';
for (let i = 0; i < 100; i++) {
let expr = generateRandomArithmeticExpression();
let val = eval(expr);
// res += `("${expr}", "${val}"),\n`;
res += `"${expr}",\n`;
}
console.log(res);

View File

@@ -12,6 +12,12 @@ bench:
run: run:
cargo run --release cargo run --release
# --- Publish ---
# Publish to Gitea cargo registry
publish:
cargo publish --registry gitea --allow-dirty
# --- WASM --- # --- WASM ---
# Build wasm package (web target) # Build wasm package (web target)

Binary file not shown.

View File

@@ -2,6 +2,7 @@ use indexmap::IndexMap;
use rust_decimal::Decimal; use rust_decimal::Decimal;
use smol_str::SmolStr; use smol_str::SmolStr;
use std::fmt; use std::fmt;
use std::rc::Rc;
/// Value type for the dExpr language /// Value type for the dExpr language
#[derive(Debug, Clone, PartialEq, Default)] #[derive(Debug, Clone, PartialEq, Default)]
@@ -11,9 +12,9 @@ pub enum Value {
Number(Decimal), Number(Decimal),
String(SmolStr), String(SmolStr),
Boolean(bool), Boolean(bool),
NumberList(Vec<Decimal>), NumberList(Rc<Vec<Decimal>>),
StringList(Vec<SmolStr>), StringList(Rc<Vec<SmolStr>>),
Object(IndexMap<SmolStr, Value>), Object(Rc<IndexMap<SmolStr, Value>>),
} }
/// Type tag constants for serialization /// Type tag constants for serialization
@@ -120,7 +121,7 @@ impl Value {
bytes.push((list.len() >> 8) as u8); bytes.push((list.len() >> 8) as u8);
bytes.push(list.len() as u8); bytes.push(list.len() as u8);
// List items // List items
for n in list { for n in list.iter() {
bytes.extend_from_slice(&n.serialize()); bytes.extend_from_slice(&n.serialize());
} }
} }
@@ -129,7 +130,7 @@ impl Value {
bytes.push((list.len() >> 8) as u8); bytes.push((list.len() >> 8) as u8);
bytes.push(list.len() as u8); bytes.push(list.len() as u8);
// List items // List items
for s in list { for s in list.iter() {
// String length (2 bytes) // String length (2 bytes)
bytes.push((s.len() >> 8) as u8); bytes.push((s.len() >> 8) as u8);
bytes.push(s.len() as u8); bytes.push(s.len() as u8);
@@ -142,7 +143,7 @@ impl Value {
bytes.push((map.len() >> 8) as u8); bytes.push((map.len() >> 8) as u8);
bytes.push(map.len() as u8); bytes.push(map.len() as u8);
// Entries: key (string) + value (recursive) // 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() >> 8) as u8);
bytes.push(key.len() as u8); bytes.push(key.len() as u8);
bytes.extend_from_slice(key.as_bytes()); bytes.extend_from_slice(key.as_bytes());
@@ -209,19 +210,19 @@ impl From<SmolStr> for Value {
impl From<Vec<Decimal>> for Value { impl From<Vec<Decimal>> for Value {
fn from(v: Vec<Decimal>) -> Self { fn from(v: Vec<Decimal>) -> Self {
Value::NumberList(v) Value::NumberList(Rc::new(v))
} }
} }
impl From<Vec<SmolStr>> for Value { impl From<Vec<SmolStr>> for Value {
fn from(v: Vec<SmolStr>) -> Self { fn from(v: Vec<SmolStr>) -> Self {
Value::StringList(v) Value::StringList(Rc::new(v))
} }
} }
impl From<IndexMap<SmolStr, Value>> for Value { impl From<IndexMap<SmolStr, Value>> for Value {
fn from(m: IndexMap<SmolStr, Value>) -> Self { fn from(m: IndexMap<SmolStr, Value>) -> Self {
Value::Object(m) Value::Object(Rc::new(m))
} }
} }
@@ -292,7 +293,7 @@ impl Value {
list.push(Decimal::deserialize(decimal_bytes)); list.push(Decimal::deserialize(decimal_bytes));
} }
Ok((Value::NumberList(list), pos)) Ok((Value::NumberList(Rc::new(list)), pos))
} }
TYPE_STRING_LIST => { TYPE_STRING_LIST => {
if bytes.len() < pos + 2 { if bytes.len() < pos + 2 {
@@ -321,7 +322,7 @@ impl Value {
list.push(s.into()); list.push(s.into());
} }
Ok((Value::StringList(list), pos)) Ok((Value::StringList(Rc::new(list)), pos))
} }
TYPE_OBJECT => { TYPE_OBJECT => {
if bytes.len() < pos + 2 { if bytes.len() < pos + 2 {
@@ -354,7 +355,7 @@ impl Value {
map.insert(key.into(), val); map.insert(key.into(), val);
} }
Ok((Value::Object(map), pos)) Ok((Value::Object(Rc::new(map)), pos))
} }
_ => Err(format!("Unknown type tag: {}", type_tag)), _ => Err(format!("Unknown type tag: {}", type_tag)),
} }
@@ -407,7 +408,7 @@ impl Value {
serde_json::Value::String(s) => Ok(Value::String(SmolStr::new(s))), serde_json::Value::String(s) => Ok(Value::String(SmolStr::new(s))),
serde_json::Value::Array(arr) => { serde_json::Value::Array(arr) => {
if arr.is_empty() { if arr.is_empty() {
return Ok(Value::StringList(Vec::new())); return Ok(Value::StringList(Rc::new(Vec::new())));
} }
// Check if all elements are the same type // Check if all elements are the same type
let first = &arr[0]; let first = &arr[0];
@@ -418,12 +419,12 @@ impl Value {
nums.push(n); nums.push(n);
} }
} }
Ok(Value::NumberList(nums)) Ok(Value::NumberList(Rc::new(nums)))
} else if first.is_string() && arr.iter().all(|v| v.is_string()) { } else if first.is_string() && arr.iter().all(|v| v.is_string()) {
let strings: Vec<SmolStr> = arr.iter() let strings: Vec<SmolStr> = arr.iter()
.filter_map(|v| v.as_str().map(SmolStr::new)) .filter_map(|v| v.as_str().map(SmolStr::new))
.collect(); .collect();
Ok(Value::StringList(strings)) Ok(Value::StringList(Rc::new(strings)))
} else { } else {
Err("Arrays must contain all numbers or all strings".to_string()) Err("Arrays must contain all numbers or all strings".to_string())
} }
@@ -433,7 +434,7 @@ impl Value {
for (k, v) in obj { for (k, v) in obj {
map.insert(SmolStr::new(k), Self::from_json_value(v)?); map.insert(SmolStr::new(k), Self::from_json_value(v)?);
} }
Ok(Value::Object(map)) Ok(Value::Object(Rc::new(map)))
} }
} }
} }

View File

@@ -126,9 +126,9 @@ impl<'a> BytecodeReader<'a> {
self.read_byte() self.read_byte()
} }
/// Read a string /// Read a string as a borrowed slice (zero-copy from bytecode)
#[inline(always)] #[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; let length = self.read_u16()? as usize;
if self.position + length > self.bytecode.len() { if self.position + length > self.bytecode.len() {
@@ -139,7 +139,13 @@ impl<'a> BytecodeReader<'a> {
.map_err(|_| "Invalid UTF-8 string".to_string())?; .map_err(|_| "Invalid UTF-8 string".to_string())?;
self.position += length; 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 /// Read a value

View File

@@ -223,6 +223,7 @@ pub fn disassemble_bytecode(bytecode: &[u8]) -> Vec<String> {
Ok(reg) => format!("{:04x}: SetResult r{}", start_position, reg), Ok(reg) => format!("{:04x}: SetResult r{}", start_position, reg),
Err(_) => format!("{:04x}: SetResult (truncated)", start_position), Err(_) => format!("{:04x}: SetResult (truncated)", start_position),
}, },
OpCodeByte::ClearResult => format!("{:04x}: ClearResult", start_position),
OpCodeByte::End => format!("{:04x}: End", start_position), OpCodeByte::End => format!("{:04x}: End", start_position),
}; };

View File

@@ -170,6 +170,7 @@ impl Compiler {
let expr_reg = self.compile_expr(expr)?; let expr_reg = self.compile_expr(expr)?;
self.emit_store_global(name, expr_reg); self.emit_store_global(name, expr_reg);
self.free_register(expr_reg); self.free_register(expr_reg);
self.emit_byte(OpCodeByte::ClearResult.to_byte());
Ok(()) Ok(())
} }
@@ -229,6 +230,7 @@ impl Compiler {
// Store root back to global // Store root back to global
self.emit_store_global(root, root_reg); self.emit_store_global(root, root_reg);
self.free_register(root_reg); self.free_register(root_reg);
self.emit_byte(OpCodeByte::ClearResult.to_byte());
Ok(()) Ok(())
} }
@@ -319,13 +321,7 @@ impl Compiler {
let result_reg = self.allocate_register()?; let result_reg = self.allocate_register()?;
let opcode = match op { let opcode = match op {
Op::Add => { Op::Add => OpCodeByte::Add,
if self.is_string_concatenation(left, right) {
OpCodeByte::Concat
} else {
OpCodeByte::Add
}
}
Op::Sub => OpCodeByte::Sub, Op::Sub => OpCodeByte::Sub,
Op::Mul => OpCodeByte::Mul, Op::Mul => OpCodeByte::Mul,
Op::Div => OpCodeByte::Div, Op::Div => OpCodeByte::Div,
@@ -359,12 +355,6 @@ impl Compiler {
Ok(result_reg) Ok(result_reg)
} }
/// Check if binary operation is string concatenation
fn is_string_concatenation(&self, left: &Expr, right: &Expr) -> bool {
matches!(left, Expr::Value(Value::String(_)))
|| matches!(right, Expr::Value(Value::String(_)))
}
/// Compile a unary operation /// Compile a unary operation
fn compile_unary_op(&mut self, op: &Op, operand: &Expr) -> Result<u8, CompileError> { fn compile_unary_op(&mut self, op: &Op, operand: &Expr) -> Result<u8, CompileError> {
let operand_reg = self.compile_expr(operand)?; let operand_reg = self.compile_expr(operand)?;

View File

@@ -11,6 +11,7 @@
//! use indexmap::IndexMap; //! use indexmap::IndexMap;
//! use smol_str::SmolStr; //! use smol_str::SmolStr;
//! use rust_decimal_macros::dec; //! use rust_decimal_macros::dec;
//! use std::rc::Rc;
//! //!
//! let mut info = LanguageInfo::builtin(); //! let mut info = LanguageInfo::builtin();
//! //!
@@ -24,7 +25,7 @@
//! let mut customer = IndexMap::new(); //! let mut customer = IndexMap::new();
//! customer.insert(SmolStr::new("name"), Value::String("Alice".into())); //! customer.insert(SmolStr::new("name"), Value::String("Alice".into()));
//! customer.insert(SmolStr::new("age"), Value::Number(dec!(30))); //! customer.insert(SmolStr::new("age"), Value::Number(dec!(30)));
//! info.add_value("customer", &Value::Object(customer), None); //! info.add_value("customer", &Value::Object(Rc::new(customer)), None);
//! info.add_value("price", &Value::Number(dec!(100)), None); //! info.add_value("price", &Value::Number(dec!(100)), None);
//! //!
//! let json = info.to_json(); //! let json = info.to_json();

View File

@@ -2,7 +2,7 @@ use dexpr::{ast::value::Value, compiler::Compiler, parser, vm::VM};
use rust_decimal_macros::dec; use rust_decimal_macros::dec;
fn main() -> Result<(), Box<dyn std::error::Error>> { fn main() -> Result<(), Box<dyn std::error::Error>> {
let input = include_str!("basic_long.dexpr"); let input = include_str!("../examples/basic_long.dexpr");
let ast = parser::program(input)?; let ast = parser::program(input)?;

View File

@@ -5,10 +5,31 @@ pub struct Register(pub u8);
/// Default (built-in) function IDs for CallDefault opcode /// Default (built-in) function IDs for CallDefault opcode
pub mod default_fn { pub mod default_fn {
pub const RAND: u8 = 0; pub const RAND: u8 = 0;
// Future: ABS = 1, MIN = 2, MAX = 3, FLOOR = 4, CEIL = 5, ROUND = 6, ... pub const ABS: u8 = 1;
pub const MIN: u8 = 2;
pub const MAX: u8 = 3;
pub const FLOOR: u8 = 4;
pub const CEIL: u8 = 5;
pub const ROUND: u8 = 6;
pub const SQRT: u8 = 7;
pub const LEN: u8 = 8;
pub const TO_STRING: u8 = 9;
pub const TO_NUMBER: u8 = 10;
/// Lookup table: function name ID /// Lookup table: function name <EFBFBD><EFBFBD><EFBFBD> ID
pub const NAMES: &[(&str, u8)] = &[("rand", RAND)]; pub const NAMES: &[(&str, u8)] = &[
("rand", RAND),
("abs", ABS),
("min", MIN),
("max", MAX),
("floor", FLOOR),
("ceil", CEIL),
("round", ROUND),
("sqrt", SQRT),
("len", LEN),
("toString", TO_STRING),
("toNumber", TO_NUMBER),
];
/// Get function name by ID /// Get function name by ID
pub fn name(id: u8) -> Option<&'static str> { pub fn name(id: u8) -> Option<&'static str> {
@@ -77,7 +98,8 @@ pub enum OpCodeByte {
CallDefault = 0xA2, // Call default (built-in) function by ID CallDefault = 0xA2, // Call default (built-in) function by ID
// Result // Result
SetResult = 0xB0, // Set expression result (for return value) SetResult = 0xB0, // Set expression result (for return value)
ClearResult = 0xB1, // Clear expression result (assignment resets last result)
// End marker // End marker
End = 0xFF, // End of program End = 0xFF, // End of program
@@ -128,6 +150,7 @@ impl OpCodeByte {
0xA1 => Some(OpCodeByte::CallExternal), 0xA1 => Some(OpCodeByte::CallExternal),
0xA2 => Some(OpCodeByte::CallDefault), 0xA2 => Some(OpCodeByte::CallDefault),
0xB0 => Some(OpCodeByte::SetResult), 0xB0 => Some(OpCodeByte::SetResult),
0xB1 => Some(OpCodeByte::ClearResult),
0xFF => Some(OpCodeByte::End), 0xFF => Some(OpCodeByte::End),
_ => None, _ => None,
}; };
@@ -178,6 +201,7 @@ impl OpCodeByte {
OpCodeByte::CallExternal => "CallExternal", OpCodeByte::CallExternal => "CallExternal",
OpCodeByte::CallDefault => "Rand", OpCodeByte::CallDefault => "Rand",
OpCodeByte::SetResult => "SetResult", OpCodeByte::SetResult => "SetResult",
OpCodeByte::ClearResult => "ClearResult",
OpCodeByte::End => "End", OpCodeByte::End => "End",
} }
} }

View File

@@ -1,23 +0,0 @@
fib:
push rbp
movrr rbp, rsp
movsr rbp, r1
cmpsi rbp, 0
jg .L0
movri eax, 0
ret
.L0:
cmpsi rbp, 2
jg .L1
movri eax, 1
ret
.L1:
movrs r1, rbp
main:
add rsp, 16
movsi 16(byte) rbp, 10
movrs r1, rbp
call fib

229
src/vm/builtins.rs Normal file
View File

@@ -0,0 +1,229 @@
use crate::ast::value::Value;
use crate::opcodes::default_fn;
use rust_decimal::{prelude::ToPrimitive, Decimal, MathematicalOps};
use super::error::VMError;
use super::vm::VM;
impl<'a> VM<'a> {
/// Dispatch a built-in (default) function call by ID
pub(super) fn dispatch_builtin(
&mut self,
dest: usize,
fn_id: u8,
arg_regs: &[usize],
) -> Result<(), VMError> {
match fn_id {
default_fn::RAND => self.builtin_rand(dest, arg_regs),
default_fn::ABS => self.builtin_abs(dest, arg_regs),
default_fn::MIN => self.builtin_min(dest, arg_regs),
default_fn::MAX => self.builtin_max(dest, arg_regs),
default_fn::FLOOR => self.builtin_floor(dest, arg_regs),
default_fn::CEIL => self.builtin_ceil(dest, arg_regs),
default_fn::ROUND => self.builtin_round(dest, arg_regs),
default_fn::SQRT => self.builtin_sqrt(dest, arg_regs),
default_fn::LEN => self.builtin_len(dest, arg_regs),
default_fn::TO_STRING => self.builtin_to_string(dest, arg_regs),
default_fn::TO_NUMBER => self.builtin_to_number(dest, arg_regs),
_ => {
let name = default_fn::name(fn_id)
.map(|s| s.to_string())
.unwrap_or_else(|| format!("unknown({})", fn_id));
Err(VMError::RuntimeError(format!(
"Unknown default function: {}",
name
)))
}
}
}
fn builtin_rand(&mut self, dest: usize, arg_regs: &[usize]) -> Result<(), VMError> {
use rand::RngExt;
if arg_regs.len() < 2 {
return Err(VMError::RuntimeError(
"rand() requires two arguments (min, max)".to_string(),
));
}
match (&self.registers[arg_regs[0]], &self.registers[arg_regs[1]]) {
(Value::Number(min), Value::Number(max)) => {
let min_i64 = min.to_i64().ok_or_else(|| {
VMError::RuntimeError("rand() min must be an integer".to_string())
})?;
let max_i64 = max.to_i64().ok_or_else(|| {
VMError::RuntimeError("rand() max must be an integer".to_string())
})?;
if min_i64 > max_i64 {
return Err(VMError::RuntimeError(
"rand() min must be <= max".to_string(),
));
}
let mut rng = rand::rng();
let result = rng.random_range(min_i64..=max_i64);
self.registers[dest] = Value::Number(Decimal::from(result));
Ok(())
}
_ => Err(VMError::RuntimeError(
"rand() requires number arguments".to_string(),
)),
}
}
fn builtin_abs(&mut self, dest: usize, arg_regs: &[usize]) -> Result<(), VMError> {
require_args("abs", arg_regs, 1)?;
let n = extract_number("abs", &self.registers[arg_regs[0]])?;
self.registers[dest] = Value::Number(n.abs());
Ok(())
}
fn builtin_min(&mut self, dest: usize, arg_regs: &[usize]) -> Result<(), VMError> {
if arg_regs.is_empty() {
return Err(VMError::RuntimeError(
"min() requires at least one argument".to_string(),
));
}
let mut result = extract_number("min", &self.registers[arg_regs[0]])?;
for &reg in &arg_regs[1..] {
let n = extract_number("min", &self.registers[reg])?;
if n < result {
result = n;
}
}
self.registers[dest] = Value::Number(result);
Ok(())
}
fn builtin_max(&mut self, dest: usize, arg_regs: &[usize]) -> Result<(), VMError> {
if arg_regs.is_empty() {
return Err(VMError::RuntimeError(
"max() requires at least one argument".to_string(),
));
}
let mut result = extract_number("max", &self.registers[arg_regs[0]])?;
for &reg in &arg_regs[1..] {
let n = extract_number("max", &self.registers[reg])?;
if n > result {
result = n;
}
}
self.registers[dest] = Value::Number(result);
Ok(())
}
fn builtin_floor(&mut self, dest: usize, arg_regs: &[usize]) -> Result<(), VMError> {
require_args("floor", arg_regs, 1)?;
let n = extract_number("floor", &self.registers[arg_regs[0]])?;
self.registers[dest] = Value::Number(n.floor());
Ok(())
}
fn builtin_ceil(&mut self, dest: usize, arg_regs: &[usize]) -> Result<(), VMError> {
require_args("ceil", arg_regs, 1)?;
let n = extract_number("ceil", &self.registers[arg_regs[0]])?;
self.registers[dest] = Value::Number(n.ceil());
Ok(())
}
fn builtin_round(&mut self, dest: usize, arg_regs: &[usize]) -> Result<(), VMError> {
require_args("round", arg_regs, 1)?;
let n = extract_number("round", &self.registers[arg_regs[0]])?;
// Optional second argument: decimal places (default 0)
let places = if arg_regs.len() > 1 {
extract_number("round", &self.registers[arg_regs[1]])?
.to_u32()
.unwrap_or(0)
} else {
0
};
self.registers[dest] = Value::Number(n.round_dp(places));
Ok(())
}
fn builtin_sqrt(&mut self, dest: usize, arg_regs: &[usize]) -> Result<(), VMError> {
require_args("sqrt", arg_regs, 1)?;
let n = extract_number("sqrt", &self.registers[arg_regs[0]])?;
if n.is_sign_negative() {
return Err(VMError::RuntimeError(
"sqrt() argument must be non-negative".to_string(),
));
}
let result = n.sqrt().ok_or_else(|| {
VMError::RuntimeError("sqrt() failed to compute".to_string())
})?;
self.registers[dest] = Value::Number(result);
Ok(())
}
fn builtin_len(&mut self, dest: usize, arg_regs: &[usize]) -> Result<(), VMError> {
require_args("len", arg_regs, 1)?;
let len = match &self.registers[arg_regs[0]] {
Value::String(s) => Decimal::from(s.len()),
Value::StringList(l) => Decimal::from(l.len()),
Value::NumberList(l) => Decimal::from(l.len()),
Value::Object(m) => Decimal::from(m.len()),
other => {
return Err(VMError::RuntimeError(format!(
"len() not supported for type {}",
other.type_name()
)));
}
};
self.registers[dest] = Value::Number(len);
Ok(())
}
fn builtin_to_string(&mut self, dest: usize, arg_regs: &[usize]) -> Result<(), VMError> {
require_args("toString", arg_regs, 1)?;
let s = super::vm::value_to_string(&self.registers[arg_regs[0]]);
self.registers[dest] = Value::String(s.into_owned().into());
Ok(())
}
fn builtin_to_number(&mut self, dest: usize, arg_regs: &[usize]) -> Result<(), VMError> {
require_args("toNumber", arg_regs, 1)?;
let result = match &self.registers[arg_regs[0]] {
Value::Number(n) => *n,
Value::String(s) => s.parse::<Decimal>().map_err(|_| {
VMError::RuntimeError(format!("toNumber() cannot parse '{}'", s))
})?,
Value::Boolean(b) => {
if *b {
Decimal::from(1)
} else {
Decimal::from(0)
}
}
other => {
return Err(VMError::RuntimeError(format!(
"toNumber() not supported for type {}",
other.type_name()
)));
}
};
self.registers[dest] = Value::Number(result);
Ok(())
}
}
/// Helper: check minimum argument count
fn require_args(name: &str, arg_regs: &[usize], min: usize) -> Result<(), VMError> {
if arg_regs.len() < min {
Err(VMError::RuntimeError(format!(
"{}() requires at least {} argument(s)",
name, min
)))
} else {
Ok(())
}
}
/// Helper: extract a Decimal from a Value or return an error
fn extract_number(name: &str, val: &Value) -> Result<Decimal, VMError> {
match val {
Value::Number(n) => Ok(*n),
other => Err(VMError::RuntimeError(format!(
"{}() requires a number argument, got {}",
name,
other.type_name()
))),
}
}

500
src/vm/methods.rs Normal file
View File

@@ -0,0 +1,500 @@
use crate::ast::value::Value;
use rust_decimal::{prelude::ToPrimitive, Decimal};
use smol_str::{SmolStr, StrExt};
use std::rc::Rc;
use super::error::VMError;
use super::vm::VM;
impl<'a> VM<'a> {
/// 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: &str,
args: &[Value],
) -> Result<(), VMError> {
// 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 type_name: SmolStr = obj_val.type_name().into();
let key = (type_name, SmolStr::from(method));
if let Some(ext_method) = self.external_methods.as_ref().and_then(|m| m.get(&key)) {
ext_method(&obj_val, args).map_err(VMError::RuntimeError)
} else {
Err(VMError::MethodNotFound {
type_name: obj_val.type_name(),
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_inner(
&self,
obj_val: &Value,
method: &str,
args: &[Value],
) -> Result<Value, VMError> {
let s = match obj_val {
Value::String(s) => s,
_ => unreachable!(),
};
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(
"split() requires a delimiter argument".to_string(),
));
}
match &args[0] {
Value::String(delim) => {
let parts: Vec<SmolStr> = s.split(delim.as_str()).map(SmolStr::new).collect();
Ok(Value::StringList(Rc::new(parts)))
}
_ => Err(VMError::RuntimeError(
"split() requires a string delimiter".to_string(),
)),
}
}
"replace" => {
if args.len() < 2 {
return Err(VMError::RuntimeError(
"replace() requires two arguments (old, new)".to_string(),
));
}
match (&args[0], &args[1]) {
(Value::String(old), Value::String(new)) => {
Ok(Value::String(SmolStr::new(s.replace(old.as_str(), new.as_str()))))
}
_ => Err(VMError::RuntimeError(
"replace() requires string arguments".to_string(),
)),
}
}
"startsWith" => {
if args.is_empty() {
return Err(VMError::RuntimeError(
"startsWith() requires a prefix argument".to_string(),
));
}
match &args[0] {
Value::String(prefix) => Ok(Value::Boolean(s.starts_with(prefix.as_str()))),
_ => Err(VMError::RuntimeError(
"startsWith() requires a string prefix".to_string(),
)),
}
}
"endsWith" => {
if args.is_empty() {
return Err(VMError::RuntimeError(
"endsWith() requires a suffix argument".to_string(),
));
}
match &args[0] {
Value::String(suffix) => Ok(Value::Boolean(s.ends_with(suffix.as_str()))),
_ => Err(VMError::RuntimeError(
"endsWith() requires a string suffix".to_string(),
)),
}
}
"contains" => {
if args.is_empty() {
return Err(VMError::RuntimeError(
"contains() requires a substring argument".to_string(),
));
}
match &args[0] {
Value::String(substr) => Ok(Value::Boolean(s.contains(substr.as_str()))),
_ => Err(VMError::RuntimeError(
"contains() requires a string substring".to_string(),
)),
}
}
"length" => Ok(Value::Number(Decimal::from(s.len()))),
"charAt" => {
if args.is_empty() {
return Err(VMError::RuntimeError(
"charAt() requires an index argument".to_string(),
));
}
match &args[0] {
Value::Number(idx) => {
let index = idx.to_u64().unwrap_or(u64::MAX) as usize;
match s.chars().nth(index) {
Some(c) => Ok(Value::String(SmolStr::new(c.to_string()))),
None => Ok(Value::Null),
}
}
_ => Err(VMError::RuntimeError(
"charAt() requires a number index".to_string(),
)),
}
}
"substring" => {
if args.is_empty() {
return Err(VMError::RuntimeError(
"substring() requires at least a start index".to_string(),
));
}
match &args[0] {
Value::Number(start_idx) => {
let start = start_idx.to_usize().unwrap_or(0);
let chars: Vec<char> = s.chars().collect();
let end = if args.len() > 1 {
match &args[1] {
Value::Number(end_idx) => end_idx.to_usize().unwrap_or(chars.len()),
_ => chars.len(),
}
} else {
chars.len()
};
if start >= chars.len() || start >= end {
Ok(Value::String(SmolStr::new("")))
} else {
let end = end.min(chars.len());
let result: String = chars[start..end].iter().collect();
Ok(Value::String(SmolStr::new(result)))
}
}
_ => Err(VMError::RuntimeError(
"substring() requires a number start index".to_string(),
)),
}
}
_ => {
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)) {
ext_method(obj_val, args).map_err(VMError::RuntimeError)
} else {
Err(VMError::MethodNotFound {
type_name: "String",
method: SmolStr::from(method),
})
}
}
}
}
fn dispatch_string_list_method_inner(
&self,
obj_val: &Value,
method: &str,
args: &[Value],
) -> Result<Value, VMError> {
let list = match obj_val {
Value::StringList(l) => l,
_ => unreachable!(),
};
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()));
}
match &args[0] {
Value::Number(idx) => {
let index = idx.to_usize().unwrap_or(usize::MAX);
Ok(list.get(index).map(|s| Value::String(s.clone())).unwrap_or(Value::Null))
}
_ => Err(VMError::RuntimeError("get() requires a number index".to_string())),
}
}
"contains" => {
if args.is_empty() {
return Err(VMError::RuntimeError("contains() requires an argument".to_string()));
}
match &args[0] {
Value::String(s) => Ok(Value::Boolean(list.contains(s))),
_ => Err(VMError::RuntimeError("contains() requires a string argument".to_string())),
}
}
"indexOf" => {
if args.is_empty() {
return Err(VMError::RuntimeError("indexOf() requires an argument".to_string()));
}
match &args[0] {
Value::String(s) => {
let idx = list.iter().position(|item| item == s);
Ok(idx.map(|i| Value::Number(Decimal::from(i))).unwrap_or(Value::Number(Decimal::from(-1))))
}
_ => Err(VMError::RuntimeError("indexOf() requires a string argument".to_string())),
}
}
"slice" => {
if args.is_empty() {
return Err(VMError::RuntimeError("slice() requires at least a start index".to_string()));
}
match &args[0] {
Value::Number(start_idx) => {
let start = start_idx.to_usize().unwrap_or(0).min(list.len());
let end = if args.len() > 1 {
match &args[1] {
Value::Number(end_idx) => end_idx.to_usize().unwrap_or(list.len()).min(list.len()),
_ => list.len(),
}
} else {
list.len()
};
Ok(Value::StringList(Rc::new(list[start..end].to_vec())))
}
_ => Err(VMError::RuntimeError("slice() requires a number index".to_string())),
}
}
"reverse" => {
let mut reversed = list.to_vec();
reversed.reverse();
Ok(Value::StringList(Rc::new(reversed)))
}
"sort" => {
let mut sorted = list.to_vec();
sorted.sort();
Ok(Value::StringList(Rc::new(sorted)))
}
"join" => {
let delim = if args.is_empty() {
""
} else {
match &args[0] {
Value::String(s) => s.as_str(),
_ => return Err(VMError::RuntimeError("join() requires a string delimiter".to_string())),
}
};
let result: String = list.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(delim);
Ok(Value::String(SmolStr::new(result)))
}
_ => {
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)) {
ext_method(obj_val, args).map_err(VMError::RuntimeError)
} else {
Err(VMError::MethodNotFound {
type_name: "StringList",
method: SmolStr::from(method),
})
}
}
}
}
fn dispatch_number_list_method_inner(
&self,
obj_val: &Value,
method: &str,
args: &[Value],
) -> Result<Value, VMError> {
let list = match obj_val {
Value::NumberList(l) => l,
_ => unreachable!(),
};
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()));
}
match &args[0] {
Value::Number(idx) => {
let index = idx.to_usize().unwrap_or(usize::MAX);
Ok(list.get(index).map(|n| Value::Number(*n)).unwrap_or(Value::Null))
}
_ => Err(VMError::RuntimeError("get() requires a number index".to_string())),
}
}
"contains" => {
if args.is_empty() {
return Err(VMError::RuntimeError("contains() requires an argument".to_string()));
}
match &args[0] {
Value::Number(n) => Ok(Value::Boolean(list.contains(n))),
_ => Err(VMError::RuntimeError("contains() requires a number argument".to_string())),
}
}
"indexOf" => {
if args.is_empty() {
return Err(VMError::RuntimeError("indexOf() requires an argument".to_string()));
}
match &args[0] {
Value::Number(n) => {
let idx = list.iter().position(|item| item == n);
Ok(idx.map(|i| Value::Number(Decimal::from(i))).unwrap_or(Value::Number(Decimal::from(-1))))
}
_ => Err(VMError::RuntimeError("indexOf() requires a number argument".to_string())),
}
}
"slice" => {
if args.is_empty() {
return Err(VMError::RuntimeError("slice() requires at least a start index".to_string()));
}
match &args[0] {
Value::Number(start_idx) => {
let start = start_idx.to_usize().unwrap_or(0).min(list.len());
let end = if args.len() > 1 {
match &args[1] {
Value::Number(end_idx) => end_idx.to_usize().unwrap_or(list.len()).min(list.len()),
_ => list.len(),
}
} else {
list.len()
};
Ok(Value::NumberList(Rc::new(list[start..end].to_vec())))
}
_ => Err(VMError::RuntimeError("slice() requires a number index".to_string())),
}
}
"reverse" => {
let mut reversed = list.to_vec();
reversed.reverse();
Ok(Value::NumberList(Rc::new(reversed)))
}
"sort" => {
let mut sorted = list.to_vec();
sorted.sort();
Ok(Value::NumberList(Rc::new(sorted)))
}
"sum" => {
let sum: Decimal = list.iter().sum();
Ok(Value::Number(sum))
}
"avg" => {
if list.is_empty() {
Ok(Value::Null)
} else {
let sum: Decimal = list.iter().sum();
let avg = sum / Decimal::from(list.len());
Ok(Value::Number(avg))
}
}
"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"), SmolStr::from(method));
if let Some(ext_method) = self.external_methods.as_ref().and_then(|m| m.get(&key)) {
ext_method(obj_val, args).map_err(VMError::RuntimeError)
} else {
Err(VMError::MethodNotFound {
type_name: "NumberList",
method: SmolStr::from(method),
})
}
}
}
}
fn dispatch_object_method_inner(
&self,
obj_val: &Value,
method: &str,
args: &[Value],
) -> Result<Value, VMError> {
let map = match obj_val {
Value::Object(m) => m,
_ => unreachable!(),
};
match method {
"keys" => {
let keys: Vec<SmolStr> = map.keys().cloned().collect();
Ok(Value::StringList(Rc::new(keys)))
}
"values" => {
let vals: Vec<Value> = map.values().cloned().collect();
if vals.is_empty() {
Ok(Value::StringList(Rc::new(Vec::new())))
} else if vals.iter().all(|v| matches!(v, Value::String(_))) {
let strings: Vec<SmolStr> = vals
.into_iter()
.map(|v| match v {
Value::String(s) => s,
_ => unreachable!(),
})
.collect();
Ok(Value::StringList(Rc::new(strings)))
} else if vals.iter().all(|v| matches!(v, Value::Number(_))) {
let numbers: Vec<Decimal> = vals
.into_iter()
.map(|v| match v {
Value::Number(n) => n,
_ => unreachable!(),
})
.collect();
Ok(Value::NumberList(Rc::new(numbers)))
} else {
Err(VMError::RuntimeError(
"values() only works when all values are the same type (String or Number)".to_string(),
))
}
}
"length" | "len" => Ok(Value::Number(Decimal::from(map.len()))),
"contains" => {
if args.is_empty() {
return Err(VMError::RuntimeError(
"contains() requires a key argument".to_string(),
));
}
match &args[0] {
Value::String(key) => Ok(Value::Boolean(map.contains_key(key))),
_ => Err(VMError::RuntimeError(
"contains() requires a string key".to_string(),
)),
}
}
"get" => {
if args.is_empty() {
return Err(VMError::RuntimeError(
"get() requires a key argument".to_string(),
));
}
match &args[0] {
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"), SmolStr::from(method));
if let Some(ext_method) = self.external_methods.as_ref().and_then(|m| m.get(&key)) {
ext_method(obj_val, args).map_err(VMError::RuntimeError)
} else {
Err(VMError::MethodNotFound {
type_name: "Object",
method: SmolStr::from(method),
})
}
}
}
}
}

View File

@@ -1,5 +1,7 @@
mod builtins;
mod debug_info; mod debug_info;
pub mod error; pub mod error;
mod methods;
mod vm; mod vm;
pub use debug_info::DebugInfo; pub use debug_info::DebugInfo;

View File

@@ -1,9 +1,10 @@
use crate::{ast::value::Value, bytecode::BytecodeReader, opcodes::OpCodeByte}; use crate::{ast::value::Value, bytecode::BytecodeReader, opcodes::OpCodeByte};
use bumpalo::Bump; use std::rc::Rc;
use micromap::Map; use micromap::Map;
use rust_decimal::{prelude::ToPrimitive, Decimal, MathematicalOps}; use rust_decimal::{Decimal, MathematicalOps};
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use smol_str::{SmolStr, StrExt}; use smallvec::SmallVec;
use smol_str::SmolStr;
/// Type alias for external (host) functions /// Type alias for external (host) functions
pub type ExternalFn = Box<dyn Fn(&[Value]) -> Result<Value, String>>; pub type ExternalFn = Box<dyn Fn(&[Value]) -> Result<Value, String>>;
@@ -33,36 +34,25 @@ macro_rules! log_debug {
/// Virtual Machine for executing dExpr bytecode /// Virtual Machine for executing dExpr bytecode
pub struct VM<'a> { pub struct VM<'a> {
bytecode: &'a [u8], // Bytecode to execute bytecode: &'a [u8],
reader: BytecodeReader<'a>, // Bytecode reader pub(super) reader: BytecodeReader<'a>,
pc: usize, // Program counter pc: usize,
// Registers for computation pub(super) registers: [Value; MAX_REGISTERS],
registers: [Value; MAX_REGISTERS],
// Global variables
globals: Map<SmolStr, Value, 64>, globals: Map<SmolStr, Value, 64>,
// Last expression result (returned by execute)
last_result: Value, last_result: Value,
// External (host) functions — lazily allocated pub(super) external_functions: Option<FxHashMap<SmolStr, ExternalFn>>,
external_functions: Option<FxHashMap<SmolStr, ExternalFn>>,
// External (host) methods per type — lazily allocated pub(super) external_methods: Option<FxHashMap<(SmolStr, SmolStr), ExternalMethod>>,
external_methods: Option<FxHashMap<(SmolStr, SmolStr), ExternalMethod>>,
// Heap for complex data types
heap: Bump,
// Debug info for error messages
debug_info: Option<&'a DebugInfo>, debug_info: Option<&'a DebugInfo>,
// Debug flag
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
debug: bool, pub(super) debug: bool,
// Profiling counts
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
opcode_counts: [usize; 256], opcode_counts: [usize; 256],
} }
@@ -79,7 +69,6 @@ impl<'a> VM<'a> {
last_result: Value::Null, last_result: Value::Null,
external_functions: None, external_functions: None,
external_methods: None, external_methods: None,
heap: Bump::new(),
debug_info: None, debug_info: None,
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
debug: false, debug: false,
@@ -153,7 +142,6 @@ impl<'a> VM<'a> {
self.registers = [const { Value::Null }; MAX_REGISTERS]; self.registers = [const { Value::Null }; MAX_REGISTERS];
self.last_result = Value::Null; self.last_result = Value::Null;
// Preserve globals // Preserve globals
self.heap = Bump::new();
} }
/// Execute the bytecode program and return the last expression result /// Execute the bytecode program and return the last expression result
@@ -195,7 +183,7 @@ impl<'a> VM<'a> {
OpCodeByte::StoreLocal => self.handle_store_local(), OpCodeByte::StoreLocal => self.handle_store_local(),
OpCodeByte::LoadGlobal => self.handle_load_global(), OpCodeByte::LoadGlobal => self.handle_load_global(),
OpCodeByte::StoreGlobal => self.handle_store_global(), OpCodeByte::StoreGlobal => self.handle_store_global(),
OpCodeByte::Add => self.binary_op(|a, b| Ok(a + b), "add"), OpCodeByte::Add => self.handle_add(),
OpCodeByte::Sub => self.binary_op(|a, b| Ok(a - b), "subtract"), OpCodeByte::Sub => self.binary_op(|a, b| Ok(a - b), "subtract"),
OpCodeByte::Mul => self.binary_op(|a, b| Ok(a * b), "multiply"), OpCodeByte::Mul => self.binary_op(|a, b| Ok(a * b), "multiply"),
OpCodeByte::Div => self.binary_op( OpCodeByte::Div => self.binary_op(
@@ -240,6 +228,11 @@ impl<'a> VM<'a> {
OpCodeByte::CallExternal => self.handle_call_external(), OpCodeByte::CallExternal => self.handle_call_external(),
OpCodeByte::CallDefault => self.handle_call_default(), OpCodeByte::CallDefault => self.handle_call_default(),
OpCodeByte::SetResult => self.handle_set_result(), OpCodeByte::SetResult => self.handle_set_result(),
OpCodeByte::ClearResult => {
self.last_result = Value::Null;
log_debug!(self, "ClearResult");
Ok(())
}
OpCodeByte::End => { OpCodeByte::End => {
log_debug!(self, "End of program"); log_debug!(self, "End of program");
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
@@ -406,12 +399,12 @@ impl<'a> VM<'a> {
#[inline] #[inline]
fn handle_load_global(&mut self) -> Result<(), VMError> { fn handle_load_global(&mut self) -> Result<(), VMError> {
let reg = self.read_register_checked()?; 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 let value = self
.globals .globals
.get(&name) .get(name)
.ok_or_else(|| VMError::UndefinedVariable(name.clone()))?; .ok_or_else(|| VMError::UndefinedVariable(SmolStr::from(name)))?;
self.registers[reg] = value.clone(); self.registers[reg] = value.clone();
log_debug!(self, log_debug!(self,
@@ -424,12 +417,12 @@ impl<'a> VM<'a> {
/// Handle StoreGlobal opcode - store register to global variable /// Handle StoreGlobal opcode - store register to global variable
#[inline] #[inline]
fn handle_store_global(&mut self) -> Result<(), VMError> { 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()?; let reg = self.read_register_checked()?;
self self
.globals .globals
.insert(name.clone(), self.registers[reg].clone()); .insert(SmolStr::from(name), self.registers[reg].clone());
log_debug!(self, log_debug!(self,
"StoreGlobal global.{} = r{} ({})", "StoreGlobal global.{} = r{} ({})",
@@ -464,6 +457,54 @@ impl<'a> VM<'a> {
Ok(()) Ok(())
} }
// ============================================================================
// Opcode Handlers - Add (Number + String coercion)
// ============================================================================
/// Handle Add opcode - number addition or string concatenation with auto-coercion
#[inline]
fn handle_add(&mut self) -> Result<(), VMError> {
let dest = self.read_register_checked()?;
let a = self.read_register_checked()?;
let b = self.read_register_checked()?;
match (&self.registers[a], &self.registers[b]) {
(Value::Number(a_num), Value::Number(b_num)) => {
self.registers[dest] = Value::Number(*a_num + *b_num);
}
(Value::String(a_str), Value::String(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 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 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) => {
return Err(VMError::InvalidOperation {
operation: "add",
left_type: a_val.type_name(),
right_type: b_val.type_name(),
});
}
}
log_debug!(self, "Add r{} = r{} + r{}", dest, a, b);
Ok(())
}
// ============================================================================ // ============================================================================
// Opcode Handlers - Boolean Operations // Opcode Handlers - Boolean Operations
// ============================================================================ // ============================================================================
@@ -614,26 +655,19 @@ impl<'a> VM<'a> {
// Opcode Handlers - String Operations // Opcode Handlers - String Operations
// ============================================================================ // ============================================================================
/// Handle Concat opcode - string concatenation /// Handle Concat opcode - string concatenation with auto-coercion
#[inline] #[inline]
fn handle_concat(&mut self) -> Result<(), VMError> { fn handle_concat(&mut self) -> Result<(), VMError> {
let dest = self.read_register_checked()?; let dest = self.read_register_checked()?;
let a = self.read_register_checked()?; let a = self.read_register_checked()?;
let b = self.read_register_checked()?; let b = self.read_register_checked()?;
match (&self.registers[a], &self.registers[b]) { let a_cow = value_to_string(&self.registers[a]);
(Value::String(a_str), Value::String(b_str)) => { let b_cow = value_to_string(&self.registers[b]);
let result = format!("{}{}", a_str, b_str); let mut result = String::with_capacity(a_cow.len() + b_cow.len());
self.registers[dest] = Value::String(result.into()); result.push_str(&a_cow);
} result.push_str(&b_cow);
(a_val, b_val) => { self.registers[dest] = Value::String(result.into());
return Err(VMError::InvalidOperation {
operation: "concat",
left_type: a_val.type_name(),
right_type: b_val.type_name(),
});
}
}
log_debug!(self, "Concat r{} = r{} + r{}", dest, a, b); log_debug!(self, "Concat r{} = r{} + r{}", dest, a, b);
Ok(()) Ok(())
@@ -643,11 +677,11 @@ impl<'a> VM<'a> {
fn handle_get_property(&mut self) -> Result<(), VMError> { fn handle_get_property(&mut self) -> Result<(), VMError> {
let dest = self.read_register_checked()?; let dest = self.read_register_checked()?;
let obj = 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] { match &self.registers[obj] {
Value::Object(map) => { 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; self.registers[dest] = value;
} }
other => { other => {
@@ -665,13 +699,13 @@ impl<'a> VM<'a> {
/// Handle SetProperty opcode - set a field on an Object (in-place on register) /// Handle SetProperty opcode - set a field on an Object (in-place on register)
fn handle_set_property(&mut self) -> Result<(), VMError> { fn handle_set_property(&mut self) -> Result<(), VMError> {
let obj = 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)?;
let val = self.read_register_checked()?; let val = self.read_register_checked()?;
let value = self.registers[val].clone(); let value = self.registers[val].clone();
match &mut self.registers[obj] { match &mut self.registers[obj] {
Value::Object(map) => { Value::Object(map) => {
map.insert(prop.clone(), value); Rc::make_mut(map).insert(SmolStr::from(prop), value);
} }
other => { other => {
return Err(VMError::RuntimeError(format!( return Err(VMError::RuntimeError(format!(
@@ -689,538 +723,21 @@ impl<'a> VM<'a> {
fn handle_method_call(&mut self) -> Result<(), VMError> { fn handle_method_call(&mut self) -> Result<(), VMError> {
let dest = self.read_register_checked()?; let dest = self.read_register_checked()?;
let obj = 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 // Read argument registers
let arg_count = self let arg_count = self
.reader .reader
.read_byte() .read_byte()
.map_err(VMError::BytecodeError)? as usize; .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 { for _ in 0..arg_count {
let reg = self.read_register_checked()?; let reg = self.read_register_checked()?;
args.push(self.registers[reg].clone()); args.push(self.registers[reg].clone());
} }
// Dispatch method call self.dispatch_method(dest, obj, method, &args)?;
match &self.registers[obj] {
Value::String(s) => match method.as_str() {
"upper" => {
let result = s.to_uppercase_smolstr();
self.registers[dest] = Value::String(result);
}
"lower" => {
let result = s.to_lowercase_smolstr();
self.registers[dest] = Value::String(result);
}
"trim" => {
let result = SmolStr::new(s.trim());
self.registers[dest] = Value::String(result);
}
"trimStart" => {
let result = SmolStr::new(s.trim_start());
self.registers[dest] = Value::String(result);
}
"trimEnd" => {
let result = SmolStr::new(s.trim_end());
self.registers[dest] = Value::String(result);
}
"split" => {
if args.is_empty() {
return Err(VMError::RuntimeError(
"split() requires a delimiter argument".to_string(),
));
}
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(),
));
}
}
}
"replace" => {
if args.len() < 2 {
return Err(VMError::RuntimeError(
"replace() requires two arguments (old, new)".to_string(),
));
}
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(),
));
}
}
}
"startsWith" => {
if args.is_empty() {
return Err(VMError::RuntimeError(
"startsWith() requires a prefix argument".to_string(),
));
}
match &args[0] {
Value::String(prefix) => {
let result = s.starts_with(prefix.as_str());
self.registers[dest] = Value::Boolean(result);
}
_ => {
return Err(VMError::RuntimeError(
"startsWith() requires a string prefix".to_string(),
));
}
}
}
"endsWith" => {
if args.is_empty() {
return Err(VMError::RuntimeError(
"endsWith() requires a suffix argument".to_string(),
));
}
match &args[0] {
Value::String(suffix) => {
let result = s.ends_with(suffix.as_str());
self.registers[dest] = Value::Boolean(result);
}
_ => {
return Err(VMError::RuntimeError(
"endsWith() requires a string suffix".to_string(),
));
}
}
}
"contains" => {
if args.is_empty() {
return Err(VMError::RuntimeError(
"contains() requires a substring argument".to_string(),
));
}
match &args[0] {
Value::String(substr) => {
let result = s.contains(substr.as_str());
self.registers[dest] = Value::Boolean(result);
}
_ => {
return Err(VMError::RuntimeError(
"contains() requires a string substring".to_string(),
));
}
}
}
"length" => {
let len = Decimal::from(s.len());
self.registers[dest] = Value::Number(len);
}
"charAt" => {
if args.is_empty() {
return Err(VMError::RuntimeError(
"charAt() requires an index argument".to_string(),
));
}
match &args[0] {
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;
}
}
}
_ => {
return Err(VMError::RuntimeError(
"charAt() requires a number index".to_string(),
));
}
}
}
"substring" => {
if args.is_empty() {
return Err(VMError::RuntimeError(
"substring() requires at least a start index".to_string(),
));
}
match &args[0] {
Value::Number(start_idx) => {
let start = start_idx.to_usize().unwrap_or(0);
let chars: Vec<char> = s.chars().collect();
let end = if args.len() > 1 {
match &args[1] {
Value::Number(end_idx) => end_idx.to_usize().unwrap_or(chars.len()),
_ => chars.len(),
}
} else {
chars.len()
};
if start >= chars.len() || start >= end {
self.registers[dest] = 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));
}
}
_ => {
return Err(VMError::RuntimeError(
"substring() requires a number start index".to_string(),
));
}
}
}
_ => {
let key = (SmolStr::new_static("String"), method.clone());
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;
} else {
return Err(VMError::MethodNotFound {
type_name: "String",
method,
});
}
}
},
Value::StringList(list) => 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);
}
"get" => {
if args.is_empty() {
return Err(VMError::RuntimeError("get() requires an index".to_string()));
}
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);
}
_ => return Err(VMError::RuntimeError("get() requires a number index".to_string())),
}
}
"contains" => {
if args.is_empty() {
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())),
}
}
"indexOf" => {
if args.is_empty() {
return Err(VMError::RuntimeError("indexOf() requires an argument".to_string()));
}
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)));
}
_ => return Err(VMError::RuntimeError("indexOf() requires a string argument".to_string())),
}
}
"slice" => {
if args.is_empty() {
return Err(VMError::RuntimeError("slice() requires at least a start index".to_string()));
}
match &args[0] {
Value::Number(start_idx) => {
let start = start_idx.to_usize().unwrap_or(0).min(list.len());
let end = if args.len() > 1 {
match &args[1] {
Value::Number(end_idx) => end_idx.to_usize().unwrap_or(list.len()).min(list.len()),
_ => list.len(),
}
} else {
list.len()
};
self.registers[dest] = Value::StringList(list[start..end].to_vec());
}
_ => return Err(VMError::RuntimeError("slice() requires a number index".to_string())),
}
}
"reverse" => {
let mut reversed = list.clone();
reversed.reverse();
self.registers[dest] = Value::StringList(reversed);
}
"sort" => {
let mut sorted = list.clone();
sorted.sort();
self.registers[dest] = Value::StringList(sorted);
}
"join" => {
let delim = if args.is_empty() {
""
} else {
match &args[0] {
Value::String(s) => s.as_str(),
_ => return Err(VMError::RuntimeError("join() requires a string delimiter".to_string())),
}
};
let result: String = list.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(delim);
self.registers[dest] = Value::String(SmolStr::new(result));
}
_ => {
let key = (SmolStr::new_static("StringList"), method.clone());
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;
} else {
return Err(VMError::MethodNotFound {
type_name: "StringList",
method,
});
}
}
},
Value::NumberList(list) => 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);
}
"get" => {
if args.is_empty() {
return Err(VMError::RuntimeError("get() requires an index".to_string()));
}
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);
}
_ => return Err(VMError::RuntimeError("get() requires a number index".to_string())),
}
}
"contains" => {
if args.is_empty() {
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())),
}
}
"indexOf" => {
if args.is_empty() {
return Err(VMError::RuntimeError("indexOf() requires an argument".to_string()));
}
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)));
}
_ => return Err(VMError::RuntimeError("indexOf() requires a number argument".to_string())),
}
}
"slice" => {
if args.is_empty() {
return Err(VMError::RuntimeError("slice() requires at least a start index".to_string()));
}
match &args[0] {
Value::Number(start_idx) => {
let start = start_idx.to_usize().unwrap_or(0).min(list.len());
let end = if args.len() > 1 {
match &args[1] {
Value::Number(end_idx) => end_idx.to_usize().unwrap_or(list.len()).min(list.len()),
_ => list.len(),
}
} else {
list.len()
};
self.registers[dest] = Value::NumberList(list[start..end].to_vec());
}
_ => return Err(VMError::RuntimeError("slice() requires a number index".to_string())),
}
}
"reverse" => {
let mut reversed = list.clone();
reversed.reverse();
self.registers[dest] = Value::NumberList(reversed);
}
"sort" => {
let mut sorted = list.clone();
sorted.sort();
self.registers[dest] = Value::NumberList(sorted);
}
"sum" => {
let sum: Decimal = list.iter().sum();
self.registers[dest] = Value::Number(sum);
}
"avg" => {
if list.is_empty() {
self.registers[dest] = Value::Null;
} else {
let sum: Decimal = list.iter().sum();
let avg = sum / Decimal::from(list.len());
self.registers[dest] = 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);
}
_ => {
let key = (SmolStr::new_static("NumberList"), method.clone());
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;
} else {
return Err(VMError::MethodNotFound {
type_name: "NumberList",
method,
});
}
}
},
Value::Object(map) => match method.as_str() {
"keys" => {
let keys: Vec<SmolStr> = map.keys().cloned().collect();
self.registers[dest] = Value::StringList(keys);
}
"values" => {
// Returns a StringList if all values are strings, NumberList if all numbers, otherwise error
let vals: Vec<Value> = map.values().cloned().collect();
if vals.is_empty() {
self.registers[dest] = Value::StringList(Vec::new());
} else if vals.iter().all(|v| matches!(v, Value::String(_))) {
let strings: Vec<SmolStr> = vals.into_iter().map(|v| match v {
Value::String(s) => s,
_ => unreachable!(),
}).collect();
self.registers[dest] = Value::StringList(strings);
} else if vals.iter().all(|v| matches!(v, Value::Number(_))) {
let numbers: Vec<Decimal> = vals.into_iter().map(|v| match v {
Value::Number(n) => n,
_ => unreachable!(),
}).collect();
self.registers[dest] = Value::NumberList(numbers);
} else {
return 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()));
}
"contains" => {
if args.is_empty() {
return Err(VMError::RuntimeError("contains() requires a key argument".to_string()));
}
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())),
}
}
"get" => {
if args.is_empty() {
return Err(VMError::RuntimeError("get() requires a key argument".to_string()));
}
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())),
}
}
_ => {
let key = (SmolStr::new_static("Object"), method.clone());
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;
} else {
return Err(VMError::MethodNotFound {
type_name: "Object",
method,
});
}
}
},
_ => {
// 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());
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;
} else {
return Err(VMError::MethodNotFound {
type_name: obj_val.type_name(),
method,
});
}
}
}
log_debug!(self, "MethodCall r{} = r{}.{}(...)", dest, obj, method); log_debug!(self, "MethodCall r{} = r{}.{}(...)", dest, obj, method);
Ok(()) Ok(())
@@ -1232,48 +749,16 @@ impl<'a> VM<'a> {
/// Handle CallDefault opcode - call a default (built-in) function by ID /// Handle CallDefault opcode - call a default (built-in) function by ID
fn handle_call_default(&mut self) -> Result<(), VMError> { fn handle_call_default(&mut self) -> Result<(), VMError> {
use crate::opcodes::default_fn;
let dest = self.read_register_checked()?; let dest = self.read_register_checked()?;
let fn_id = self.reader.read_byte().map_err(VMError::BytecodeError)?; 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 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 { for _ in 0..arg_count {
arg_regs.push(self.read_register_checked()?); arg_regs.push(self.read_register_checked()?);
} }
match fn_id { self.dispatch_builtin(dest, fn_id, &arg_regs)?;
default_fn::RAND => {
use rand::RngExt;
if arg_regs.len() < 2 {
return Err(VMError::RuntimeError("rand() requires two arguments (min, max)".to_string()));
}
match (&self.registers[arg_regs[0]], &self.registers[arg_regs[1]]) {
(Value::Number(min), Value::Number(max)) => {
let min_i64 = min.to_i64().ok_or_else(|| {
VMError::RuntimeError("rand() min must be an integer".to_string())
})?;
let max_i64 = max.to_i64().ok_or_else(|| {
VMError::RuntimeError("rand() max must be an integer".to_string())
})?;
if min_i64 > max_i64 {
return Err(VMError::RuntimeError("rand() min must be <= max".to_string()));
}
let mut rng = rand::rng();
let result = rng.random_range(min_i64..=max_i64);
self.registers[dest] = Value::Number(Decimal::from(result));
}
_ => return Err(VMError::RuntimeError("rand() requires number arguments".to_string())),
}
}
_ => {
let name = default_fn::name(fn_id)
.map(|s| s.to_string())
.unwrap_or_else(|| format!("unknown({})", fn_id));
return Err(VMError::RuntimeError(format!("Unknown default function: {}", name)));
}
}
log_debug!(self, "CallDefault r{} = fn#{}(...)", dest, fn_id); log_debug!(self, "CallDefault r{} = fn#{}(...)", dest, fn_id);
Ok(()) Ok(())
@@ -1282,20 +767,21 @@ impl<'a> VM<'a> {
/// Handle CallExternal opcode - call host function by name /// Handle CallExternal opcode - call host function by name
fn handle_call_external(&mut self) -> Result<(), VMError> { fn handle_call_external(&mut self) -> Result<(), VMError> {
let dest = self.read_register_checked()?; 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 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 { for _ in 0..arg_count {
let reg = self.read_register_checked()?; let reg = self.read_register_checked()?;
args.push(self.registers[reg].clone()); args.push(self.registers[reg].clone());
} }
let name_key = SmolStr::from(name);
let func = self let func = self
.external_functions .external_functions
.as_ref() .as_ref()
.and_then(|m| m.get(&name)) .and_then(|m| m.get(&name_key))
.ok_or_else(|| VMError::RuntimeError(format!("Undefined function: {}", name)))?; .ok_or_else(|| VMError::RuntimeError(format!("Undefined function: {}", name_key)))?;
let result = func(&args).map_err(VMError::RuntimeError)?; let result = func(&args).map_err(VMError::RuntimeError)?;
self.registers[dest] = result; self.registers[dest] = result;
@@ -1415,3 +901,14 @@ impl<'a> VM<'a> {
Ok(()) Ok(())
} }
} }
/// Convert a Value to its string representation for concatenation (no quotes around strings)
pub(super) fn value_to_string(val: &Value) -> std::borrow::Cow<'_, str> {
match val {
Value::String(s) => std::borrow::Cow::Borrowed(s.as_str()),
Value::Number(n) => std::borrow::Cow::Owned(n.to_string()),
Value::Boolean(b) => std::borrow::Cow::Borrowed(if *b { "true" } else { "false" }),
Value::Null => std::borrow::Cow::Borrowed("null"),
other => std::borrow::Cow::Owned(format!("{}", other)),
}
}

144
tests/data_driven_tests.rs Normal file
View File

@@ -0,0 +1,144 @@
use dexpr::{ast::value::Value, compiler::Compiler, parser, vm::VM};
use indexmap::IndexMap;
use rust_decimal::Decimal;
use smol_str::SmolStr;
use std::collections::HashMap;
use std::rc::Rc;
use std::str::FromStr;
#[derive(serde::Deserialize)]
struct TestCase {
name: String,
code: String,
#[serde(default)]
globals: HashMap<String, ValueDef>,
expected: ValueDef,
}
#[derive(serde::Deserialize)]
struct ValueDef {
#[serde(rename = "type")]
typ: String,
#[serde(default)]
value: Option<serde_json::Value>,
}
fn value_def_to_value(def: &ValueDef) -> Value {
match def.typ.as_str() {
"null" => Value::Null,
"number" => {
let s = def.value.as_ref().unwrap().as_str().unwrap();
Value::Number(Decimal::from_str(s).unwrap())
}
"string" => {
let s = def.value.as_ref().unwrap().as_str().unwrap();
Value::String(s.into())
}
"boolean" => {
let b = def.value.as_ref().unwrap().as_bool().unwrap();
Value::Boolean(b)
}
"object" => {
fn json_obj_to_value(obj: &serde_json::Map<String, serde_json::Value>) -> Value {
let mut map = IndexMap::new();
for (k, v) in obj {
let val = match v {
serde_json::Value::String(s) => {
if let Ok(d) = Decimal::from_str(s) {
Value::Number(d)
} else {
Value::String(SmolStr::from(s.as_str()))
}
}
serde_json::Value::Bool(b) => Value::Boolean(*b),
serde_json::Value::Object(nested) => json_obj_to_value(nested),
_ => Value::String(SmolStr::from(v.to_string())),
};
map.insert(SmolStr::from(k.as_str()), val);
}
Value::Object(Rc::new(map))
}
let obj = def.value.as_ref().unwrap().as_object().unwrap();
json_obj_to_value(obj)
}
other => panic!("Unknown type: {other}"),
}
}
#[test]
fn test_all_cases() {
let json = include_str!("test_cases.json");
let cases: Vec<TestCase> =
serde_json::from_str(json).expect("Failed to parse test_cases.json");
let mut failures = Vec::new();
let total = cases.len();
for case in &cases {
let ast = match parser::program(&case.code) {
Ok(ast) => ast,
Err(e) => {
failures.push(format!(
"FAIL: {}\n code: {}\n error: parse failed: {e}",
case.name,
case.code.replace('\n', "\\n"),
));
continue;
}
};
let mut compiler = Compiler::new();
let bytecode = match compiler.compile(ast) {
Ok(bc) => bc,
Err(e) => {
failures.push(format!(
"FAIL: {}\n code: {}\n error: compile failed: {e}",
case.name,
case.code.replace('\n', "\\n"),
));
continue;
}
};
let mut vm = VM::new(&bytecode);
for (name, def) in &case.globals {
vm.set_global(name, value_def_to_value(def));
}
let result = match vm.execute() {
Ok(v) => v,
Err(e) => {
failures.push(format!(
"FAIL: {}\n code: {}\n error: execute failed: {e}",
case.name,
case.code.replace('\n', "\\n"),
));
continue;
}
};
let expected = value_def_to_value(&case.expected);
if result != expected {
failures.push(format!(
"FAIL: {}\n code: {}\n expected: {:?}\n got: {:?}",
case.name,
case.code.replace('\n', "\\n"),
expected,
result
));
}
}
if !failures.is_empty() {
panic!(
"\n{} / {} test cases failed:\n\n{}\n",
failures.len(),
total,
failures.join("\n\n")
);
}
eprintln!("All {total} test cases passed.");
}

View File

@@ -3,6 +3,7 @@ use indexmap::IndexMap;
use rust_decimal::prelude::ToPrimitive; use rust_decimal::prelude::ToPrimitive;
use rust_decimal_macros::dec; use rust_decimal_macros::dec;
use smol_str::SmolStr; use smol_str::SmolStr;
use std::rc::Rc;
/// Helper to run code and get the value of "result" variable /// Helper to run code and get the value of "result" variable
fn run_and_get_result(code: &str) -> Value { fn run_and_get_result(code: &str) -> Value {
@@ -628,7 +629,7 @@ fn test_numberlist_contains() {
let mut compiler = Compiler::new(); let mut compiler = Compiler::new();
let bytecode = compiler.compile(ast).expect("compile"); let bytecode = compiler.compile(ast).expect("compile");
let mut vm = VM::new(&bytecode); 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(Rc::new(vec![dec!(1), dec!(2), dec!(3), dec!(4)])));
vm.execute().expect("execute"); vm.execute().expect("execute");
assert_eq!(vm.get_global("result").unwrap(), &Value::Boolean(true)); assert_eq!(vm.get_global("result").unwrap(), &Value::Boolean(true));
} }
@@ -639,7 +640,7 @@ fn test_numberlist_indexof() {
let mut compiler = Compiler::new(); let mut compiler = Compiler::new();
let bytecode = compiler.compile(ast).expect("compile"); let bytecode = compiler.compile(ast).expect("compile");
let mut vm = VM::new(&bytecode); 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(Rc::new(vec![dec!(1), dec!(2), dec!(3)])));
vm.execute().expect("execute"); vm.execute().expect("execute");
assert_eq!(vm.get_global("result").unwrap(), &Value::Number(dec!(2))); assert_eq!(vm.get_global("result").unwrap(), &Value::Number(dec!(2)));
} }
@@ -650,7 +651,7 @@ fn test_numberlist_slice() {
let mut compiler = Compiler::new(); let mut compiler = Compiler::new();
let bytecode = compiler.compile(ast).expect("compile"); let bytecode = compiler.compile(ast).expect("compile");
let mut vm = VM::new(&bytecode); 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(Rc::new(vec![dec!(10), dec!(20), dec!(30), dec!(40)])));
vm.execute().expect("execute"); vm.execute().expect("execute");
// slice(1,3) = [20, 30], sum = 50 // slice(1,3) = [20, 30], sum = 50
assert_eq!(vm.get_global("result").unwrap(), &Value::Number(dec!(50))); assert_eq!(vm.get_global("result").unwrap(), &Value::Number(dec!(50)));
@@ -662,7 +663,7 @@ fn test_numberlist_reverse() {
let mut compiler = Compiler::new(); let mut compiler = Compiler::new();
let bytecode = compiler.compile(ast).expect("compile"); let bytecode = compiler.compile(ast).expect("compile");
let mut vm = VM::new(&bytecode); 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(Rc::new(vec![dec!(1), dec!(2), dec!(3)])));
vm.execute().expect("execute"); vm.execute().expect("execute");
assert_eq!(vm.get_global("result").unwrap(), &Value::Number(dec!(3))); assert_eq!(vm.get_global("result").unwrap(), &Value::Number(dec!(3)));
} }
@@ -673,7 +674,7 @@ fn test_numberlist_sort() {
let mut compiler = Compiler::new(); let mut compiler = Compiler::new();
let bytecode = compiler.compile(ast).expect("compile"); let bytecode = compiler.compile(ast).expect("compile");
let mut vm = VM::new(&bytecode); 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(Rc::new(vec![dec!(30), dec!(10), dec!(20)])));
vm.execute().expect("execute"); vm.execute().expect("execute");
assert_eq!(vm.get_global("result").unwrap(), &Value::Number(dec!(10))); assert_eq!(vm.get_global("result").unwrap(), &Value::Number(dec!(10)));
} }
@@ -684,7 +685,7 @@ fn test_numberlist_isempty() {
let mut compiler = Compiler::new(); let mut compiler = Compiler::new();
let bytecode = compiler.compile(ast).expect("compile"); let bytecode = compiler.compile(ast).expect("compile");
let mut vm = VM::new(&bytecode); let mut vm = VM::new(&bytecode);
vm.set_global("nums", Value::NumberList(vec![])); vm.set_global("nums", Value::NumberList(Rc::new(vec![])));
vm.execute().expect("execute"); vm.execute().expect("execute");
assert_eq!(vm.get_global("result").unwrap(), &Value::Boolean(true)); assert_eq!(vm.get_global("result").unwrap(), &Value::Boolean(true));
} }
@@ -959,27 +960,19 @@ result = x % y"#;
} }
#[test] #[test]
fn test_error_location_type_mismatch() { fn test_string_number_auto_coercion() {
// string + number now auto-coerces to string concatenation
let code = r#"x = "hello" let code = r#"x = "hello"
y = 5 y = 5
result = x + y"#; x + y"#;
let ast = parser::program(code).unwrap();
let mut compiler = Compiler::new(); let mut compiler = Compiler::new();
let (bytecode, debug_info) = compiler let bytecode = compiler.compile(ast).unwrap();
.compile_from_source(code)
.expect("Failed to compile");
let mut vm = VM::new(&bytecode); let mut vm = VM::new(&bytecode);
vm.set_debug_info(&debug_info); let result = vm.execute().unwrap();
let err = vm.execute().unwrap_err(); assert_eq!(result, Value::String("hello5".into()));
let err_msg = err.to_string();
assert!(
err_msg.contains("line 3"),
"Error should contain line 3, got: {}",
err_msg
);
} }
#[test] #[test]
@@ -1036,7 +1029,7 @@ fn make_object(entries: Vec<(&str, Value)>) -> Value {
for (k, v) in entries { for (k, v) in entries {
map.insert(SmolStr::new(k), v); map.insert(SmolStr::new(k), v);
} }
Value::Object(map) Value::Object(Rc::new(map))
} }
#[test] #[test]
@@ -1121,7 +1114,7 @@ fn test_object_method_keys() {
let result = run_expr_with_globals("person.keys()", vec![("person", obj)]); let result = run_expr_with_globals("person.keys()", vec![("person", obj)]);
assert_eq!( assert_eq!(
result, result,
Value::StringList(vec![SmolStr::new("name"), SmolStr::new("age")]) Value::StringList(Rc::new(vec![SmolStr::new("name"), SmolStr::new("age")]))
); );
} }
@@ -1185,7 +1178,7 @@ fn test_object_method_values_strings() {
let result = run_expr_with_globals("obj.values()", vec![("obj", obj)]); let result = run_expr_with_globals("obj.values()", vec![("obj", obj)]);
assert_eq!( assert_eq!(
result, result,
Value::StringList(vec![SmolStr::new("x"), SmolStr::new("y")]) Value::StringList(Rc::new(vec![SmolStr::new("x"), SmolStr::new("y")]))
); );
} }
@@ -1196,7 +1189,7 @@ fn test_object_method_values_numbers() {
("b", Value::Number(dec!(2))), ("b", Value::Number(dec!(2))),
]); ]);
let result = run_expr_with_globals("obj.values()", vec![("obj", obj)]); 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(Rc::new(vec![dec!(1), dec!(2)])));
} }
#[test] #[test]

1288
tests/test_cases.json Normal file

File diff suppressed because it is too large Load Diff