mirror of
https://github.com/duhanbalci/dexpr.git
synced 2026-07-02 00:29:15 +00:00
Compare commits
3 Commits
75ab9bec9f
...
953b39d433
| Author | SHA1 | Date | |
|---|---|---|---|
| 953b39d433 | |||
| b0ea71e104 | |||
| 7582c5aee7 |
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -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",
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
19
docs/vm.md
19
docs/vm.md
@@ -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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
491
flamegraph.svg
491
flamegraph.svg
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 48 KiB |
74
gen.js
74
gen.js
@@ -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);
|
|
||||||
6
justfile
6
justfile
@@ -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)
|
||||||
|
|||||||
BIN
profile.json.gz
BIN
profile.json.gz
Binary file not shown.
@@ -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)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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)?;
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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)?;
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
229
src/vm/builtins.rs
Normal 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 ® 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 ® 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
500
src/vm/methods.rs
Normal 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),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
707
src/vm/vm.rs
707
src/vm/vm.rs
@@ -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
144
tests/data_driven_tests.rs
Normal 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.");
|
||||||
|
}
|
||||||
@@ -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
1288
tests/test_cases.json
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user