Compare commits

..

4 Commits

Author SHA1 Message Date
0fa388abeb 0.3.0 2026-04-07 15:16:19 +03:00
953b39d433 perf improvements 2026-04-06 02:51:42 +03:00
b0ea71e104 performance improvements 2026-04-06 00:04:40 +03:00
7582c5aee7 refactor 2026-04-05 23:05:31 +03:00
46 changed files with 3999 additions and 1308 deletions

2
.gitignore vendored
View File

@@ -1 +1,3 @@
/target /target
editor/dist
editor/node_modules

View File

@@ -72,6 +72,7 @@ The dexpr language supports:
- Line comments (`//`) and block comments (`/* */`) - Line comments (`//`) and block comments (`/* */`)
- Lists: `NumberList` and `StringList` types with methods (`sum`, `avg`, `min`, `max`, `first`, `last`, `get`, `join`, `contains`, `indexOf`, `slice`, `reverse`, `sort`, `isEmpty`, etc.) - Lists: `NumberList` and `StringList` types with methods (`sum`, `avg`, `min`, `max`, `first`, `last`, `get`, `join`, `contains`, `indexOf`, `slice`, `reverse`, `sort`, `isEmpty`, etc.)
- Objects: `Object` type (provided externally via `set_global`) with property access (`obj.field`), nested access (`obj.a.b`), property assignment (`obj.field = value`), and methods (`keys()`, `values()`, `length()`, `contains(key)`, `get(key)`) - Objects: `Object` type (provided externally via `set_global`) with property access (`obj.field`), nested access (`obj.a.b`), property assignment (`obj.field = value`), and methods (`keys()`, `values()`, `length()`, `contains(key)`, `get(key)`)
- Lists: `List` type for heterogeneous arrays (including array of objects), with methods (`length`, `isEmpty`, `first`, `last`, `get`, `contains`, `indexOf`, `slice`, `reverse`, `join`, `map(field)`, `filter(field, value?)`, `find(field, value?)`, `sort(field)`). Property projection: `kalemler.tutar` extracts field from each Object element, returning NumberList/StringList/List. `map("field")` also available as explicit alternative
## Detailed Module Documentation ## Detailed Module Documentation

3
Cargo.lock generated
View File

@@ -304,7 +304,7 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
[[package]] [[package]]
name = "dexpr" name = "dexpr"
version = "0.1.0" version = "0.3.0"
dependencies = [ dependencies = [
"bumpalo", "bumpalo",
"criterion", "criterion",
@@ -315,6 +315,7 @@ dependencies = [
"rust_decimal", "rust_decimal",
"rust_decimal_macros", "rust_decimal_macros",
"rustc-hash", "rustc-hash",
"serde",
"serde_json", "serde_json",
"smallvec", "smallvec",
"smol_str", "smol_str",

View File

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

View File

@@ -1,21 +1,39 @@
use criterion::{criterion_group, criterion_main, Criterion}; use criterion::{criterion_group, criterion_main, Criterion};
use dexpr::{ast::value::Value, compiler::Compiler, parser, vm::VM}; use dexpr::{ast::value::Value, compiler::Compiler, parser, vm::VM};
use indexmap::IndexMap;
use rust_decimal::Decimal;
use rust_decimal_macros::dec; use rust_decimal_macros::dec;
use smol_str::SmolStr;
use std::rc::Rc;
/// Helper: compile source to bytecode
fn compile(source: &str) -> Vec<u8> {
let ast = parser::program(source).unwrap();
let mut compiler = Compiler::new();
compiler.compile(ast).unwrap()
}
/// Helper: build a sample object with N fields
fn sample_object(n: usize) -> Value {
let mut map = IndexMap::new();
for i in 0..n {
map.insert(SmolStr::new(format!("field{}", i)), Value::Number(Decimal::from(i)));
}
Value::Object(Rc::new(map))
}
pub fn criterion_benchmark(c: &mut Criterion) { pub fn criterion_benchmark(c: &mut Criterion) {
// 1. Parser Benchmark // ── Existing benchmarks ──────────────────────────────────────────────
c.bench_function("parser_long", |b| { c.bench_function("parser_long", |b| {
let input = include_str!("../src/bench_long.dexpr"); let input = include_str!("../examples/bench_long.dexpr");
b.iter(|| { b.iter(|| {
let _ = parser::program(input).unwrap(); let _ = parser::program(input).unwrap();
}) })
}); });
// 2. Compiler Benchmark
c.bench_function("compiler_long", |b| { c.bench_function("compiler_long", |b| {
let input = include_str!("../src/bench_long.dexpr"); let input = include_str!("../examples/bench_long.dexpr");
let ast = parser::program(input).unwrap(); let ast = parser::program(input).unwrap();
b.iter(|| { b.iter(|| {
let mut compiler = Compiler::new(); let mut compiler = Compiler::new();
@@ -23,11 +41,8 @@ pub fn criterion_benchmark(c: &mut Criterion) {
}) })
}); });
// 3. VM Benchmarks
// basic_long.dexpr benchmark
c.bench_function("vm_basic_long", |b| { c.bench_function("vm_basic_long", |b| {
let input = include_str!("../src/basic_long.dexpr"); let input = include_str!("../examples/basic_long.dexpr");
let ast = parser::program(input).unwrap(); let ast = parser::program(input).unwrap();
let mut compiler = Compiler::new(); let mut compiler = Compiler::new();
let bytecode = compiler.compile(ast).unwrap(); let bytecode = compiler.compile(ast).unwrap();
@@ -38,9 +53,8 @@ pub fn criterion_benchmark(c: &mut Criterion) {
}) })
}); });
// Long code benchmark (using bench_long.dexpr)
c.bench_function("vm_long", |b| { c.bench_function("vm_long", |b| {
let input = include_str!("../src/bench_long.dexpr"); let input = include_str!("../examples/bench_long.dexpr");
let ast = parser::program(input).unwrap(); let ast = parser::program(input).unwrap();
let mut compiler = Compiler::new(); let mut compiler = Compiler::new();
let bytecode = compiler.compile(ast).unwrap(); let bytecode = compiler.compile(ast).unwrap();
@@ -50,12 +64,9 @@ pub fn criterion_benchmark(c: &mut Criterion) {
}) })
}); });
// Short code benchmark
c.bench_function("vm_short", |b| { c.bench_function("vm_short", |b| {
let input = "5.12 + test * 1.5"; let input = "5.12 + test * 1.5";
let ast = parser::program(input).unwrap(); let bytecode = compile(input);
let mut compiler = Compiler::new();
let bytecode = compiler.compile(ast).unwrap();
let test_val = dec!(100); let test_val = dec!(100);
b.iter(|| { b.iter(|| {
let mut vm = VM::new(&bytecode); let mut vm = VM::new(&bytecode);
@@ -63,6 +74,220 @@ pub fn criterion_benchmark(c: &mut Criterion) {
let _ = vm.execute().unwrap(); let _ = vm.execute().unwrap();
}) })
}); });
// ── #1: Method dispatch clone overhead ───────────────────────────────
// Object method — clone entire IndexMap per call
c.bench_function("vm_object_method_keys", |b| {
let bytecode = compile("obj.keys()");
let obj = sample_object(20);
b.iter(|| {
let mut vm = VM::new(&bytecode);
vm.set_global("obj", obj.clone());
let _ = vm.execute().unwrap();
})
});
c.bench_function("vm_object_method_length", |b| {
let bytecode = compile("obj.length()");
let obj = sample_object(20);
b.iter(|| {
let mut vm = VM::new(&bytecode);
vm.set_global("obj", obj.clone());
let _ = vm.execute().unwrap();
})
});
c.bench_function("vm_object_method_contains", |b| {
let bytecode = compile(r#"obj.contains("field10")"#);
let obj = sample_object(20);
b.iter(|| {
let mut vm = VM::new(&bytecode);
vm.set_global("obj", obj.clone());
let _ = vm.execute().unwrap();
})
});
// StringList method — clone entire Vec per call
c.bench_function("vm_strlist_method_length", |b| {
let bytecode = compile("items.length()");
let items = Value::StringList(Rc::new((0..50).map(|i| SmolStr::new(format!("item{}", i))).collect()));
b.iter(|| {
let mut vm = VM::new(&bytecode);
vm.set_global("items", items.clone());
let _ = vm.execute().unwrap();
})
});
c.bench_function("vm_numlist_method_sum", |b| {
let bytecode = compile("nums.sum()");
let nums = Value::NumberList(Rc::new((0..100).map(Decimal::from).collect()));
b.iter(|| {
let mut vm = VM::new(&bytecode);
vm.set_global("nums", nums.clone());
let _ = vm.execute().unwrap();
})
});
// String method — lighter clone (SmolStr)
c.bench_function("vm_string_method_upper", |b| {
let bytecode = compile(r#"s.upper()"#);
b.iter(|| {
let mut vm = VM::new(&bytecode);
vm.set_global("s", Value::String(SmolStr::new("hello world this is a test string")));
let _ = vm.execute().unwrap();
})
});
// ── #2 & #3: Vec alloc in method/external calls ─────────────────────
c.bench_function("vm_method_call_with_args", |b| {
let bytecode = compile(r#"s.replace("hello", "world")"#);
b.iter(|| {
let mut vm = VM::new(&bytecode);
vm.set_global("s", Value::String(SmolStr::new("hello world hello")));
let _ = vm.execute().unwrap();
})
});
c.bench_function("vm_external_fn_call", |b| {
let bytecode = compile("getRate(a, b)");
b.iter(|| {
let mut vm = VM::new(&bytecode);
vm.set_global("a", Value::Number(dec!(10)));
vm.set_global("b", Value::Number(dec!(20)));
vm.register_function("getRate", |_args| Ok(Value::Number(dec!(34.5))));
let _ = vm.execute().unwrap();
})
});
// ── #4: Value enum size (cache pressure on register ops) ─────────────
c.bench_function("vm_arithmetic_chain", |b| {
// Pure arithmetic — measures register read/write cache performance
let bytecode = compile(
"a = 1.5\nb = 2.3\nc = a + b\nd = c * a\ne = d - b\nf = e / c\ng = f + d\ng * 2.0",
);
b.iter(|| {
let mut vm = VM::new(&bytecode);
let _ = vm.execute().unwrap();
})
});
c.bench_function("vm_comparison_chain", |b| {
let bytecode = compile(
"a = 10\nb = 20\nc = a < b\nd = b >= a\ne = a == 10\nf = b != 15\nc && d && e && f",
);
b.iter(|| {
let mut vm = VM::new(&bytecode);
let _ = vm.execute().unwrap();
})
});
// ── #5: SmolStr alloc per opcode (string table missing) ──────────────
c.bench_function("vm_global_read_heavy", |b| {
// Many LoadGlobal ops → SmolStr alloc per read (split to stay within register limit)
let bytecode = compile(
"r1 = x1 + x2 + x3\nr2 = x4 + x5 + x6\nr = r1 + r2\nr",
);
b.iter(|| {
let mut vm = VM::new(&bytecode);
for i in 1..=6 {
vm.set_global(&format!("x{}", i), Value::Number(Decimal::from(i)));
}
let _ = vm.execute().unwrap();
})
});
c.bench_function("vm_global_write_heavy", |b| {
// Many StoreGlobal ops
let bytecode = compile(
"a = 1\nb = 2\nc = 3\nd = 4\ne = 5\nf = 6\nr = a + b + c\ns = d + e + f\nr + s",
);
b.iter(|| {
let mut vm = VM::new(&bytecode);
let _ = vm.execute().unwrap();
})
});
c.bench_function("vm_property_access_chain", |b| {
// GetProperty → read_string per access
let bytecode = compile("obj.field0 + obj.field1 + obj.field2 + obj.field3 + obj.field4");
let obj = sample_object(10);
b.iter(|| {
let mut vm = VM::new(&bytecode);
vm.set_global("obj", obj.clone());
let _ = vm.execute().unwrap();
})
});
// ── #8: String concat with format! ───────────────────────────────────
c.bench_function("vm_string_concat", |b| {
let bytecode = compile(
r#"a = "hello" + " " + "world"
b = a + " " + "this"
c = b + " " + "test"
c"#,
);
b.iter(|| {
let mut vm = VM::new(&bytecode);
let _ = vm.execute().unwrap();
})
});
c.bench_function("vm_string_number_coerce", |b| {
let bytecode = compile(
r#"a = "value: " + 42
b = a + " and " + 3.14
b"#,
);
b.iter(|| {
let mut vm = VM::new(&bytecode);
let _ = vm.execute().unwrap();
})
});
// ── #6: Opcode dispatch (overall loop throughput) ────────────────────
c.bench_function("vm_opcode_throughput", |b| {
// Many simple ops to stress the dispatch loop
let bytecode = compile(
"a = 1\nb = 2\nc = a + b\nd = c * 2\ne = d - 1\nf = e / 3\n\
g = f + a\nh = g * b\ni = h - c\nj = i + d\n\
k = j * 2\nl = k - 1\nm = l + 3\nn = m / 2\n\
o = n + a\np = o * b\np",
);
b.iter(|| {
let mut vm = VM::new(&bytecode);
let _ = vm.execute().unwrap();
})
});
// ── Combined: realistic expression with multiple issue areas ─────────
c.bench_function("vm_realistic_mixed", |b| {
let bytecode = compile(
r#"
price = 100.50
tax = 18
discount = 5.5
net = price * (1 + tax / 100) - discount
label = "Total: " + net
if net > 100 then
result = label + " (high)"
else
result = label + " (low)"
end
result
"#,
);
b.iter(|| {
let mut vm = VM::new(&bytecode);
let _ = vm.execute().unwrap();
})
});
} }
criterion_group!(benches, criterion_benchmark); criterion_group!(benches, criterion_benchmark);

View File

@@ -88,12 +88,13 @@ struct Spanned<T> { node: T, span: Span }
| `NumberList(Vec<Decimal>)` | `Vec<Decimal>` | Sayı listesi | | `NumberList(Vec<Decimal>)` | `Vec<Decimal>` | Sayı listesi |
| `StringList(Vec<SmolStr>)` | `Vec<SmolStr>` | String listesi | | `StringList(Vec<SmolStr>)` | `Vec<SmolStr>` | String listesi |
| `Object(IndexMap<SmolStr, Value>)` | `IndexMap<SmolStr, Value>` | Anahtar-değer nesnesi | | `Object(IndexMap<SmolStr, Value>)` | `IndexMap<SmolStr, Value>` | Anahtar-değer nesnesi |
| `List(Vec<Value>)` | `Vec<Value>` | Genel liste (object array dahil) |
### Serileştirme ### Serileştirme
Her `Value` bytecode'a gömülebilir. Serileştirme formatı: Her `Value` bytecode'a gömülebilir. Serileştirme formatı:
1. **Tip etiketi** (1 byte): `NULL=0x00`, `NUMBER=0x01`, `STRING=0x02`, `BOOLEAN=0x03`, `NUMBER_LIST=0x04`, `STRING_LIST=0x05`, `OBJECT=0x06` 1. **Tip etiketi** (1 byte): `NULL=0x00`, `NUMBER=0x01`, `STRING=0x02`, `BOOLEAN=0x03`, `NUMBER_LIST=0x04`, `STRING_LIST=0x05`, `OBJECT=0x06`, `LIST=0x07`
2. **Veri:** 2. **Veri:**
- Number: 16 byte (Decimal serialization) - Number: 16 byte (Decimal serialization)
- String: 2-byte uzunluk + UTF-8 bytes - String: 2-byte uzunluk + UTF-8 bytes
@@ -101,6 +102,7 @@ Her `Value` bytecode'a gömülebilir. Serileştirme formatı:
- NumberList: 2-byte count + her sayı için 16 byte - NumberList: 2-byte count + her sayı için 16 byte
- StringList: 2-byte count + her string için (2-byte uzunluk + bytes) - StringList: 2-byte count + her string için (2-byte uzunluk + bytes)
- Object: 2-byte entry count + her girdi için (anahtar: 2-byte uzunluk + bytes, değer: rekürsif serialize) - Object: 2-byte entry count + her girdi için (anahtar: 2-byte uzunluk + bytes, değer: rekürsif serialize)
- List: 2-byte count + her eleman için rekürsif serialize
`serialize()` ve `deserialize()` metodları bu dönüşümü gerçekleştirir. `serialize()` ve `deserialize()` metodları bu dönüşümü gerçekleştirir.

View File

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

View File

@@ -155,7 +155,9 @@ Bu analiz her autocomplete tetiklendiğinde Lezer tree üzerinde yapılır. Bozu
| `x.` (assignment'tan `String` çıkarıldı) | String metodları | | `x.` (assignment'tan `String` çıkarıldı) | String metodları |
| `items.` (config'de `StringList`) | StringList metodları | | `items.` (config'de `StringList`) | StringList metodları |
| `scores.` (config'de `NumberList`) | NumberList metodları | | `scores.` (config'de `NumberList`) | NumberList metodları |
| `obj.` (config'de `Object`) | Object metodları | | `obj.` (config'de `Object`) | Object field'ları + Object metodları |
| `kalemler.` (config'de `List`) | Element field'ları (property projection) + List metodları |
| `kalemler.tutar.` (List projection → `NumberList`) | NumberList metodları (sum, avg, min, max...) |
| `result.` (tip bilinmiyor) | Tüm metodlar | | `result.` (tip bilinmiyor) | Tüm metodlar |
| `42.` | Öneri yok | | `42.` | Öneri yok |
@@ -260,7 +262,7 @@ Eğer host uygulama çalışma sırasında yeni fonksiyon/değişken eklerse, ed
| Tip | Açıklama | | Tip | Açıklama |
|-----|----------| |-----|----------|
| `DexprLanguageInfo` | Metadata arayüzü (JSON yapısı) | | `DexprLanguageInfo` | Metadata arayüzü (JSON yapısı) |
| `DexprType` | `"String" \| "Number" \| "Boolean" \| "NumberList" \| "StringList" \| "Object"` | | `DexprType` | `"String" \| "Number" \| "Boolean" \| "NumberList" \| "StringList" \| "Object" \| "List"` |
| `FunctionInfo` | Fonksiyon metadata'sı | | `FunctionInfo` | Fonksiyon metadata'sı |
| `MethodInfo` | Metod metadata'sı | | `MethodInfo` | Metod metadata'sı |
| `VariableInfo` | Değişken metadata'sı | | `VariableInfo` | Değişken metadata'sı |

View File

@@ -29,7 +29,7 @@ Editör entegrasyonu için dil metadata'sı üretir. Built-in fonksiyonlar, tipe
| Alan | Tip | Açıklama | | Alan | Tip | Açıklama |
|------|-----|----------| |------|-----|----------|
| `name` | `String` | Değişken adı | | `name` | `String` | Değişken adı |
| `type_name` | `String` | Tip adı: `String`, `Number`, `Boolean`, `NumberList`, `StringList`, `Object` | | `type_name` | `String` | Tip adı: `String`, `Number`, `Boolean`, `NumberList`, `StringList`, `Object`, `List` |
| `doc` | `Option<String>` | Opsiyonel açıklama | | `doc` | `Option<String>` | Opsiyonel açıklama |
### LanguageInfo ### LanguageInfo
@@ -62,6 +62,7 @@ Tüm built-in fonksiyon ve metodları içeren yeni bir `LanguageInfo` oluşturur
| `NumberList` | `length`, `len`, `isEmpty`, `first`, `last`, `get`, `contains`, `indexOf`, `slice`, `reverse`, `sort`, `sum`, `avg`, `min`, `max` | | `NumberList` | `length`, `len`, `isEmpty`, `first`, `last`, `get`, `contains`, `indexOf`, `slice`, `reverse`, `sort`, `sum`, `avg`, `min`, `max` |
| `StringList` | `length`, `len`, `isEmpty`, `first`, `last`, `get`, `contains`, `indexOf`, `slice`, `reverse`, `sort`, `join` | | `StringList` | `length`, `len`, `isEmpty`, `first`, `last`, `get`, `contains`, `indexOf`, `slice`, `reverse`, `sort`, `join` |
| `Object` | `keys`, `values`, `length`, `len`, `contains`, `get` | | `Object` | `keys`, `values`, `length`, `len`, `contains`, `get` |
| `List` | `length`, `len`, `isEmpty`, `first`, `last`, `get`, `contains`, `indexOf`, `slice`, `reverse`, `join`, `map`, `filter`, `find`, `sort` |
### `add_function(name, signature, doc)` ### `add_function(name, signature, doc)`

View File

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

View File

@@ -11,7 +11,9 @@ Register tabanlı sanal makine. Bytecode'u çalıştırır, 8 register ve global
| Dosya | İçerik | | Dosya | İçerik |
|-------|--------| |-------|--------|
| `vm/mod.rs` | Modül export'ları | | `vm/mod.rs` | Modül export'ları |
| `vm/vm.rs` | Ana VM implementasyonu | | `vm/vm.rs` | Ana VM implementasyonu (core çalıştırma döngüsü) |
| `vm/methods.rs` | Metod dispatch (String, StringList, NumberList, Object metodları) |
| `vm/builtins.rs` | Built-in fonksiyon implementasyonları (abs, min, max, floor, ceil, round, sqrt, len, toString, toNumber) |
| `vm/error.rs` | Hata türleri (VMError) | | `vm/error.rs` | Hata türleri (VMError) |
| `vm/debug_info.rs` | Bytecode offset → kaynak konum eşleştirme | | `vm/debug_info.rs` | Bytecode offset → kaynak konum eşleştirme |
@@ -120,6 +122,7 @@ struct VM<'a> {
### Aritmetik ### Aritmetik
- **`binary_op(f, name)`** — Genel handler: iki operand register'ı oku, fonksiyonu uygula, sonucu kaydet - **`binary_op(f, name)`** — Genel handler: iki operand register'ı oku, fonksiyonu uygula, sonucu kaydet
- Sıfıra bölme kontrolü yapılır - Sıfıra bölme kontrolü yapılır
- **`Add` opcode:** Sayısal toplama yanında string birleştirmeyi de destekler. Otomatik tip dönüşümü (auto-coercion) yapılır: String+String, String+Number, Number+String, String+Boolean kombinasyonları birleştirme olarak çalışır
- **`handle_neg()`** — Sadece Number tipinde tekli negatif - **`handle_neg()`** — Sadece Number tipinde tekli negatif
### Karşılaştırma ### Karşılaştırma
@@ -133,18 +136,19 @@ 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. List register'ında property projection yapar: her Object elemanından ilgili alanı çıkarıp NumberList/StringList/List döndürü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
- **String metodları:** `upper`, `lower`, `trim`, `trimStart`, `trimEnd`, `split(delimiter)`, `replace(old, new)`, `startsWith(prefix)`, `endsWith(suffix)`, `contains(substr)`, `length`, `charAt(index)`, `substring(start, end?)` - **String metodları:** `upper`, `lower`, `trim`, `trimStart`, `trimEnd`, `split(delimiter)`, `replace(old, new)`, `startsWith(prefix)`, `endsWith(suffix)`, `contains(substr)`, `length`, `charAt(index)`, `substring(start, end?)`
- **StringList metodları:** `length`/`len`, `isEmpty`, `first`, `last`, `get(index)`, `contains(value)`, `indexOf(value)`, `slice(start, end?)`, `reverse()`, `sort()`, `join(delimiter?)` - **StringList metodları:** `length`/`len`, `isEmpty`, `first`, `last`, `get(index)`, `contains(value)`, `indexOf(value)`, `slice(start, end?)`, `reverse()`, `sort()`, `join(delimiter?)`
- **NumberList metodları:** `length`/`len`, `isEmpty`, `first`, `last`, `get(index)`, `contains(value)`, `indexOf(value)`, `slice(start, end?)`, `reverse()`, `sort()`, `sum`, `avg`, `min`, `max` - **NumberList metodları:** `length`/`len`, `isEmpty`, `first`, `last`, `get(index)`, `contains(value)`, `indexOf(value)`, `slice(start, end?)`, `reverse()`, `sort()`, `sum`, `avg`, `min`, `max`
- **Object metodları:** `keys()`, `values()`, `length`/`len()`, `contains(key)`, `get(key)` - **Object metodları:** `keys()`, `values()`, `length`/`len()`, `contains(key)`, `get(key)`
- **List metodları:** `length`/`len`, `isEmpty`, `first`, `last`, `get(index)`, `contains(value)`, `indexOf(value)`, `slice(start, end?)`, `reverse()`, `join(delim?)`, `map(field)`, `filter(field, value?)`, `find(field, value?)`, `sort(field)`
- **Harici metodlar:** Yukarıdaki built-in metodlar bulunamazsa `external_methods` HashMap'inde aranır - **Harici metodlar:** Yukarıdaki built-in metodlar bulunamazsa `external_methods` HashMap'inde aranır
### Üyelik Testi ### Üyelik Testi
- **`handle_contains()`** — `in` operatörü: String in StringList, Number in NumberList, String in String (substring), String in Object (anahtar varlığı kontrolü) - **`handle_contains()`** — `in` operatörü: String in StringList, Number in NumberList, String in String (substring), String in Object (anahtar varlığı kontrolü), Value in List
### Harici Fonksiyonlar ve Sonuç ### Harici Fonksiyonlar ve Sonuç
- **`handle_call_external()`** — İsimle harici fonksiyon çağır (HashMap lookup) - **`handle_call_external()`** — İsimle harici fonksiyon çağır (HashMap lookup)
@@ -153,6 +157,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.
--- ---
@@ -188,7 +204,7 @@ vm.register_method("Number", "format", |this, args| {
}); });
``` ```
**Tip isimleri:** `"Number"`, `"String"`, `"Boolean"`, `"NumberList"`, `"StringList"`, `"Object"`, `"Null"` **Tip isimleri:** `"Number"`, `"String"`, `"Boolean"`, `"NumberList"`, `"StringList"`, `"Object"`, `"List"`, `"Null"`
--- ---

View File

@@ -15,6 +15,7 @@
"codemirror": "^6.0.2", "codemirror": "^6.0.2",
"tsup": "^8.0.0", "tsup": "^8.0.0",
"typescript": "^5.0.0", "typescript": "^5.0.0",
"vitest": "^4.1.3",
}, },
"peerDependencies": { "peerDependencies": {
"@codemirror/autocomplete": "^6.0.0", "@codemirror/autocomplete": "^6.0.0",
@@ -22,6 +23,7 @@
"@codemirror/state": "^6.0.0", "@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0", "@codemirror/view": "^6.0.0",
"@lezer/highlight": "^1.0.0", "@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0",
}, },
}, },
}, },
@@ -40,6 +42,12 @@
"@codemirror/view": ["@codemirror/view@6.41.0", "", { "dependencies": { "@codemirror/state": "^6.6.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-6H/qadXsVuDY219Yljhohglve8xf4B8xJkVOEWfA5uiYKiTFppjqsvsfR5iPA0RbvRBoOyTZpbLIxe9+0UR8xA=="], "@codemirror/view": ["@codemirror/view@6.41.0", "", { "dependencies": { "@codemirror/state": "^6.6.0", "crelt": "^1.0.6", "style-mod": "^4.1.0", "w3c-keyname": "^2.2.4" } }, "sha512-6H/qadXsVuDY219Yljhohglve8xf4B8xJkVOEWfA5uiYKiTFppjqsvsfR5iPA0RbvRBoOyTZpbLIxe9+0UR8xA=="],
"@emnapi/core": ["@emnapi/core@1.9.1", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.0", "tslib": "^2.4.0" } }, "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA=="],
"@emnapi/runtime": ["@emnapi/runtime@1.9.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA=="],
"@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.2.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="], "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.7", "", { "os": "aix", "cpu": "ppc64" }, "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="], "@esbuild/android-arm": ["@esbuild/android-arm@0.27.7", "", { "os": "android", "cpu": "arm" }, "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ=="],
@@ -110,6 +118,42 @@
"@marijn/find-cluster-break": ["@marijn/find-cluster-break@1.0.2", "", {}, "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="], "@marijn/find-cluster-break": ["@marijn/find-cluster-break@1.0.2", "", {}, "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g=="],
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@1.1.2", "", { "dependencies": { "@tybys/wasm-util": "^0.10.1" }, "peerDependencies": { "@emnapi/core": "^1.7.1", "@emnapi/runtime": "^1.7.1" } }, "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw=="],
"@oxc-project/types": ["@oxc-project/types@0.123.0", "", {}, "sha512-YtECP/y8Mj1lSHiUWGSRzy/C6teUKlS87dEfuVKT09LgQbUsBW1rNg+MiJ4buGu3yuADV60gbIvo9/HplA56Ew=="],
"@rolldown/binding-android-arm64": ["@rolldown/binding-android-arm64@1.0.0-rc.13", "", { "os": "android", "cpu": "arm64" }, "sha512-5ZiiecKH2DXAVJTNN13gNMUcCDg4Jy8ZjbXEsPnqa248wgOVeYRX0iqXXD5Jz4bI9BFHgKsI2qmyJynstbmr+g=="],
"@rolldown/binding-darwin-arm64": ["@rolldown/binding-darwin-arm64@1.0.0-rc.13", "", { "os": "darwin", "cpu": "arm64" }, "sha512-tz/v/8G77seu8zAB3A5sK3UFoOl06zcshEzhUO62sAEtrEuW/H1CcyoupOrD+NbQJytYgA4CppXPzlrmp4JZKA=="],
"@rolldown/binding-darwin-x64": ["@rolldown/binding-darwin-x64@1.0.0-rc.13", "", { "os": "darwin", "cpu": "x64" }, "sha512-8DakphqOz8JrMYWTJmWA+vDJxut6LijZ8Xcdc4flOlAhU7PNVwo2MaWBF9iXjJAPo5rC/IxEFZDhJ3GC7NHvug=="],
"@rolldown/binding-freebsd-x64": ["@rolldown/binding-freebsd-x64@1.0.0-rc.13", "", { "os": "freebsd", "cpu": "x64" }, "sha512-4wBQFfjDuXYN/SVI8inBF3Aa+isq40rc6VMFbk5jcpolUBTe5cYnMsHZ51nFWsx3PVyyNN3vgoESki0Hmr/4BA=="],
"@rolldown/binding-linux-arm-gnueabihf": ["@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.13", "", { "os": "linux", "cpu": "arm" }, "sha512-JW/e4yPIXLms+jmnbwwy5LA/LxVwZUWLN8xug+V200wzaVi5TEGIWQlh8o91gWYFxW609euI98OCCemmWGuPrw=="],
"@rolldown/binding-linux-arm64-gnu": ["@rolldown/binding-linux-arm64-gnu@1.0.0-rc.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-ZfKWpXiUymDnavepCaM6KG/uGydJ4l2nBmMxg60Ci4CbeefpqjPWpfaZM7PThOhk2dssqBAcwLc6rAyr0uTdXg=="],
"@rolldown/binding-linux-arm64-musl": ["@rolldown/binding-linux-arm64-musl@1.0.0-rc.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-bmRg3O6Z0gq9yodKKWCIpnlH051sEfdVwt+6m5UDffAQMUUqU0xjnQqqAUm+Gu7ofAAly9DqiQDtKu2nPDEABA=="],
"@rolldown/binding-linux-ppc64-gnu": ["@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.13", "", { "os": "linux", "cpu": "ppc64" }, "sha512-8Wtnbw4k7pMYN9B/mOEAsQ8HOiq7AZ31Ig4M9BKn2So4xRaFEhtCSa4ZJaOutOWq50zpgR4N5+L/opnlaCx8wQ=="],
"@rolldown/binding-linux-s390x-gnu": ["@rolldown/binding-linux-s390x-gnu@1.0.0-rc.13", "", { "os": "linux", "cpu": "s390x" }, "sha512-D/0Nlo8mQuxSMohNJUF2lDXWRsFDsHldfRRgD9bRgktj+EndGPj4DOV37LqDKPYS+osdyhZEH7fTakTAEcW7qg=="],
"@rolldown/binding-linux-x64-gnu": ["@rolldown/binding-linux-x64-gnu@1.0.0-rc.13", "", { "os": "linux", "cpu": "x64" }, "sha512-eRrPvat2YaVQcwwKi/JzOP6MKf1WRnOCr+VaI3cTWz3ZoLcP/654z90lVCJ4dAuMEpPdke0n+qyAqXDZdIC4rA=="],
"@rolldown/binding-linux-x64-musl": ["@rolldown/binding-linux-x64-musl@1.0.0-rc.13", "", { "os": "linux", "cpu": "x64" }, "sha512-PsdONiFRp8hR8KgVjTWjZ9s7uA3uueWL0t74/cKHfM4dR5zXYv4AjB8BvA+QDToqxAFg4ZkcVEqeu5F7inoz5w=="],
"@rolldown/binding-openharmony-arm64": ["@rolldown/binding-openharmony-arm64@1.0.0-rc.13", "", { "os": "none", "cpu": "arm64" }, "sha512-hCNXgC5dI3TVOLrPT++PKFNZ+1EtS0mLQwfXXXSUD/+rGlB65gZDwN/IDuxLpQP4x8RYYHqGomlUXzpO8aVI2w=="],
"@rolldown/binding-wasm32-wasi": ["@rolldown/binding-wasm32-wasi@1.0.0-rc.13", "", { "dependencies": { "@emnapi/core": "1.9.1", "@emnapi/runtime": "1.9.1", "@napi-rs/wasm-runtime": "^1.1.2" }, "cpu": "none" }, "sha512-viLS5C5et8NFtLWw9Sw3M/w4vvnVkbWkO7wSNh3C+7G1+uCkGpr6PcjNDSFcNtmXY/4trjPBqUfcOL+P3sWy/g=="],
"@rolldown/binding-win32-arm64-msvc": ["@rolldown/binding-win32-arm64-msvc@1.0.0-rc.13", "", { "os": "win32", "cpu": "arm64" }, "sha512-Fqa3Tlt1xL4wzmAYxGNFV36Hb+VfPc9PYU+E25DAnswXv3ODDu/yyWjQDbXMo5AGWkQVjLgQExuVu8I/UaZhPQ=="],
"@rolldown/binding-win32-x64-msvc": ["@rolldown/binding-win32-x64-msvc@1.0.0-rc.13", "", { "os": "win32", "cpu": "x64" }, "sha512-/pLI5kPkGEi44TDlnbio3St/5gUFeN51YWNAk/Gnv6mEQBOahRBh52qVFVBpmrnU01n2yysvBML9Ynu7K4kGAQ=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-rc.13", "", {}, "sha512-3ngTAv6F/Py35BsYbeeLeecvhMKdsKm4AoOETVhAA+Qc8nrA2I0kF7oa93mE9qnIurngOSpMnQ0x2nQY2FPviA=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.1", "", { "os": "android", "cpu": "arm" }, "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA=="], "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.60.1", "", { "os": "android", "cpu": "arm" }, "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.1", "", { "os": "android", "cpu": "arm64" }, "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA=="], "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.60.1", "", { "os": "android", "cpu": "arm64" }, "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA=="],
@@ -160,16 +204,42 @@
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ=="], "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.60.1", "", { "os": "win32", "cpu": "x64" }, "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ=="],
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
"@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="],
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="], "@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@vitest/expect": ["@vitest/expect@4.1.3", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", "@vitest/spy": "4.1.3", "@vitest/utils": "4.1.3", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" } }, "sha512-CW8Q9KMtXDGHj0vCsqui0M5KqRsu0zm0GNDW7Gd3U7nZ2RFpPKSCpeCXoT+/+5zr1TNlsoQRDEz+LzZUyq6gnQ=="],
"@vitest/mocker": ["@vitest/mocker@4.1.3", "", { "dependencies": { "@vitest/spy": "4.1.3", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-XN3TrycitDQSzGRnec/YWgoofkYRhouyVQj4YNsJ5r/STCUFqMrP4+oxEv3e7ZbLi4og5kIHrZwekDJgw6hcjw=="],
"@vitest/pretty-format": ["@vitest/pretty-format@4.1.3", "", { "dependencies": { "tinyrainbow": "^3.1.0" } }, "sha512-hYqqwuMbpkkBodpRh4k4cQSOELxXky1NfMmQvOfKvV8zQHz8x8Dla+2wzElkMkBvSAJX5TRGHJAQvK0TcOafwg=="],
"@vitest/runner": ["@vitest/runner@4.1.3", "", { "dependencies": { "@vitest/utils": "4.1.3", "pathe": "^2.0.3" } }, "sha512-VwgOz5MmT0KhlUj40h02LWDpUBVpflZ/b7xZFA25F29AJzIrE+SMuwzFf0b7t4EXdwRNX61C3B6auIXQTR3ttA=="],
"@vitest/snapshot": ["@vitest/snapshot@4.1.3", "", { "dependencies": { "@vitest/pretty-format": "4.1.3", "@vitest/utils": "4.1.3", "magic-string": "^0.30.21", "pathe": "^2.0.3" } }, "sha512-9l+k/J9KG5wPJDX9BcFFzhhwNjwkRb8RsnYhaT1vPY7OufxmQFc9sZzScRCPTiETzl37mrIWVY9zxzmdVeJwDQ=="],
"@vitest/spy": ["@vitest/spy@4.1.3", "", {}, "sha512-ujj5Uwxagg4XUIfAUyRQxAg631BP6e9joRiN99mr48Bg9fRs+5mdUElhOoZ6rP5mBr8Bs3lmrREnkrQWkrsTCw=="],
"@vitest/utils": ["@vitest/utils@4.1.3", "", { "dependencies": { "@vitest/pretty-format": "4.1.3", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" } }, "sha512-Pc/Oexse/khOWsGB+w3q4yzA4te7W4gpZZAvk+fr8qXfTURZUMj5i7kuxsNK5mP/dEB6ao3jfr0rs17fHhbHdw=="],
"acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="], "acorn": ["acorn@8.16.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
"any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="],
"assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
"bundle-require": ["bundle-require@5.1.0", "", { "dependencies": { "load-tsconfig": "^0.2.3" }, "peerDependencies": { "esbuild": ">=0.18" } }, "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA=="], "bundle-require": ["bundle-require@5.1.0", "", { "dependencies": { "load-tsconfig": "^0.2.3" }, "peerDependencies": { "esbuild": ">=0.18" } }, "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA=="],
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="], "cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
"chai": ["chai@6.2.2", "", {}, "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg=="],
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
"codemirror": ["codemirror@6.0.2", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/commands": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/lint": "^6.0.0", "@codemirror/search": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0" } }, "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw=="], "codemirror": ["codemirror@6.0.2", "", { "dependencies": { "@codemirror/autocomplete": "^6.0.0", "@codemirror/commands": "^6.0.0", "@codemirror/language": "^6.0.0", "@codemirror/lint": "^6.0.0", "@codemirror/search": "^6.0.0", "@codemirror/state": "^6.0.0", "@codemirror/view": "^6.0.0" } }, "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw=="],
@@ -180,12 +250,22 @@
"consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="], "consola": ["consola@3.4.2", "", {}, "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA=="],
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
"crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="], "crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"es-module-lexer": ["es-module-lexer@2.0.0", "", {}, "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw=="],
"esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="], "esbuild": ["esbuild@0.27.7", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.7", "@esbuild/android-arm": "0.27.7", "@esbuild/android-arm64": "0.27.7", "@esbuild/android-x64": "0.27.7", "@esbuild/darwin-arm64": "0.27.7", "@esbuild/darwin-x64": "0.27.7", "@esbuild/freebsd-arm64": "0.27.7", "@esbuild/freebsd-x64": "0.27.7", "@esbuild/linux-arm": "0.27.7", "@esbuild/linux-arm64": "0.27.7", "@esbuild/linux-ia32": "0.27.7", "@esbuild/linux-loong64": "0.27.7", "@esbuild/linux-mips64el": "0.27.7", "@esbuild/linux-ppc64": "0.27.7", "@esbuild/linux-riscv64": "0.27.7", "@esbuild/linux-s390x": "0.27.7", "@esbuild/linux-x64": "0.27.7", "@esbuild/netbsd-arm64": "0.27.7", "@esbuild/netbsd-x64": "0.27.7", "@esbuild/openbsd-arm64": "0.27.7", "@esbuild/openbsd-x64": "0.27.7", "@esbuild/openharmony-arm64": "0.27.7", "@esbuild/sunos-x64": "0.27.7", "@esbuild/win32-arm64": "0.27.7", "@esbuild/win32-ia32": "0.27.7", "@esbuild/win32-x64": "0.27.7" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w=="],
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
"expect-type": ["expect-type@1.3.0", "", {}, "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"fix-dts-default-cjs-exports": ["fix-dts-default-cjs-exports@1.0.1", "", { "dependencies": { "magic-string": "^0.30.17", "mlly": "^1.7.4", "rollup": "^4.34.8" } }, "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg=="], "fix-dts-default-cjs-exports": ["fix-dts-default-cjs-exports@1.0.1", "", { "dependencies": { "magic-string": "^0.30.17", "mlly": "^1.7.4", "rollup": "^4.34.8" } }, "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg=="],
@@ -194,6 +274,30 @@
"joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="],
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ=="],
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.32.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w=="],
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.32.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig=="],
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.32.0", "", { "os": "linux", "cpu": "arm" }, "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw=="],
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ=="],
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.32.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg=="],
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA=="],
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.32.0", "", { "os": "linux", "cpu": "x64" }, "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.32.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.32.0", "", { "os": "win32", "cpu": "x64" }, "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q=="],
"lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="], "lilconfig": ["lilconfig@3.1.3", "", {}, "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw=="],
"lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="], "lines-and-columns": ["lines-and-columns@1.2.4", "", {}, "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg=="],
@@ -208,8 +312,12 @@
"mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
@@ -220,16 +328,28 @@
"pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="], "pkg-types": ["pkg-types@1.3.1", "", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
"postcss": ["postcss@8.5.8", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg=="],
"postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="], "postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="],
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="], "readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="], "resolve-from": ["resolve-from@5.0.0", "", {}, "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw=="],
"rolldown": ["rolldown@1.0.0-rc.13", "", { "dependencies": { "@oxc-project/types": "=0.123.0", "@rolldown/pluginutils": "1.0.0-rc.13" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.13", "@rolldown/binding-darwin-arm64": "1.0.0-rc.13", "@rolldown/binding-darwin-x64": "1.0.0-rc.13", "@rolldown/binding-freebsd-x64": "1.0.0-rc.13", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.13", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.13", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.13", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.13", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.13", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.13", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.13", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.13", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.13", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.13", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.13" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-bvVj8YJmf0rq4pSFmH7laLa6pYrhghv3PRzrCdRAr23g66zOKVJ4wkvFtgohtPLWmthgg8/rkaqRHrpUEh0Zbw=="],
"rollup": ["rollup@4.60.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.1", "@rollup/rollup-android-arm64": "4.60.1", "@rollup/rollup-darwin-arm64": "4.60.1", "@rollup/rollup-darwin-x64": "4.60.1", "@rollup/rollup-freebsd-arm64": "4.60.1", "@rollup/rollup-freebsd-x64": "4.60.1", "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", "@rollup/rollup-linux-arm-musleabihf": "4.60.1", "@rollup/rollup-linux-arm64-gnu": "4.60.1", "@rollup/rollup-linux-arm64-musl": "4.60.1", "@rollup/rollup-linux-loong64-gnu": "4.60.1", "@rollup/rollup-linux-loong64-musl": "4.60.1", "@rollup/rollup-linux-ppc64-gnu": "4.60.1", "@rollup/rollup-linux-ppc64-musl": "4.60.1", "@rollup/rollup-linux-riscv64-gnu": "4.60.1", "@rollup/rollup-linux-riscv64-musl": "4.60.1", "@rollup/rollup-linux-s390x-gnu": "4.60.1", "@rollup/rollup-linux-x64-gnu": "4.60.1", "@rollup/rollup-linux-x64-musl": "4.60.1", "@rollup/rollup-openbsd-x64": "4.60.1", "@rollup/rollup-openharmony-arm64": "4.60.1", "@rollup/rollup-win32-arm64-msvc": "4.60.1", "@rollup/rollup-win32-ia32-msvc": "4.60.1", "@rollup/rollup-win32-x64-gnu": "4.60.1", "@rollup/rollup-win32-x64-msvc": "4.60.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w=="], "rollup": ["rollup@4.60.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.60.1", "@rollup/rollup-android-arm64": "4.60.1", "@rollup/rollup-darwin-arm64": "4.60.1", "@rollup/rollup-darwin-x64": "4.60.1", "@rollup/rollup-freebsd-arm64": "4.60.1", "@rollup/rollup-freebsd-x64": "4.60.1", "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", "@rollup/rollup-linux-arm-musleabihf": "4.60.1", "@rollup/rollup-linux-arm64-gnu": "4.60.1", "@rollup/rollup-linux-arm64-musl": "4.60.1", "@rollup/rollup-linux-loong64-gnu": "4.60.1", "@rollup/rollup-linux-loong64-musl": "4.60.1", "@rollup/rollup-linux-ppc64-gnu": "4.60.1", "@rollup/rollup-linux-ppc64-musl": "4.60.1", "@rollup/rollup-linux-riscv64-gnu": "4.60.1", "@rollup/rollup-linux-riscv64-musl": "4.60.1", "@rollup/rollup-linux-s390x-gnu": "4.60.1", "@rollup/rollup-linux-x64-gnu": "4.60.1", "@rollup/rollup-linux-x64-musl": "4.60.1", "@rollup/rollup-openbsd-x64": "4.60.1", "@rollup/rollup-openharmony-arm64": "4.60.1", "@rollup/rollup-win32-arm64-msvc": "4.60.1", "@rollup/rollup-win32-ia32-msvc": "4.60.1", "@rollup/rollup-win32-x64-gnu": "4.60.1", "@rollup/rollup-win32-x64-msvc": "4.60.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w=="],
"siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
"source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="], "source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
"std-env": ["std-env@4.0.0", "", {}, "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ=="],
"style-mod": ["style-mod@4.1.3", "", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="], "style-mod": ["style-mod@4.1.3", "", {}, "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ=="],
"sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="], "sucrase": ["sucrase@3.35.1", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", "pirates": "^4.0.1", "tinyglobby": "^0.2.11", "ts-interface-checker": "^0.1.9" }, "bin": { "sucrase": "bin/sucrase", "sucrase-node": "bin/sucrase-node" } }, "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw=="],
@@ -238,20 +358,34 @@
"thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="],
"tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
"tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="],
"tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="], "tinyglobby": ["tinyglobby@0.2.15", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.3" } }, "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ=="],
"tinyrainbow": ["tinyrainbow@3.1.0", "", {}, "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw=="],
"tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="], "tree-kill": ["tree-kill@1.2.2", "", { "bin": { "tree-kill": "cli.js" } }, "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A=="],
"ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="], "ts-interface-checker": ["ts-interface-checker@0.1.13", "", {}, "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"tsup": ["tsup@8.5.1", "", { "dependencies": { "bundle-require": "^5.1.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "consola": "^3.4.0", "debug": "^4.4.0", "esbuild": "^0.27.0", "fix-dts-default-cjs-exports": "^1.0.0", "joycon": "^3.1.1", "picocolors": "^1.1.1", "postcss-load-config": "^6.0.1", "resolve-from": "^5.0.0", "rollup": "^4.34.8", "source-map": "^0.7.6", "sucrase": "^3.35.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.11", "tree-kill": "^1.2.2" }, "peerDependencies": { "@microsoft/api-extractor": "^7.36.0", "@swc/core": "^1", "postcss": "^8.4.12", "typescript": ">=4.5.0" }, "optionalPeers": ["@microsoft/api-extractor", "@swc/core", "postcss", "typescript"], "bin": { "tsup": "dist/cli-default.js", "tsup-node": "dist/cli-node.js" } }, "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing=="], "tsup": ["tsup@8.5.1", "", { "dependencies": { "bundle-require": "^5.1.0", "cac": "^6.7.14", "chokidar": "^4.0.3", "consola": "^3.4.0", "debug": "^4.4.0", "esbuild": "^0.27.0", "fix-dts-default-cjs-exports": "^1.0.0", "joycon": "^3.1.1", "picocolors": "^1.1.1", "postcss-load-config": "^6.0.1", "resolve-from": "^5.0.0", "rollup": "^4.34.8", "source-map": "^0.7.6", "sucrase": "^3.35.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.11", "tree-kill": "^1.2.2" }, "peerDependencies": { "@microsoft/api-extractor": "^7.36.0", "@swc/core": "^1", "postcss": "^8.4.12", "typescript": ">=4.5.0" }, "optionalPeers": ["@microsoft/api-extractor", "@swc/core", "postcss", "typescript"], "bin": { "tsup": "dist/cli-default.js", "tsup-node": "dist/cli-node.js" } }, "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing=="],
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], "ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="],
"vite": ["vite@8.0.6", "", { "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", "postcss": "^8.5.8", "rolldown": "1.0.0-rc.13", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "@vitejs/devtools": "^0.1.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "@vitejs/devtools", "esbuild", "jiti", "less", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-jeOXoY6N8rOfit/mZADMd0misLqjRdWBB3/S23ZQNuPcbVsfMBJutWD8b4ftdczMOsNyMBnKro0Z1Kt0HIqq5Q=="],
"vitest": ["vitest@4.1.3", "", { "dependencies": { "@vitest/expect": "4.1.3", "@vitest/mocker": "4.1.3", "@vitest/pretty-format": "4.1.3", "@vitest/runner": "4.1.3", "@vitest/snapshot": "4.1.3", "@vitest/spy": "4.1.3", "@vitest/utils": "4.1.3", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", "obug": "^2.1.1", "pathe": "^2.0.3", "picomatch": "^4.0.3", "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", "tinyexec": "^1.0.2", "tinyglobby": "^0.2.15", "tinyrainbow": "^3.1.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", "@vitest/browser-playwright": "4.1.3", "@vitest/browser-preview": "4.1.3", "@vitest/browser-webdriverio": "4.1.3", "@vitest/coverage-istanbul": "4.1.3", "@vitest/coverage-v8": "4.1.3", "@vitest/ui": "4.1.3", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@opentelemetry/api", "@types/node", "@vitest/browser-playwright", "@vitest/browser-preview", "@vitest/browser-webdriverio", "@vitest/coverage-istanbul", "@vitest/coverage-v8", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-DBc4Tx0MPNsqb9isoyOq00lHftVx/KIU44QOm2q59npZyLUkENn8TMFsuzuO+4U2FUa9rgbbPt3udrP25GcjXw=="],
"w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="], "w3c-keyname": ["w3c-keyname@2.2.8", "", {}, "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ=="],
"why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
"vitest/tinyexec": ["tinyexec@1.0.4", "", {}, "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw=="],
} }
} }

15
editor/dist/index.cjs vendored
View File

@@ -285,6 +285,15 @@ function inferMethodReturnType(method) {
// depends on input type // depends on input type
case "join": case "join":
return "String"; return "String";
// List methods
case "map":
return null;
// depends on field type (NumberList, StringList, or List)
case "filter":
return "List";
case "find":
return null;
// returns single element
default: default:
return null; return null;
} }
@@ -318,7 +327,7 @@ function dexprCompletion(info) {
} }
const objectFieldCompletions = /* @__PURE__ */ new Map(); const objectFieldCompletions = /* @__PURE__ */ new Map();
for (const v of info.variables ?? []) { for (const v of info.variables ?? []) {
if (v.type === "Object" && v.fields) { if ((v.type === "Object" || v.type === "List") && v.fields) {
const fieldItems = []; const fieldItems = [];
for (const f of v.fields) { for (const f of v.fields) {
configVarTypes.set(`${v.name}.${f.name}`, f.type); configVarTypes.set(`${v.name}.${f.name}`, f.type);
@@ -392,6 +401,10 @@ function dexprCompletion(info) {
const fieldItems = objectFieldCompletions.get(rootVarName) ?? []; const fieldItems = objectFieldCompletions.get(rootVarName) ?? [];
const objMethods = methodsByType["Object"] ?? []; const objMethods = methodsByType["Object"] ?? [];
options = [...fieldItems, ...objMethods]; options = [...fieldItems, ...objMethods];
} else if (finalType === "List") {
const rootVarName = path[0];
const listMethods = methodsByType["List"] ?? [];
options = [...listMethods];
} else if (finalType) { } else if (finalType) {
options = methodsByType[finalType] ?? allMethods; options = methodsByType[finalType] ?? allMethods;
} else { } else {

View File

@@ -2,7 +2,7 @@ import { Extension } from '@codemirror/state';
import { Completion } from '@codemirror/autocomplete'; import { Completion } from '@codemirror/autocomplete';
import { LRLanguage, HighlightStyle } from '@codemirror/language'; import { LRLanguage, HighlightStyle } from '@codemirror/language';
type DexprType = "String" | "Number" | "Boolean" | "NumberList" | "StringList" | "Object"; type DexprType = "String" | "Number" | "Boolean" | "NumberList" | "StringList" | "Object" | "List";
interface FunctionInfo { interface FunctionInfo {
name: string; name: string;
signature: string; signature: string;

View File

@@ -2,7 +2,7 @@ import { Extension } from '@codemirror/state';
import { Completion } from '@codemirror/autocomplete'; import { Completion } from '@codemirror/autocomplete';
import { LRLanguage, HighlightStyle } from '@codemirror/language'; import { LRLanguage, HighlightStyle } from '@codemirror/language';
type DexprType = "String" | "Number" | "Boolean" | "NumberList" | "StringList" | "Object"; type DexprType = "String" | "Number" | "Boolean" | "NumberList" | "StringList" | "Object" | "List";
interface FunctionInfo { interface FunctionInfo {
name: string; name: string;
signature: string; signature: string;

15
editor/dist/index.js vendored
View File

@@ -258,6 +258,15 @@ function inferMethodReturnType(method) {
// depends on input type // depends on input type
case "join": case "join":
return "String"; return "String";
// List methods
case "map":
return null;
// depends on field type (NumberList, StringList, or List)
case "filter":
return "List";
case "find":
return null;
// returns single element
default: default:
return null; return null;
} }
@@ -291,7 +300,7 @@ function dexprCompletion(info) {
} }
const objectFieldCompletions = /* @__PURE__ */ new Map(); const objectFieldCompletions = /* @__PURE__ */ new Map();
for (const v of info.variables ?? []) { for (const v of info.variables ?? []) {
if (v.type === "Object" && v.fields) { if ((v.type === "Object" || v.type === "List") && v.fields) {
const fieldItems = []; const fieldItems = [];
for (const f of v.fields) { for (const f of v.fields) {
configVarTypes.set(`${v.name}.${f.name}`, f.type); configVarTypes.set(`${v.name}.${f.name}`, f.type);
@@ -365,6 +374,10 @@ function dexprCompletion(info) {
const fieldItems = objectFieldCompletions.get(rootVarName) ?? []; const fieldItems = objectFieldCompletions.get(rootVarName) ?? [];
const objMethods = methodsByType["Object"] ?? []; const objMethods = methodsByType["Object"] ?? [];
options = [...fieldItems, ...objMethods]; options = [...fieldItems, ...objMethods];
} else if (finalType === "List") {
const rootVarName = path[0];
const listMethods = methodsByType["List"] ?? [];
options = [...listMethods];
} else if (finalType) { } else if (finalType) {
options = methodsByType[finalType] ?? allMethods; options = methodsByType[finalType] ?? allMethods;
} else { } else {

View File

@@ -1,6 +1,6 @@
{ {
"name": "codemirror-lang-dexpr", "name": "@duhanbalci/codemirror-lang-dexpr",
"version": "0.1.0", "version": "0.3.0",
"description": "CodeMirror 6 language support for dexpr", "description": "CodeMirror 6 language support for dexpr",
"type": "module", "type": "module",
"main": "dist/index.cjs", "main": "dist/index.cjs",
@@ -17,7 +17,8 @@
"scripts": { "scripts": {
"build": "tsup src/index.ts --format esm,cjs --dts --external @codemirror/language --external @codemirror/autocomplete --external @codemirror/state --external @codemirror/view --external @lezer/highlight --external @lezer/lr", "build": "tsup src/index.ts --format esm,cjs --dts --external @codemirror/language --external @codemirror/autocomplete --external @codemirror/state --external @codemirror/view --external @lezer/highlight --external @lezer/lr",
"demo": "tsup demo.ts --format iife --outDir dist --no-dts", "demo": "tsup demo.ts --format iife --outDir dist --no-dts",
"dev": "tsup src/index.ts --format esm,cjs --dts --watch" "dev": "tsup src/index.ts --format esm,cjs --dts --watch",
"test": "vitest run"
}, },
"peerDependencies": { "peerDependencies": {
"@codemirror/autocomplete": "^6.0.0", "@codemirror/autocomplete": "^6.0.0",
@@ -37,7 +38,8 @@
"@lezer/lr": "^1.4.8", "@lezer/lr": "^1.4.8",
"codemirror": "^6.0.2", "codemirror": "^6.0.2",
"tsup": "^8.0.0", "tsup": "^8.0.0",
"typescript": "^5.0.0" "typescript": "^5.0.0",
"vitest": "^4.1.3"
}, },
"license": "MIT" "license": "MIT"
} }

View File

@@ -0,0 +1,149 @@
import { describe, it, expect } from "vitest";
import { inferMethodReturnType, projectedListType } from "./completions";
import type { DexprType } from "./completions";
// ==================== inferMethodReturnType ====================
describe("inferMethodReturnType", () => {
// String methods → String
it.each(["upper", "lower", "trim", "trimStart", "trimEnd", "replace", "charAt", "substring"])(
"%s → String",
(method) => {
expect(inferMethodReturnType(method)).toBe("String");
}
);
// String/List methods → Boolean
it.each(["contains", "startsWith", "endsWith", "isEmpty"])(
"%s → Boolean",
(method) => {
expect(inferMethodReturnType(method)).toBe("Boolean");
}
);
// Methods → Number
it.each(["length", "len", "indexOf", "sum", "avg", "min", "max", "first", "last"])(
"%s → Number",
(method) => {
expect(inferMethodReturnType(method)).toBe("Number");
}
);
// split → StringList
it("split → StringList", () => {
expect(inferMethodReturnType("split")).toBe("StringList");
});
// join → String
it("join → String", () => {
expect(inferMethodReturnType("join")).toBe("String");
});
// filter → List
it("filter → List", () => {
expect(inferMethodReturnType("filter")).toBe("List");
});
// Methods that depend on input type → null
it.each(["reverse", "sort", "slice", "map", "find"])(
"%s → null (context-dependent)",
(method) => {
expect(inferMethodReturnType(method)).toBeNull();
}
);
// Unknown method → null
it("unknown method → null", () => {
expect(inferMethodReturnType("foobar")).toBeNull();
});
});
// ==================== projectedListType ====================
describe("projectedListType", () => {
it("Number field → NumberList", () => {
expect(projectedListType("Number")).toBe("NumberList");
});
it("String field → StringList", () => {
expect(projectedListType("String")).toBe("StringList");
});
it("Boolean field → List", () => {
expect(projectedListType("Boolean")).toBe("List");
});
it("Object field → List", () => {
expect(projectedListType("Object")).toBe("List");
});
it("List field → List", () => {
expect(projectedListType("List")).toBe("List");
});
it("null (unknown) field → List", () => {
expect(projectedListType(null)).toBe("List");
});
it("NumberList field → List (nested lists stay as List)", () => {
expect(projectedListType("NumberList")).toBe("List");
});
it("StringList field → List", () => {
expect(projectedListType("StringList")).toBe("List");
});
});
// ==================== Type flow scenarios ====================
describe("type flow scenarios", () => {
// Simulates what happens in the autocomplete pipeline
it("kalemler.tutar.sum() — List → NumberList → Number", () => {
// Step 1: kalemler is List, tutar field is Number
const projectedType = projectedListType("Number");
expect(projectedType).toBe("NumberList");
// Step 2: .sum() on NumberList returns Number
const resultType = inferMethodReturnType("sum");
expect(resultType).toBe("Number");
});
it("kalemler.adi.join() — List → StringList → String", () => {
const projectedType = projectedListType("String");
expect(projectedType).toBe("StringList");
const resultType = inferMethodReturnType("join");
expect(resultType).toBe("String");
});
it("kalemler.filter().tutar.sum() — List → List → NumberList → Number", () => {
// filter returns List
const afterFilter = inferMethodReturnType("filter");
expect(afterFilter).toBe("List");
// .tutar on List with Number field
const afterProjection = projectedListType("Number");
expect(afterProjection).toBe("NumberList");
// .sum() on NumberList
const result = inferMethodReturnType("sum");
expect(result).toBe("Number");
});
it("kalemler.tutar.max() — projection then aggregate", () => {
const projected = projectedListType("Number");
expect(projected).toBe("NumberList");
const result = inferMethodReturnType("max");
expect(result).toBe("Number");
});
it("kalemler.birim.contains() — StringList method", () => {
const projected = projectedListType("String");
expect(projected).toBe("StringList");
const result = inferMethodReturnType("contains");
expect(result).toBe("Boolean");
});
});

View File

@@ -17,7 +17,8 @@ export type DexprType =
| "Boolean" | "Boolean"
| "NumberList" | "NumberList"
| "StringList" | "StringList"
| "Object"; | "Object"
| "List";
export interface FunctionInfo { export interface FunctionInfo {
name: string; name: string;
@@ -171,15 +172,15 @@ function inferExprType(
if (objNode.name === "VariableName") { if (objNode.name === "VariableName") {
const varName = doc.sliceString(objNode.from, objNode.to); const varName = doc.sliceString(objNode.from, objNode.to);
const fieldName = doc.sliceString(propNode.from, propNode.to); const fieldName = doc.sliceString(propNode.from, propNode.to);
// Look up from objectFieldTypes via the global lookup
// (We use knownTypes to check if root is Object, then check field)
const rootType = knownTypes.get(varName); const rootType = knownTypes.get(varName);
if (rootType === "Object") { if (rootType === "Object") {
// Field type needs to come from config — stored as "varName.fieldName" key
// We can't access objectFieldTypes here, so use the convention
// that knownTypes may contain "varName.fieldName" entries
return knownTypes.get(`${varName}.${fieldName}`) ?? null; return knownTypes.get(`${varName}.${fieldName}`) ?? null;
} }
if (rootType === "List") {
// Property projection: list.field → typed list based on field type
const fieldType = knownTypes.get(`${varName}.${fieldName}`) ?? null;
return projectedListType(fieldType);
}
} }
return null; return null;
} }
@@ -204,7 +205,7 @@ function findChild(
} }
/** Infer return type from known method names */ /** Infer return type from known method names */
function inferMethodReturnType(method: string): DexprType | null { export function inferMethodReturnType(method: string): DexprType | null {
switch (method) { switch (method) {
// String -> String // String -> String
case "upper": case "upper":
@@ -245,11 +246,29 @@ function inferMethodReturnType(method: string): DexprType | null {
return null; // depends on input type return null; // depends on input type
case "join": case "join":
return "String"; return "String";
// List methods
case "map":
return null; // depends on field type (NumberList, StringList, or List)
case "filter":
return "List";
case "find":
return null; // returns single element
default: default:
return null; return null;
} }
} }
/**
* Given a field type from an Object element within a List,
* return the projected list type after property access.
* e.g. List with Number field "tutar" → kalemler.tutar → NumberList
*/
export function projectedListType(fieldType: DexprType | null): DexprType {
if (fieldType === "Number") return "NumberList";
if (fieldType === "String") return "StringList";
return "List";
}
// --- Autocomplete --- // --- Autocomplete ---
function dedup(items: Completion[]): Completion[] { function dedup(items: Completion[]): Completion[] {
@@ -290,7 +309,7 @@ export function dexprCompletion(info: DexprLanguageInfo): Extension {
// and field completions per Object variable // and field completions per Object variable
const objectFieldCompletions = new Map<string, Completion[]>(); const objectFieldCompletions = new Map<string, Completion[]>();
for (const v of info.variables ?? []) { for (const v of info.variables ?? []) {
if (v.type === "Object" && v.fields) { if ((v.type === "Object" || v.type === "List") && v.fields) {
const fieldItems: Completion[] = []; const fieldItems: Completion[] = [];
for (const f of v.fields) { for (const f of v.fields) {
// Store "customer.name" → "String" in configVarTypes for type inference // Store "customer.name" → "String" in configVarTypes for type inference
@@ -354,13 +373,17 @@ export function dexprCompletion(info: DexprLanguageInfo): Extension {
// e.g. path=["customer","name"] → look up "customer.name" in varTypes // e.g. path=["customer","name"] → look up "customer.name" in varTypes
let currentType = rootType; let currentType = rootType;
for (let i = 1; i < path.length; i++) { for (let i = 1; i < path.length; i++) {
if (currentType !== "Object") { if (currentType === "Object") {
const key = `${path[i - 1]}.${path[i]}`;
currentType = varTypes.get(key) ?? null;
} else if (currentType === "List") {
// Property projection: list.field → typed list
const key = `${path[0]}.${path[i]}`;
const fieldType = varTypes.get(key) ?? null;
currentType = projectedListType(fieldType);
} else {
return { type: currentType, path }; return { type: currentType, path };
} }
// "customer.name" key convention
const key = `${path[i - 1]}.${path[i]}`;
const fieldType = varTypes.get(key) ?? null;
currentType = fieldType;
} }
return { type: currentType, path }; return { type: currentType, path };
@@ -408,6 +431,12 @@ export function dexprCompletion(info: DexprLanguageInfo): Extension {
const fieldItems = objectFieldCompletions.get(rootVarName) ?? []; const fieldItems = objectFieldCompletions.get(rootVarName) ?? [];
const objMethods = methodsByType["Object"] ?? []; const objMethods = methodsByType["Object"] ?? [];
options = [...fieldItems, ...objMethods]; options = [...fieldItems, ...objMethods];
} else if (finalType === "List") {
// Show field names (property projection) + List methods
const rootVarName = path[0];
const fieldItems = objectFieldCompletions.get(rootVarName) ?? [];
const listMethods = methodsByType["List"] ?? [];
options = [...fieldItems, ...listMethods];
} else if (finalType) { } else if (finalType) {
options = methodsByType[finalType] ?? allMethods; options = methodsByType[finalType] ?? allMethods;
} else { } else {

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 48 KiB

74
gen.js
View File

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

View File

@@ -12,15 +12,21 @@ bench:
run: run:
cargo run --release cargo run --release
# --- Publish ---
# Publish dexpr to Gitea cargo registry
publish:
cargo publish --registry gitea --allow-dirty
# --- WASM --- # --- WASM ---
# Build wasm package (web target) # Build wasm package (web target)
wasm: wasm:
cd wasm && wasm-pack build --target web --release cd wasm && wasm-pack build --target web --release --scope duhanbalci
# Build wasm package (bundler target, for npm) # Build wasm package (bundler target, for npm)
wasm-bundler: wasm-bundler:
cd wasm && wasm-pack build --target bundler --release cd wasm && wasm-pack build --target bundler --release --scope duhanbalci
# --- Editor --- # --- Editor ---
@@ -36,6 +42,14 @@ editor-build: editor-grammar
editor-demo: editor-build editor-demo: editor-build
cd editor && bun run demo cd editor && bun run demo
# Publish editor package to Gitea npm registry
editor-publish: editor-build
cd editor && npm publish
# Publish wasm package to Gitea npm registry
wasm-publish: wasm-bundler
cd wasm/pkg && npm publish
# --- Combined --- # --- Combined ---
# Build everything (wasm + editor) # Build everything (wasm + editor)

Binary file not shown.

View File

@@ -2,6 +2,7 @@ use indexmap::IndexMap;
use rust_decimal::Decimal; use rust_decimal::Decimal;
use smol_str::SmolStr; use smol_str::SmolStr;
use std::fmt; use std::fmt;
use std::rc::Rc;
/// Value type for the dExpr language /// Value type for the dExpr language
#[derive(Debug, Clone, PartialEq, Default)] #[derive(Debug, Clone, PartialEq, Default)]
@@ -11,9 +12,10 @@ 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>>),
List(Rc<Vec<Value>>),
} }
/// Type tag constants for serialization /// Type tag constants for serialization
@@ -24,6 +26,7 @@ pub const TYPE_BOOLEAN: u8 = 0x03;
pub const TYPE_NUMBER_LIST: u8 = 0x04; pub const TYPE_NUMBER_LIST: u8 = 0x04;
pub const TYPE_STRING_LIST: u8 = 0x05; pub const TYPE_STRING_LIST: u8 = 0x05;
pub const TYPE_OBJECT: u8 = 0x06; pub const TYPE_OBJECT: u8 = 0x06;
pub const TYPE_LIST: u8 = 0x07;
impl fmt::Display for Value { impl fmt::Display for Value {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@@ -62,6 +65,16 @@ impl fmt::Display for Value {
} }
write!(f, "}}") write!(f, "}}")
} }
Value::List(list) => {
write!(f, "[")?;
for (i, val) in list.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{}", val)?;
}
write!(f, "]")
}
} }
} }
} }
@@ -77,6 +90,7 @@ impl Value {
Value::NumberList(_) => TYPE_NUMBER_LIST, Value::NumberList(_) => TYPE_NUMBER_LIST,
Value::StringList(_) => TYPE_STRING_LIST, Value::StringList(_) => TYPE_STRING_LIST,
Value::Object(_) => TYPE_OBJECT, Value::Object(_) => TYPE_OBJECT,
Value::List(_) => TYPE_LIST,
} }
} }
@@ -90,6 +104,7 @@ impl Value {
Value::NumberList(_) => "NumberList", Value::NumberList(_) => "NumberList",
Value::StringList(_) => "StringList", Value::StringList(_) => "StringList",
Value::Object(_) => "Object", Value::Object(_) => "Object",
Value::List(_) => "List",
} }
} }
@@ -120,7 +135,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 +144,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,13 +157,22 @@ 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());
bytes.extend_from_slice(&val.serialize()); bytes.extend_from_slice(&val.serialize());
} }
} }
Value::List(list) => {
// List length (2 bytes)
bytes.push((list.len() >> 8) as u8);
bytes.push(list.len() as u8);
// List items (recursive serialization)
for val in list.iter() {
bytes.extend_from_slice(&val.serialize());
}
}
} }
bytes bytes
@@ -209,19 +233,25 @@ 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))
}
}
impl From<Vec<Value>> for Value {
fn from(v: Vec<Value>) -> Self {
Value::List(Rc::new(v))
} }
} }
@@ -292,7 +322,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 +351,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 +384,23 @@ impl Value {
map.insert(key.into(), val); map.insert(key.into(), val);
} }
Ok((Value::Object(map), pos)) Ok((Value::Object(Rc::new(map)), pos))
}
TYPE_LIST => {
if bytes.len() < pos + 2 {
return Err("Insufficient bytes for List length".to_string());
}
let len = u16::from_be_bytes([bytes[pos], bytes[pos + 1]]) as usize;
pos += 2;
let mut list = Vec::with_capacity(len);
for _ in 0..len {
let (val, val_bytes) = Value::deserialize(&bytes[pos..])?;
pos += val_bytes;
list.push(val);
}
Ok((Value::List(Rc::new(list)), pos))
} }
_ => Err(format!("Unknown type tag: {}", type_tag)), _ => Err(format!("Unknown type tag: {}", type_tag)),
} }
@@ -407,7 +453,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,14 +464,18 @@ 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()) let mut items = Vec::with_capacity(arr.len());
for item in arr {
items.push(Self::from_json_value(item)?);
}
Ok(Value::List(Rc::new(items)))
} }
} }
serde_json::Value::Object(obj) => { serde_json::Value::Object(obj) => {
@@ -433,7 +483,7 @@ impl Value {
for (k, v) in obj { for (k, v) in obj {
map.insert(SmolStr::new(k), Self::from_json_value(v)?); map.insert(SmolStr::new(k), Self::from_json_value(v)?);
} }
Ok(Value::Object(map)) Ok(Value::Object(Rc::new(map)))
} }
} }
} }

View File

@@ -126,9 +126,9 @@ impl<'a> BytecodeReader<'a> {
self.read_byte() self.read_byte()
} }
/// Read a string /// Read a string as a borrowed slice (zero-copy from bytecode)
#[inline(always)] #[inline(always)]
pub fn read_string(&mut self) -> Result<SmolStr, String> { pub fn read_str(&mut self) -> Result<&'a str, String> {
let length = self.read_u16()? as usize; let length = self.read_u16()? as usize;
if self.position + length > self.bytecode.len() { if self.position + length > self.bytecode.len() {
@@ -139,7 +139,13 @@ impl<'a> BytecodeReader<'a> {
.map_err(|_| "Invalid UTF-8 string".to_string())?; .map_err(|_| "Invalid UTF-8 string".to_string())?;
self.position += length; self.position += length;
Ok(s.into()) Ok(s)
}
/// Read a string as SmolStr (allocating — use read_str when possible)
#[inline(always)]
pub fn read_string(&mut self) -> Result<SmolStr, String> {
self.read_str().map(SmolStr::from)
} }
/// Read a value /// Read a value

View File

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

View File

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

View File

@@ -11,6 +11,7 @@
//! use indexmap::IndexMap; //! use indexmap::IndexMap;
//! use smol_str::SmolStr; //! use smol_str::SmolStr;
//! use rust_decimal_macros::dec; //! use rust_decimal_macros::dec;
//! use std::rc::Rc;
//! //!
//! let mut info = LanguageInfo::builtin(); //! let mut info = LanguageInfo::builtin();
//! //!
@@ -24,7 +25,7 @@
//! let mut customer = IndexMap::new(); //! let mut customer = IndexMap::new();
//! customer.insert(SmolStr::new("name"), Value::String("Alice".into())); //! customer.insert(SmolStr::new("name"), Value::String("Alice".into()));
//! customer.insert(SmolStr::new("age"), Value::Number(dec!(30))); //! customer.insert(SmolStr::new("age"), Value::Number(dec!(30)));
//! info.add_value("customer", &Value::Object(customer), None); //! info.add_value("customer", &Value::Object(Rc::new(customer)), None);
//! info.add_value("price", &Value::Number(dec!(100)), None); //! info.add_value("price", &Value::Number(dec!(100)), None);
//! //!
//! let json = info.to_json(); //! let json = info.to_json();
@@ -120,6 +121,19 @@ impl LanguageInfo {
type_name: v.type_name().to_string(), type_name: v.type_name().to_string(),
}).collect()) }).collect())
} }
Value::List(list) => {
// For List of Objects, derive fields from the first Object element
list.iter().find_map(|item| {
if let Value::Object(map) = item {
Some(map.iter().map(|(k, v)| FieldInfo {
name: k.to_string(),
type_name: v.type_name().to_string(),
}).collect())
} else {
None
}
})
}
_ => None, _ => None,
}; };
self.variables.push(VariableInfo { self.variables.push(VariableInfo {
@@ -275,6 +289,23 @@ fn builtin_methods() -> Vec<(&'static str, Vec<MethodInfo>)> {
MethodInfo { name: "contains", signature: "(key: String) -> Boolean", doc: Some("Check if key exists") }, MethodInfo { name: "contains", signature: "(key: String) -> Boolean", doc: Some("Check if key exists") },
MethodInfo { name: "get", signature: "(key: String) -> any", doc: Some("Get value by key") }, MethodInfo { name: "get", signature: "(key: String) -> any", doc: Some("Get value by key") },
]), ]),
("List", vec![
MethodInfo { name: "length", signature: "() -> Number", doc: Some("Number of elements") },
MethodInfo { name: "len", signature: "() -> Number", doc: None },
MethodInfo { name: "isEmpty", signature: "() -> Boolean", doc: None },
MethodInfo { name: "first", signature: "() -> any", doc: Some("First element") },
MethodInfo { name: "last", signature: "() -> any", doc: Some("Last element") },
MethodInfo { name: "get", signature: "(index: Number) -> any", doc: None },
MethodInfo { name: "contains", signature: "(value: any) -> Boolean", doc: None },
MethodInfo { name: "indexOf", signature: "(value: any) -> Number", doc: None },
MethodInfo { name: "slice", signature: "(start: Number, end?: Number) -> List", doc: None },
MethodInfo { name: "reverse", signature: "() -> List", doc: None },
MethodInfo { name: "join", signature: "(delim?: String) -> String", doc: None },
MethodInfo { name: "map", signature: "(field: String) -> NumberList | StringList | List", doc: Some("Extract field from each Object element") },
MethodInfo { name: "filter", signature: "(field: String, value?: any) -> List", doc: Some("Filter by field value or truthy field") },
MethodInfo { name: "find", signature: "(field: String, value?: any) -> any", doc: Some("Find first element matching field condition") },
MethodInfo { name: "sort", signature: "(field: String) -> List", doc: Some("Sort by field value") },
]),
("StringList", vec![ ("StringList", vec![
MethodInfo { name: "length", signature: "() -> Number", doc: None }, MethodInfo { name: "length", signature: "() -> Number", doc: None },
MethodInfo { name: "len", signature: "() -> Number", doc: None }, MethodInfo { name: "len", signature: "() -> Number", doc: None },

View File

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

View File

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

View File

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

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

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

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

@@ -0,0 +1,749 @@
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),
Value::List(_) => self.dispatch_list_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 {
Ok(Value::List(Rc::new(vals)))
}
}
"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),
})
}
}
}
}
fn dispatch_list_method_inner(
&self,
obj_val: &Value,
method: &str,
args: &[Value],
) -> Result<Value, VMError> {
let list = match obj_val {
Value::List(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().cloned().unwrap_or(Value::Null)),
"last" => Ok(list.last().cloned().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).cloned().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()));
}
Ok(Value::Boolean(list.contains(&args[0])))
}
"indexOf" => {
if args.is_empty() {
return Err(VMError::RuntimeError("indexOf() requires an argument".to_string()));
}
let idx = list.iter().position(|item| item == &args[0]);
Ok(idx.map(|i| Value::Number(Decimal::from(i))).unwrap_or(Value::Number(Decimal::from(-1))))
}
"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::List(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::List(Rc::new(reversed)))
}
"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 strings: Vec<String> = list.iter().map(|v| format!("{}", v)).collect();
Ok(Value::String(SmolStr::new(strings.join(delim))))
}
"map" => {
// Property shorthand: list.map("fieldName") extracts a field from each Object element
if args.is_empty() {
return Err(VMError::RuntimeError("map() requires a field name argument".to_string()));
}
match &args[0] {
Value::String(field) => {
let mut values: Vec<Value> = Vec::with_capacity(list.len());
for item in list.iter() {
match item {
Value::Object(map) => {
values.push(map.get(field.as_str()).cloned().unwrap_or(Value::Null));
}
_ => {
return Err(VMError::RuntimeError(format!(
"map() requires all elements to be Objects, got {}",
item.type_name()
)));
}
}
}
// Smart return type: if all values are the same primitive type, return typed list
if values.is_empty() {
return Ok(Value::List(Rc::new(values)));
}
if values.iter().all(|v| matches!(v, Value::Number(_))) {
let nums: Vec<Decimal> = values
.into_iter()
.map(|v| match v {
Value::Number(n) => n,
_ => unreachable!(),
})
.collect();
Ok(Value::NumberList(Rc::new(nums)))
} else if values.iter().all(|v| matches!(v, Value::String(_))) {
let strings: Vec<SmolStr> = values
.into_iter()
.map(|v| match v {
Value::String(s) => s,
_ => unreachable!(),
})
.collect();
Ok(Value::StringList(Rc::new(strings)))
} else {
Ok(Value::List(Rc::new(values)))
}
}
_ => Err(VMError::RuntimeError("map() requires a string field name".to_string())),
}
}
"filter" => {
// Property shorthand:
// list.filter("active") — filter by truthy boolean field
// list.filter("field", value) — filter where field == value
if args.is_empty() {
return Err(VMError::RuntimeError("filter() requires a field name argument".to_string()));
}
match &args[0] {
Value::String(field) => {
let filtered: Result<Vec<Value>, VMError> = list
.iter()
.filter_map(|item| {
match item {
Value::Object(map) => {
let field_val = map.get(field.as_str()).cloned().unwrap_or(Value::Null);
if args.len() > 1 {
// filter("field", value) — equality check
if field_val == args[1] {
Some(Ok(item.clone()))
} else {
None
}
} else {
// filter("field") — truthy check
match &field_val {
Value::Boolean(b) => if *b { Some(Ok(item.clone())) } else { None },
Value::Null => None,
_ => Some(Ok(item.clone())),
}
}
}
_ => Some(Err(VMError::RuntimeError(format!(
"filter() requires all elements to be Objects, got {}",
item.type_name()
)))),
}
})
.collect();
Ok(Value::List(Rc::new(filtered?)))
}
_ => Err(VMError::RuntimeError("filter() requires a string field name".to_string())),
}
}
"find" => {
// Property shorthand: list.find("field", value) — first element where field == value
if args.is_empty() {
return Err(VMError::RuntimeError("find() requires a field name argument".to_string()));
}
match &args[0] {
Value::String(field) => {
for item in list.iter() {
match item {
Value::Object(map) => {
let field_val = map.get(field.as_str()).cloned().unwrap_or(Value::Null);
if args.len() > 1 {
if field_val == args[1] {
return Ok(item.clone());
}
} else {
// find("field") — first with truthy field
match &field_val {
Value::Boolean(b) => if *b { return Ok(item.clone()); },
Value::Null => {},
_ => return Ok(item.clone()),
}
}
}
_ => {
return Err(VMError::RuntimeError(format!(
"find() requires all elements to be Objects, got {}",
item.type_name()
)));
}
}
}
Ok(Value::Null)
}
_ => Err(VMError::RuntimeError("find() requires a string field name".to_string())),
}
}
"sort" => {
// Property shorthand: list.sort("field") — sort by field value
if args.is_empty() {
return Err(VMError::RuntimeError("sort() on List requires a field name argument".to_string()));
}
match &args[0] {
Value::String(field) => {
let mut sorted = list.to_vec();
sorted.sort_by(|a, b| {
let a_val = match a {
Value::Object(map) => map.get(field.as_str()).cloned().unwrap_or(Value::Null),
_ => Value::Null,
};
let b_val = match b {
Value::Object(map) => map.get(field.as_str()).cloned().unwrap_or(Value::Null),
_ => Value::Null,
};
match (&a_val, &b_val) {
(Value::Number(a), Value::Number(b)) => a.cmp(b),
(Value::String(a), Value::String(b)) => a.cmp(b),
_ => std::cmp::Ordering::Equal,
}
});
Ok(Value::List(Rc::new(sorted)))
}
_ => Err(VMError::RuntimeError("sort() requires a string field name".to_string())),
}
}
_ => {
let key = (SmolStr::new_static("List"), 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: "List",
method: SmolStr::from(method),
})
}
}
}
}
}

View File

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

View File

@@ -1,9 +1,10 @@
use crate::{ast::value::Value, bytecode::BytecodeReader, opcodes::OpCodeByte}; use crate::{ast::value::Value, bytecode::BytecodeReader, opcodes::OpCodeByte};
use bumpalo::Bump; use std::rc::Rc;
use micromap::Map; use micromap::Map;
use rust_decimal::{prelude::ToPrimitive, Decimal, MathematicalOps}; use rust_decimal::{Decimal, MathematicalOps};
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use smol_str::{SmolStr, StrExt}; use smallvec::SmallVec;
use smol_str::SmolStr;
/// Type alias for external (host) functions /// Type alias for external (host) functions
pub type ExternalFn = Box<dyn Fn(&[Value]) -> Result<Value, String>>; pub type ExternalFn = Box<dyn Fn(&[Value]) -> Result<Value, String>>;
@@ -33,36 +34,25 @@ macro_rules! log_debug {
/// Virtual Machine for executing dExpr bytecode /// Virtual Machine for executing dExpr bytecode
pub struct VM<'a> { pub struct VM<'a> {
bytecode: &'a [u8], // Bytecode to execute bytecode: &'a [u8],
reader: BytecodeReader<'a>, // Bytecode reader pub(super) reader: BytecodeReader<'a>,
pc: usize, // Program counter pc: usize,
// Registers for computation pub(super) registers: [Value; MAX_REGISTERS],
registers: [Value; MAX_REGISTERS],
// Global variables
globals: Map<SmolStr, Value, 64>, globals: Map<SmolStr, Value, 64>,
// Last expression result (returned by execute)
last_result: Value, last_result: Value,
// External (host) functions — lazily allocated pub(super) external_functions: Option<FxHashMap<SmolStr, ExternalFn>>,
external_functions: Option<FxHashMap<SmolStr, ExternalFn>>,
// External (host) methods per type — lazily allocated pub(super) external_methods: Option<FxHashMap<(SmolStr, SmolStr), ExternalMethod>>,
external_methods: Option<FxHashMap<(SmolStr, SmolStr), ExternalMethod>>,
// Heap for complex data types
heap: Bump,
// Debug info for error messages
debug_info: Option<&'a DebugInfo>, debug_info: Option<&'a DebugInfo>,
// Debug flag
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
debug: bool, pub(super) debug: bool,
// Profiling counts
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
opcode_counts: [usize; 256], opcode_counts: [usize; 256],
} }
@@ -79,7 +69,6 @@ impl<'a> VM<'a> {
last_result: Value::Null, last_result: Value::Null,
external_functions: None, external_functions: None,
external_methods: None, external_methods: None,
heap: Bump::new(),
debug_info: None, debug_info: None,
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
debug: false, debug: false,
@@ -153,7 +142,6 @@ impl<'a> VM<'a> {
self.registers = [const { Value::Null }; MAX_REGISTERS]; self.registers = [const { Value::Null }; MAX_REGISTERS];
self.last_result = Value::Null; self.last_result = Value::Null;
// Preserve globals // Preserve globals
self.heap = Bump::new();
} }
/// Execute the bytecode program and return the last expression result /// Execute the bytecode program and return the last expression result
@@ -195,7 +183,7 @@ impl<'a> VM<'a> {
OpCodeByte::StoreLocal => self.handle_store_local(), OpCodeByte::StoreLocal => self.handle_store_local(),
OpCodeByte::LoadGlobal => self.handle_load_global(), OpCodeByte::LoadGlobal => self.handle_load_global(),
OpCodeByte::StoreGlobal => self.handle_store_global(), OpCodeByte::StoreGlobal => self.handle_store_global(),
OpCodeByte::Add => self.binary_op(|a, b| Ok(a + b), "add"), OpCodeByte::Add => self.handle_add(),
OpCodeByte::Sub => self.binary_op(|a, b| Ok(a - b), "subtract"), OpCodeByte::Sub => self.binary_op(|a, b| Ok(a - b), "subtract"),
OpCodeByte::Mul => self.binary_op(|a, b| Ok(a * b), "multiply"), OpCodeByte::Mul => self.binary_op(|a, b| Ok(a * b), "multiply"),
OpCodeByte::Div => self.binary_op( OpCodeByte::Div => self.binary_op(
@@ -240,6 +228,11 @@ impl<'a> VM<'a> {
OpCodeByte::CallExternal => self.handle_call_external(), OpCodeByte::CallExternal => self.handle_call_external(),
OpCodeByte::CallDefault => self.handle_call_default(), OpCodeByte::CallDefault => self.handle_call_default(),
OpCodeByte::SetResult => self.handle_set_result(), OpCodeByte::SetResult => self.handle_set_result(),
OpCodeByte::ClearResult => {
self.last_result = Value::Null;
log_debug!(self, "ClearResult");
Ok(())
}
OpCodeByte::End => { OpCodeByte::End => {
log_debug!(self, "End of program"); log_debug!(self, "End of program");
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
@@ -406,12 +399,12 @@ impl<'a> VM<'a> {
#[inline] #[inline]
fn handle_load_global(&mut self) -> Result<(), VMError> { fn handle_load_global(&mut self) -> Result<(), VMError> {
let reg = self.read_register_checked()?; let reg = self.read_register_checked()?;
let name = self.reader.read_string().map_err(VMError::BytecodeError)?; let name = self.reader.read_str().map_err(VMError::BytecodeError)?;
let value = self let value = self
.globals .globals
.get(&name) .get(name)
.ok_or_else(|| VMError::UndefinedVariable(name.clone()))?; .ok_or_else(|| VMError::UndefinedVariable(SmolStr::from(name)))?;
self.registers[reg] = value.clone(); self.registers[reg] = value.clone();
log_debug!(self, log_debug!(self,
@@ -424,12 +417,12 @@ impl<'a> VM<'a> {
/// Handle StoreGlobal opcode - store register to global variable /// Handle StoreGlobal opcode - store register to global variable
#[inline] #[inline]
fn handle_store_global(&mut self) -> Result<(), VMError> { fn handle_store_global(&mut self) -> Result<(), VMError> {
let name = self.reader.read_string().map_err(VMError::BytecodeError)?; let name = self.reader.read_str().map_err(VMError::BytecodeError)?;
let reg = self.read_register_checked()?; let reg = self.read_register_checked()?;
self self
.globals .globals
.insert(name.clone(), self.registers[reg].clone()); .insert(SmolStr::from(name), self.registers[reg].clone());
log_debug!(self, log_debug!(self,
"StoreGlobal global.{} = r{} ({})", "StoreGlobal global.{} = r{} ({})",
@@ -464,6 +457,54 @@ impl<'a> VM<'a> {
Ok(()) Ok(())
} }
// ============================================================================
// Opcode Handlers - Add (Number + String coercion)
// ============================================================================
/// Handle Add opcode - number addition or string concatenation with auto-coercion
#[inline]
fn handle_add(&mut self) -> Result<(), VMError> {
let dest = self.read_register_checked()?;
let a = self.read_register_checked()?;
let b = self.read_register_checked()?;
match (&self.registers[a], &self.registers[b]) {
(Value::Number(a_num), Value::Number(b_num)) => {
self.registers[dest] = Value::Number(*a_num + *b_num);
}
(Value::String(a_str), Value::String(b_str)) => {
let mut result = String::with_capacity(a_str.len() + b_str.len());
result.push_str(a_str);
result.push_str(b_str);
self.registers[dest] = Value::String(result.into());
}
(Value::String(a_str), other) => {
let b_cow = value_to_string(other);
let mut result = String::with_capacity(a_str.len() + b_cow.len());
result.push_str(a_str);
result.push_str(&b_cow);
self.registers[dest] = Value::String(result.into());
}
(other, Value::String(b_str)) => {
let a_cow = value_to_string(other);
let mut result = String::with_capacity(a_cow.len() + b_str.len());
result.push_str(&a_cow);
result.push_str(b_str);
self.registers[dest] = Value::String(result.into());
}
(a_val, b_val) => {
return Err(VMError::InvalidOperation {
operation: "add",
left_type: a_val.type_name(),
right_type: b_val.type_name(),
});
}
}
log_debug!(self, "Add r{} = r{} + r{}", dest, a, b);
Ok(())
}
// ============================================================================ // ============================================================================
// Opcode Handlers - Boolean Operations // Opcode Handlers - Boolean Operations
// ============================================================================ // ============================================================================
@@ -556,6 +597,8 @@ impl<'a> VM<'a> {
} }
// String in Object (key check) // String in Object (key check)
(Value::String(key), Value::Object(map)) => map.contains_key(key), (Value::String(key), Value::Object(map)) => map.contains_key(key),
// Value in List
(needle_val, Value::List(list)) => list.contains(needle_val),
(needle_val, haystack_val) => { (needle_val, haystack_val) => {
return Err(VMError::InvalidOperation { return Err(VMError::InvalidOperation {
operation: "in", operation: "in",
@@ -614,26 +657,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,13 +679,49 @@ 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;
} }
Value::List(list) => {
// Property projection: list.field → extract field from each Object element
let mut values: Vec<Value> = Vec::with_capacity(list.len());
for item in list.iter() {
match item {
Value::Object(map) => {
values.push(map.get(prop).cloned().unwrap_or(Value::Null));
}
_ => {
return Err(VMError::RuntimeError(format!(
"Cannot access property '{}' on non-Object element in List (got {})",
prop,
item.type_name()
)));
}
}
}
// Smart return type: NumberList/StringList when homogeneous
if values.is_empty() {
self.registers[dest] = Value::List(Rc::new(values));
} else if values.iter().all(|v| matches!(v, Value::Number(_))) {
let nums: Vec<Decimal> = values.into_iter().map(|v| match v {
Value::Number(n) => n,
_ => unreachable!(),
}).collect();
self.registers[dest] = Value::NumberList(Rc::new(nums));
} else if values.iter().all(|v| matches!(v, Value::String(_))) {
let strings: Vec<SmolStr> = values.into_iter().map(|v| match v {
Value::String(s) => s,
_ => unreachable!(),
}).collect();
self.registers[dest] = Value::StringList(Rc::new(strings));
} else {
self.registers[dest] = Value::List(Rc::new(values));
}
}
other => { other => {
return Err(VMError::RuntimeError(format!( return Err(VMError::RuntimeError(format!(
"Cannot access property '{}' on type {}", "Cannot access property '{}' on type {}",
@@ -665,13 +737,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 +761,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 +787,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 +805,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 +939,14 @@ impl<'a> VM<'a> {
Ok(()) Ok(())
} }
} }
/// Convert a Value to its string representation for concatenation (no quotes around strings)
pub(super) fn value_to_string(val: &Value) -> std::borrow::Cow<'_, str> {
match val {
Value::String(s) => std::borrow::Cow::Borrowed(s.as_str()),
Value::Number(n) => std::borrow::Cow::Owned(n.to_string()),
Value::Boolean(b) => std::borrow::Cow::Borrowed(if *b { "true" } else { "false" }),
Value::Null => std::borrow::Cow::Borrowed("null"),
other => std::borrow::Cow::Owned(format!("{}", other)),
}
}

144
tests/data_driven_tests.rs Normal file
View File

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

View File

@@ -3,6 +3,7 @@ use indexmap::IndexMap;
use rust_decimal::prelude::ToPrimitive; use rust_decimal::prelude::ToPrimitive;
use rust_decimal_macros::dec; use rust_decimal_macros::dec;
use smol_str::SmolStr; use smol_str::SmolStr;
use std::rc::Rc;
/// Helper to run code and get the value of "result" variable /// Helper to run code and get the value of "result" variable
fn run_and_get_result(code: &str) -> Value { fn run_and_get_result(code: &str) -> Value {
@@ -628,7 +629,7 @@ fn test_numberlist_contains() {
let mut compiler = Compiler::new(); let mut compiler = Compiler::new();
let bytecode = compiler.compile(ast).expect("compile"); let bytecode = compiler.compile(ast).expect("compile");
let mut vm = VM::new(&bytecode); let mut vm = VM::new(&bytecode);
vm.set_global("nums", Value::NumberList(vec![dec!(1), dec!(2), dec!(3), dec!(4)])); vm.set_global("nums", Value::NumberList(Rc::new(vec![dec!(1), dec!(2), dec!(3), dec!(4)])));
vm.execute().expect("execute"); vm.execute().expect("execute");
assert_eq!(vm.get_global("result").unwrap(), &Value::Boolean(true)); assert_eq!(vm.get_global("result").unwrap(), &Value::Boolean(true));
} }
@@ -639,7 +640,7 @@ fn test_numberlist_indexof() {
let mut compiler = Compiler::new(); let mut compiler = Compiler::new();
let bytecode = compiler.compile(ast).expect("compile"); let bytecode = compiler.compile(ast).expect("compile");
let mut vm = VM::new(&bytecode); let mut vm = VM::new(&bytecode);
vm.set_global("nums", Value::NumberList(vec![dec!(1), dec!(2), dec!(3)])); vm.set_global("nums", Value::NumberList(Rc::new(vec![dec!(1), dec!(2), dec!(3)])));
vm.execute().expect("execute"); vm.execute().expect("execute");
assert_eq!(vm.get_global("result").unwrap(), &Value::Number(dec!(2))); assert_eq!(vm.get_global("result").unwrap(), &Value::Number(dec!(2)));
} }
@@ -650,7 +651,7 @@ fn test_numberlist_slice() {
let mut compiler = Compiler::new(); let mut compiler = Compiler::new();
let bytecode = compiler.compile(ast).expect("compile"); let bytecode = compiler.compile(ast).expect("compile");
let mut vm = VM::new(&bytecode); let mut vm = VM::new(&bytecode);
vm.set_global("nums", Value::NumberList(vec![dec!(10), dec!(20), dec!(30), dec!(40)])); vm.set_global("nums", Value::NumberList(Rc::new(vec![dec!(10), dec!(20), dec!(30), dec!(40)])));
vm.execute().expect("execute"); vm.execute().expect("execute");
// slice(1,3) = [20, 30], sum = 50 // slice(1,3) = [20, 30], sum = 50
assert_eq!(vm.get_global("result").unwrap(), &Value::Number(dec!(50))); assert_eq!(vm.get_global("result").unwrap(), &Value::Number(dec!(50)));
@@ -662,7 +663,7 @@ fn test_numberlist_reverse() {
let mut compiler = Compiler::new(); let mut compiler = Compiler::new();
let bytecode = compiler.compile(ast).expect("compile"); let bytecode = compiler.compile(ast).expect("compile");
let mut vm = VM::new(&bytecode); let mut vm = VM::new(&bytecode);
vm.set_global("nums", Value::NumberList(vec![dec!(1), dec!(2), dec!(3)])); vm.set_global("nums", Value::NumberList(Rc::new(vec![dec!(1), dec!(2), dec!(3)])));
vm.execute().expect("execute"); vm.execute().expect("execute");
assert_eq!(vm.get_global("result").unwrap(), &Value::Number(dec!(3))); assert_eq!(vm.get_global("result").unwrap(), &Value::Number(dec!(3)));
} }
@@ -673,7 +674,7 @@ fn test_numberlist_sort() {
let mut compiler = Compiler::new(); let mut compiler = Compiler::new();
let bytecode = compiler.compile(ast).expect("compile"); let bytecode = compiler.compile(ast).expect("compile");
let mut vm = VM::new(&bytecode); let mut vm = VM::new(&bytecode);
vm.set_global("nums", Value::NumberList(vec![dec!(30), dec!(10), dec!(20)])); vm.set_global("nums", Value::NumberList(Rc::new(vec![dec!(30), dec!(10), dec!(20)])));
vm.execute().expect("execute"); vm.execute().expect("execute");
assert_eq!(vm.get_global("result").unwrap(), &Value::Number(dec!(10))); assert_eq!(vm.get_global("result").unwrap(), &Value::Number(dec!(10)));
} }
@@ -684,7 +685,7 @@ fn test_numberlist_isempty() {
let mut compiler = Compiler::new(); let mut compiler = Compiler::new();
let bytecode = compiler.compile(ast).expect("compile"); let bytecode = compiler.compile(ast).expect("compile");
let mut vm = VM::new(&bytecode); let mut vm = VM::new(&bytecode);
vm.set_global("nums", Value::NumberList(vec![])); vm.set_global("nums", Value::NumberList(Rc::new(vec![])));
vm.execute().expect("execute"); vm.execute().expect("execute");
assert_eq!(vm.get_global("result").unwrap(), &Value::Boolean(true)); assert_eq!(vm.get_global("result").unwrap(), &Value::Boolean(true));
} }
@@ -959,27 +960,19 @@ result = x % y"#;
} }
#[test] #[test]
fn test_error_location_type_mismatch() { fn test_string_number_auto_coercion() {
// string + number now auto-coerces to string concatenation
let code = r#"x = "hello" let code = r#"x = "hello"
y = 5 y = 5
result = x + y"#; x + y"#;
let ast = parser::program(code).unwrap();
let mut compiler = Compiler::new(); let mut compiler = Compiler::new();
let (bytecode, debug_info) = compiler let bytecode = compiler.compile(ast).unwrap();
.compile_from_source(code)
.expect("Failed to compile");
let mut vm = VM::new(&bytecode); let mut vm = VM::new(&bytecode);
vm.set_debug_info(&debug_info); let result = vm.execute().unwrap();
let err = vm.execute().unwrap_err(); assert_eq!(result, Value::String("hello5".into()));
let err_msg = err.to_string();
assert!(
err_msg.contains("line 3"),
"Error should contain line 3, got: {}",
err_msg
);
} }
#[test] #[test]
@@ -1036,7 +1029,7 @@ fn make_object(entries: Vec<(&str, Value)>) -> Value {
for (k, v) in entries { for (k, v) in entries {
map.insert(SmolStr::new(k), v); map.insert(SmolStr::new(k), v);
} }
Value::Object(map) Value::Object(Rc::new(map))
} }
#[test] #[test]
@@ -1121,7 +1114,7 @@ fn test_object_method_keys() {
let result = run_expr_with_globals("person.keys()", vec![("person", obj)]); let result = run_expr_with_globals("person.keys()", vec![("person", obj)]);
assert_eq!( assert_eq!(
result, result,
Value::StringList(vec![SmolStr::new("name"), SmolStr::new("age")]) Value::StringList(Rc::new(vec![SmolStr::new("name"), SmolStr::new("age")]))
); );
} }
@@ -1185,7 +1178,7 @@ fn test_object_method_values_strings() {
let result = run_expr_with_globals("obj.values()", vec![("obj", obj)]); let result = run_expr_with_globals("obj.values()", vec![("obj", obj)]);
assert_eq!( assert_eq!(
result, result,
Value::StringList(vec![SmolStr::new("x"), SmolStr::new("y")]) Value::StringList(Rc::new(vec![SmolStr::new("x"), SmolStr::new("y")]))
); );
} }
@@ -1196,7 +1189,7 @@ fn test_object_method_values_numbers() {
("b", Value::Number(dec!(2))), ("b", Value::Number(dec!(2))),
]); ]);
let result = run_expr_with_globals("obj.values()", vec![("obj", obj)]); let result = run_expr_with_globals("obj.values()", vec![("obj", obj)]);
assert_eq!(result, Value::NumberList(vec![dec!(1), dec!(2)])); assert_eq!(result, Value::NumberList(Rc::new(vec![dec!(1), dec!(2)])));
} }
#[test] #[test]
@@ -1342,3 +1335,608 @@ fn test_from_json_full_workflow() {
let r = run_and_get_result_with_globals(code, vec![("customer", customer)]); let r = run_and_get_result_with_globals(code, vec![("customer", customer)]);
assert_eq!(r, Value::String("premium".into())); assert_eq!(r, Value::String("premium".into()));
} }
// ==================== LIST (ARRAY OF OBJECTS) TESTS ====================
/// Build the invoice items test data
fn kalemler() -> Value {
let items: Vec<Value> = vec![
make_kalem(1, "Web Uygulama Gelistirme", 1, "Adet", dec!(45000), dec!(45000)),
make_kalem(2, "Mobil Uygulama Gelistirme", 1, "Adet", dec!(35000), dec!(35000)),
make_kalem(3, "UI/UX Tasarim Hizmeti", 40, "Saat", dec!(750), dec!(30000)),
make_kalem(4, "Sunucu Bakim Sozlesmesi (Yillik)", 1, "Adet", dec!(12000), dec!(12000)),
make_kalem(5, "SSL Sertifikasi", 3, "Adet", dec!(500), dec!(1500)),
make_kalem(6, "Veritabani Yonetimi", 12, "Ay", dec!(2000), dec!(24000)),
make_kalem(7, "API Entegrasyon Hizmeti", 1, "Adet", dec!(18000), dec!(18000)),
make_kalem(8, "Bulut Altyapi Kurulumu", 1, "Adet", dec!(8000), dec!(8000)),
make_kalem(9, "Siber Guvenlik Danismanligi", 20, "Saat", dec!(900), dec!(18000)),
make_kalem(10, "E-posta Sunucu Yapilandirmasi", 1, "Adet", dec!(3500), dec!(3500)),
make_kalem(11, "Yedekleme Sistemi Kurulumu", 1, "Adet", dec!(5000), dec!(5000)),
make_kalem(12, "SEO Optimizasyonu", 1, "Adet", dec!(7500), dec!(7500)),
make_kalem(13, "Egitim ve Dokumantasyon", 8, "Saat", dec!(600), dec!(4800)),
make_kalem(14, "Performans Testi ve Raporlama", 1, "Adet", dec!(6000), dec!(6000)),
make_kalem(15, "Teknik Destek Paketi (6 Ay)", 1, "Adet", dec!(9000), dec!(9000)),
];
Value::List(Rc::new(items))
}
fn make_kalem(sira_no: i64, adi: &str, miktar: i64, birim: &str, birim_fiyat: rust_decimal::Decimal, tutar: rust_decimal::Decimal) -> Value {
let mut map = IndexMap::new();
map.insert(SmolStr::new("siraNo"), Value::Number(rust_decimal::Decimal::from(sira_no)));
map.insert(SmolStr::new("adi"), Value::String(SmolStr::new(adi)));
map.insert(SmolStr::new("miktar"), Value::Number(rust_decimal::Decimal::from(miktar)));
map.insert(SmolStr::new("birim"), Value::String(SmolStr::new(birim)));
map.insert(SmolStr::new("birimFiyat"), Value::Number(birim_fiyat));
map.insert(SmolStr::new("tutar"), Value::Number(tutar));
Value::Object(Rc::new(map))
}
// --- List: length / len ---
#[test]
fn test_list_length() {
let result = run_expr_with_globals("kalemler.length()", vec![("kalemler", kalemler())]);
assert_eq!(result, Value::Number(dec!(15)));
}
#[test]
fn test_list_len() {
let result = run_expr_with_globals("kalemler.len()", vec![("kalemler", kalemler())]);
assert_eq!(result, Value::Number(dec!(15)));
}
// --- List: isEmpty ---
#[test]
fn test_list_is_empty_false() {
let result = run_expr_with_globals("kalemler.isEmpty()", vec![("kalemler", kalemler())]);
assert_eq!(result, Value::Boolean(false));
}
#[test]
fn test_list_is_empty_true() {
let empty = Value::List(Rc::new(vec![]));
let result = run_expr_with_globals("items.isEmpty()", vec![("items", empty)]);
assert_eq!(result, Value::Boolean(true));
}
// --- List: first / last ---
#[test]
fn test_list_first() {
let result = run_expr_with_globals("kalemler.first().adi", vec![("kalemler", kalemler())]);
assert_eq!(result, Value::String("Web Uygulama Gelistirme".into()));
}
#[test]
fn test_list_last() {
let result = run_expr_with_globals("kalemler.last().adi", vec![("kalemler", kalemler())]);
assert_eq!(result, Value::String("Teknik Destek Paketi (6 Ay)".into()));
}
#[test]
fn test_list_first_empty() {
let empty = Value::List(Rc::new(vec![]));
let result = run_expr_with_globals("items.first()", vec![("items", empty)]);
assert_eq!(result, Value::Null);
}
#[test]
fn test_list_last_empty() {
let empty = Value::List(Rc::new(vec![]));
let result = run_expr_with_globals("items.last()", vec![("items", empty)]);
assert_eq!(result, Value::Null);
}
// --- List: get ---
#[test]
fn test_list_get() {
let result = run_expr_with_globals("kalemler.get(2).adi", vec![("kalemler", kalemler())]);
assert_eq!(result, Value::String("UI/UX Tasarim Hizmeti".into()));
}
#[test]
fn test_list_get_out_of_bounds() {
let result = run_expr_with_globals("kalemler.get(100)", vec![("kalemler", kalemler())]);
assert_eq!(result, Value::Null);
}
// --- List: contains ---
#[test]
fn test_list_contains() {
let items = Value::List(Rc::new(vec![
Value::Number(dec!(1)),
Value::Number(dec!(2)),
Value::Number(dec!(3)),
]));
let result = run_expr_with_globals("items.contains(2)", vec![("items", items)]);
assert_eq!(result, Value::Boolean(true));
}
#[test]
fn test_list_contains_false() {
let items = Value::List(Rc::new(vec![
Value::Number(dec!(1)),
Value::Number(dec!(2)),
]));
let result = run_expr_with_globals("items.contains(5)", vec![("items", items)]);
assert_eq!(result, Value::Boolean(false));
}
// --- List: indexOf ---
#[test]
fn test_list_index_of() {
let items = Value::List(Rc::new(vec![
Value::String("a".into()),
Value::String("b".into()),
Value::String("c".into()),
]));
let result = run_expr_with_globals(r#"items.indexOf("b")"#, vec![("items", items)]);
assert_eq!(result, Value::Number(dec!(1)));
}
#[test]
fn test_list_index_of_not_found() {
let items = Value::List(Rc::new(vec![
Value::String("a".into()),
]));
let result = run_expr_with_globals(r#"items.indexOf("z")"#, vec![("items", items)]);
assert_eq!(result, Value::Number(dec!(-1)));
}
// --- List: slice ---
#[test]
fn test_list_slice() {
let result = run_expr_with_globals("kalemler.slice(0, 3).length()", vec![("kalemler", kalemler())]);
assert_eq!(result, Value::Number(dec!(3)));
}
#[test]
fn test_list_slice_to_end() {
let result = run_expr_with_globals("kalemler.slice(13).length()", vec![("kalemler", kalemler())]);
assert_eq!(result, Value::Number(dec!(2)));
}
// --- List: reverse ---
#[test]
fn test_list_reverse() {
let result = run_expr_with_globals("kalemler.reverse().first().adi", vec![("kalemler", kalemler())]);
assert_eq!(result, Value::String("Teknik Destek Paketi (6 Ay)".into()));
}
// --- List: join ---
#[test]
fn test_list_join() {
let items = Value::List(Rc::new(vec![
Value::Number(dec!(1)),
Value::Number(dec!(2)),
Value::Number(dec!(3)),
]));
let result = run_expr_with_globals(r#"items.join(", ")"#, vec![("items", items)]);
assert_eq!(result, Value::String("1, 2, 3".into()));
}
// --- List: map (property shorthand) ---
#[test]
fn test_list_map_number_field() {
// map("tutar") should return NumberList when all values are Number
let result = run_expr_with_globals(r#"kalemler.map("tutar").sum()"#, vec![("kalemler", kalemler())]);
// 45000+35000+30000+12000+1500+24000+18000+8000+18000+3500+5000+7500+4800+6000+9000 = 227300
assert_eq!(result, Value::Number(dec!(227300)));
}
#[test]
fn test_list_map_string_field() {
// map("birim") should return StringList when all values are String
let result = run_expr_with_globals(r#"kalemler.map("birim").contains("Saat")"#, vec![("kalemler", kalemler())]);
assert_eq!(result, Value::Boolean(true));
}
#[test]
fn test_list_map_string_field_join() {
let items = Value::List(Rc::new(vec![
make_kalem(1, "A", 1, "X", dec!(10), dec!(10)),
make_kalem(2, "B", 1, "Y", dec!(20), dec!(20)),
]));
let result = run_expr_with_globals(r#"items.map("adi").join(", ")"#, vec![("items", items)]);
assert_eq!(result, Value::String("A, B".into()));
}
// --- The main use case: kalemler.map("tutar").sum() * kdvOrani ---
#[test]
fn test_list_map_sum_multiply() {
let code = r#"kalemler.map("tutar").sum() * kdvOrani"#;
let result = run_expr_with_globals(code, vec![
("kalemler", kalemler()),
("kdvOrani", Value::Number(dec!(0.20))),
]);
// 227300 * 0.20 = 45460
assert_eq!(result, Value::Number(dec!(45460.00)));
}
#[test]
fn test_list_map_avg() {
let result = run_expr_with_globals(r#"kalemler.map("tutar").avg()"#, vec![("kalemler", kalemler())]);
// 227300 / 15 = 15153.333...
let avg = result.clone();
match avg {
Value::Number(n) => {
let rounded = n.round_dp(2);
assert_eq!(rounded, dec!(15153.33));
}
_ => panic!("Expected Number, got {:?}", avg),
}
}
#[test]
fn test_list_map_min() {
let result = run_expr_with_globals(r#"kalemler.map("tutar").min()"#, vec![("kalemler", kalemler())]);
assert_eq!(result, Value::Number(dec!(1500)));
}
#[test]
fn test_list_map_max() {
let result = run_expr_with_globals(r#"kalemler.map("tutar").max()"#, vec![("kalemler", kalemler())]);
assert_eq!(result, Value::Number(dec!(45000)));
}
// --- List: filter ---
#[test]
fn test_list_filter_by_value() {
let result = run_expr_with_globals(
r#"kalemler.filter("birim", "Saat").length()"#,
vec![("kalemler", kalemler())],
);
// Items 3, 9, 13 have birim="Saat"
assert_eq!(result, Value::Number(dec!(3)));
}
#[test]
fn test_list_filter_by_value_sum() {
let result = run_expr_with_globals(
r#"kalemler.filter("birim", "Saat").map("tutar").sum()"#,
vec![("kalemler", kalemler())],
);
// 30000 + 18000 + 4800 = 52800
assert_eq!(result, Value::Number(dec!(52800)));
}
#[test]
fn test_list_filter_by_boolean_field() {
let items = Value::List(Rc::new(vec![
{
let mut m = IndexMap::new();
m.insert(SmolStr::new("name"), Value::String("A".into()));
m.insert(SmolStr::new("active"), Value::Boolean(true));
Value::Object(Rc::new(m))
},
{
let mut m = IndexMap::new();
m.insert(SmolStr::new("name"), Value::String("B".into()));
m.insert(SmolStr::new("active"), Value::Boolean(false));
Value::Object(Rc::new(m))
},
{
let mut m = IndexMap::new();
m.insert(SmolStr::new("name"), Value::String("C".into()));
m.insert(SmolStr::new("active"), Value::Boolean(true));
Value::Object(Rc::new(m))
},
]));
let result = run_expr_with_globals(r#"items.filter("active").length()"#, vec![("items", items)]);
assert_eq!(result, Value::Number(dec!(2)));
}
// --- List: find ---
#[test]
fn test_list_find_by_value() {
let result = run_expr_with_globals(
r#"kalemler.find("siraNo", 5).adi"#,
vec![("kalemler", kalemler())],
);
assert_eq!(result, Value::String("SSL Sertifikasi".into()));
}
#[test]
fn test_list_find_not_found() {
let result = run_expr_with_globals(
r#"kalemler.find("siraNo", 99)"#,
vec![("kalemler", kalemler())],
);
assert_eq!(result, Value::Null);
}
// --- List: sort ---
#[test]
fn test_list_sort_by_field() {
let result = run_expr_with_globals(
r#"kalemler.sort("tutar").first().adi"#,
vec![("kalemler", kalemler())],
);
// Smallest tutar is 1500 (SSL Sertifikasi)
assert_eq!(result, Value::String("SSL Sertifikasi".into()));
}
#[test]
fn test_list_sort_by_field_last() {
let result = run_expr_with_globals(
r#"kalemler.sort("tutar").last().adi"#,
vec![("kalemler", kalemler())],
);
// Largest tutar is 45000 (Web Uygulama Gelistirme)
assert_eq!(result, Value::String("Web Uygulama Gelistirme".into()));
}
// --- List: in operator ---
#[test]
fn test_list_in_operator() {
let items = Value::List(Rc::new(vec![
Value::String("a".into()),
Value::String("b".into()),
Value::String("c".into()),
]));
let result = run_expr_with_globals(r#""b" in items"#, vec![("items", items)]);
assert_eq!(result, Value::Boolean(true));
}
#[test]
fn test_list_in_operator_false() {
let items = Value::List(Rc::new(vec![
Value::Number(dec!(1)),
Value::Number(dec!(2)),
]));
let result = run_expr_with_globals("5 in items", vec![("items", items)]);
assert_eq!(result, Value::Boolean(false));
}
// --- List: len() builtin function ---
#[test]
fn test_list_builtin_len() {
let result = run_expr_with_globals("len(kalemler)", vec![("kalemler", kalemler())]);
assert_eq!(result, Value::Number(dec!(15)));
}
// --- List: from_json ---
#[test]
fn test_list_from_json() {
let json = r#"[{"a": 1}, {"a": 2}]"#;
let val = Value::from_json(json).unwrap();
match &val {
Value::List(list) => {
assert_eq!(list.len(), 2);
match &list[0] {
Value::Object(map) => assert_eq!(map.get("a"), Some(&Value::Number(dec!(1)))),
other => panic!("Expected Object, got {:?}", other),
}
}
other => panic!("Expected List, got {:?}", other),
}
}
#[test]
fn test_list_from_json_mixed() {
// Mixed types in array → List
let json = r#"[1, "hello", true]"#;
let val = Value::from_json(json).unwrap();
match &val {
Value::List(list) => {
assert_eq!(list.len(), 3);
assert_eq!(list[0], Value::Number(dec!(1)));
assert_eq!(list[1], Value::String("hello".into()));
assert_eq!(list[2], Value::Boolean(true));
}
other => panic!("Expected List, got {:?}", other),
}
}
// --- List: serialization round-trip ---
#[test]
fn test_list_serialize_deserialize() {
let original = Value::List(Rc::new(vec![
Value::Number(dec!(42)),
Value::String("hello".into()),
Value::Boolean(true),
]));
let bytes = original.serialize();
let (deserialized, _) = Value::deserialize(&bytes).unwrap();
assert_eq!(original, deserialized);
}
#[test]
fn test_list_serialize_deserialize_objects() {
let original = kalemler();
let bytes = original.serialize();
let (deserialized, _) = Value::deserialize(&bytes).unwrap();
assert_eq!(original, deserialized);
}
// --- List: chained operations ---
#[test]
fn test_list_chain_filter_map_sum() {
// Filter only "Adet" items, then sum their tutars
let code = r#"kalemler.filter("birim", "Adet").map("tutar").sum()"#;
let result = run_expr_with_globals(code, vec![("kalemler", kalemler())]);
// Adet items: 45000+35000+12000+1500+18000+8000+3500+5000+7500+6000+9000 = 150500
assert_eq!(result, Value::Number(dec!(150500)));
}
#[test]
fn test_list_chain_filter_length() {
let code = r#"kalemler.filter("birim", "Ay").length()"#;
let result = run_expr_with_globals(code, vec![("kalemler", kalemler())]);
// Only item 6 (Veritabani Yonetimi) has birim="Ay"
assert_eq!(result, Value::Number(dec!(1)));
}
#[test]
fn test_list_chain_sort_slice_map() {
// Sort by tutar, take top 3, get names
let code = r#"kalemler.sort("tutar").reverse().slice(0, 3).map("adi").join(", ")"#;
let result = run_expr_with_globals(code, vec![("kalemler", kalemler())]);
assert_eq!(result, Value::String("Web Uygulama Gelistirme, Mobil Uygulama Gelistirme, UI/UX Tasarim Hizmeti".into()));
}
// --- Object values() now returns List for mixed types ---
#[test]
fn test_object_values_mixed_returns_list() {
let mut map = IndexMap::new();
map.insert(SmolStr::new("name"), Value::String("Alice".into()));
map.insert(SmolStr::new("age"), Value::Number(dec!(30)));
let obj = Value::Object(Rc::new(map));
let result = run_expr_with_globals("item.values().length()", vec![("item", obj)]);
assert_eq!(result, Value::Number(dec!(2)));
}
// --- List: Display ---
#[test]
fn test_list_display() {
let list = Value::List(Rc::new(vec![
Value::Number(dec!(1)),
Value::String("hello".into()),
Value::Boolean(true),
]));
assert_eq!(format!("{}", list), r#"[1, "hello", true]"#);
}
// --- List: complex invoice scenario ---
#[test]
fn test_invoice_total_with_kdv() {
let code = r#"
araToplam = kalemler.map("tutar").sum()
kdv = araToplam * kdvOrani
result = araToplam + kdv
"#;
let result = run_and_get_result_with_globals(code, vec![
("kalemler", kalemler()),
("kdvOrani", Value::Number(dec!(0.20))),
]);
// 227300 + 45460 = 272760
assert_eq!(result, Value::Number(dec!(272760.00)));
}
#[test]
fn test_invoice_item_count_by_birim() {
let code = r#"
adetSayisi = kalemler.filter("birim", "Adet").length()
saatSayisi = kalemler.filter("birim", "Saat").length()
result = adetSayisi + saatSayisi
"#;
let result = run_and_get_result_with_globals(code, vec![("kalemler", kalemler())]);
// 11 Adet + 3 Saat = 14
assert_eq!(result, Value::Number(dec!(14)));
}
#[test]
fn test_invoice_find_and_access_property() {
let code = r#"kalemler.find("adi", "SSL Sertifikasi").birimFiyat"#;
let result = run_expr_with_globals(code, vec![("kalemler", kalemler())]);
assert_eq!(result, Value::Number(dec!(500)));
}
// ==================== LIST PROPERTY PROJECTION TESTS ====================
#[test]
fn test_list_property_projection_number() {
// kalemler.tutar → NumberList
let result = run_expr_with_globals("kalemler.tutar.sum()", vec![("kalemler", kalemler())]);
assert_eq!(result, Value::Number(dec!(227300)));
}
#[test]
fn test_list_property_projection_string() {
// kalemler.adi → StringList
let result = run_expr_with_globals(r#"kalemler.birim.contains("Saat")"#, vec![("kalemler", kalemler())]);
assert_eq!(result, Value::Boolean(true));
}
#[test]
fn test_list_property_projection_sum_multiply() {
// The main use case: kalemler.tutar.sum() * kdvOrani
let code = "kalemler.tutar.sum() * kdvOrani";
let result = run_expr_with_globals(code, vec![
("kalemler", kalemler()),
("kdvOrani", Value::Number(dec!(0.20))),
]);
assert_eq!(result, Value::Number(dec!(45460.00)));
}
#[test]
fn test_list_property_projection_min_max() {
let result = run_expr_with_globals("kalemler.tutar.min()", vec![("kalemler", kalemler())]);
assert_eq!(result, Value::Number(dec!(1500)));
let result = run_expr_with_globals("kalemler.tutar.max()", vec![("kalemler", kalemler())]);
assert_eq!(result, Value::Number(dec!(45000)));
}
#[test]
fn test_list_property_projection_join() {
let items = Value::List(Rc::new(vec![
make_kalem(1, "A", 1, "X", dec!(10), dec!(10)),
make_kalem(2, "B", 1, "Y", dec!(20), dec!(20)),
]));
let result = run_expr_with_globals(r#"items.adi.join(", ")"#, vec![("items", items)]);
assert_eq!(result, Value::String("A, B".into()));
}
#[test]
fn test_list_property_projection_chained_with_filter() {
// filter → projection: kalemler.filter("birim", "Saat").tutar.sum()
let code = r#"kalemler.filter("birim", "Saat").tutar.sum()"#;
let result = run_expr_with_globals(code, vec![("kalemler", kalemler())]);
// Saat items: 30000 + 18000 + 4800 = 52800
assert_eq!(result, Value::Number(dec!(52800)));
}
#[test]
fn test_list_property_projection_empty() {
let empty = Value::List(Rc::new(vec![]));
let result = run_expr_with_globals("items.tutar", vec![("items", empty)]);
match result {
Value::List(l) => assert!(l.is_empty()),
_ => panic!("Expected empty List, got {:?}", result),
}
}
#[test]
fn test_list_property_projection_with_sort() {
// sort → projection
let code = r#"kalemler.sort("tutar").adi.first()"#;
let result = run_expr_with_globals(code, vec![("kalemler", kalemler())]);
assert_eq!(result, Value::String("SSL Sertifikasi".into()));
}
#[test]
fn test_invoice_full_scenario() {
let code = r#"
araToplam = kalemler.tutar.sum()
kdv = araToplam * kdvOrani
genelToplam = araToplam + kdv
saatlikHizmetler = kalemler.filter("birim", "Saat").tutar.sum()
result = genelToplam
"#;
let result = run_and_get_result_with_globals(code, vec![
("kalemler", kalemler()),
("kdvOrani", Value::Number(dec!(0.20))),
]);
assert_eq!(result, Value::Number(dec!(272760.00)));
}

1288
tests/test_cases.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "dexpr-wasm" name = "dexpr-wasm"
version = "0.1.0" version = "0.3.0"
edition = "2021" edition = "2021"
[lib] [lib]

View File

@@ -37,6 +37,9 @@ fn value_to_json(val: &Value) -> serde_json::Value {
.collect(); .collect();
serde_json::Value::Object(obj) serde_json::Value::Object(obj)
} }
Value::List(list) => {
serde_json::Value::Array(list.iter().map(|v| value_to_json(v)).collect())
}
} }
} }