mirror of
https://github.com/duhanbalci/dexpr.git
synced 2026-07-02 00:29:15 +00:00
Compare commits
4 Commits
75ab9bec9f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 0fa388abeb | |||
| 953b39d433 | |||
| b0ea71e104 | |||
| 7582c5aee7 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1 +1,3 @@
|
||||
/target
|
||||
editor/dist
|
||||
editor/node_modules
|
||||
|
||||
@@ -72,6 +72,7 @@ The dexpr language supports:
|
||||
- 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.)
|
||||
- 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
|
||||
|
||||
|
||||
3
Cargo.lock
generated
3
Cargo.lock
generated
@@ -304,7 +304,7 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
|
||||
|
||||
[[package]]
|
||||
name = "dexpr"
|
||||
version = "0.1.0"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"criterion",
|
||||
@@ -315,6 +315,7 @@ dependencies = [
|
||||
"rust_decimal",
|
||||
"rust_decimal_macros",
|
||||
"rustc-hash",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"smallvec",
|
||||
"smol_str",
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
[package]
|
||||
name = "dexpr"
|
||||
version = "0.1.0"
|
||||
version = "0.3.0"
|
||||
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
|
||||
|
||||
@@ -35,6 +38,7 @@ serde_json = "1"
|
||||
|
||||
[dev-dependencies]
|
||||
criterion = { version = "0.8.2", features = ["html_reports"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
[[bench]]
|
||||
name = "my_benchmark"
|
||||
|
||||
@@ -1,21 +1,39 @@
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
use dexpr::{ast::value::Value, compiler::Compiler, parser, vm::VM};
|
||||
use indexmap::IndexMap;
|
||||
use rust_decimal::Decimal;
|
||||
use rust_decimal_macros::dec;
|
||||
use smol_str::SmolStr;
|
||||
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) {
|
||||
// 1. Parser Benchmark
|
||||
// ── Existing benchmarks ──────────────────────────────────────────────
|
||||
|
||||
c.bench_function("parser_long", |b| {
|
||||
let input = include_str!("../src/bench_long.dexpr");
|
||||
let input = include_str!("../examples/bench_long.dexpr");
|
||||
b.iter(|| {
|
||||
let _ = parser::program(input).unwrap();
|
||||
})
|
||||
});
|
||||
|
||||
// 2. Compiler Benchmark
|
||||
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();
|
||||
b.iter(|| {
|
||||
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| {
|
||||
let input = include_str!("../src/basic_long.dexpr");
|
||||
let input = include_str!("../examples/basic_long.dexpr");
|
||||
let ast = parser::program(input).unwrap();
|
||||
let mut compiler = Compiler::new();
|
||||
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| {
|
||||
let input = include_str!("../src/bench_long.dexpr");
|
||||
let input = include_str!("../examples/bench_long.dexpr");
|
||||
let ast = parser::program(input).unwrap();
|
||||
let mut compiler = Compiler::new();
|
||||
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| {
|
||||
let input = "5.12 + test * 1.5";
|
||||
let ast = parser::program(input).unwrap();
|
||||
let mut compiler = Compiler::new();
|
||||
let bytecode = compiler.compile(ast).unwrap();
|
||||
let bytecode = compile(input);
|
||||
let test_val = dec!(100);
|
||||
b.iter(|| {
|
||||
let mut vm = VM::new(&bytecode);
|
||||
@@ -63,6 +74,220 @@ pub fn criterion_benchmark(c: &mut Criterion) {
|
||||
let _ = vm.execute().unwrap();
|
||||
})
|
||||
});
|
||||
|
||||
// ── #1: Method dispatch clone overhead ───────────────────────────────
|
||||
|
||||
// Object method — clone entire IndexMap per call
|
||||
c.bench_function("vm_object_method_keys", |b| {
|
||||
let bytecode = compile("obj.keys()");
|
||||
let obj = sample_object(20);
|
||||
b.iter(|| {
|
||||
let mut vm = VM::new(&bytecode);
|
||||
vm.set_global("obj", obj.clone());
|
||||
let _ = vm.execute().unwrap();
|
||||
})
|
||||
});
|
||||
|
||||
c.bench_function("vm_object_method_length", |b| {
|
||||
let bytecode = compile("obj.length()");
|
||||
let obj = sample_object(20);
|
||||
b.iter(|| {
|
||||
let mut vm = VM::new(&bytecode);
|
||||
vm.set_global("obj", obj.clone());
|
||||
let _ = vm.execute().unwrap();
|
||||
})
|
||||
});
|
||||
|
||||
c.bench_function("vm_object_method_contains", |b| {
|
||||
let bytecode = compile(r#"obj.contains("field10")"#);
|
||||
let obj = sample_object(20);
|
||||
b.iter(|| {
|
||||
let mut vm = VM::new(&bytecode);
|
||||
vm.set_global("obj", obj.clone());
|
||||
let _ = vm.execute().unwrap();
|
||||
})
|
||||
});
|
||||
|
||||
// StringList method — clone entire Vec per call
|
||||
c.bench_function("vm_strlist_method_length", |b| {
|
||||
let bytecode = compile("items.length()");
|
||||
let items = Value::StringList(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);
|
||||
|
||||
@@ -88,12 +88,13 @@ struct Spanned<T> { node: T, span: Span }
|
||||
| `NumberList(Vec<Decimal>)` | `Vec<Decimal>` | Sayı listesi |
|
||||
| `StringList(Vec<SmolStr>)` | `Vec<SmolStr>` | String listesi |
|
||||
| `Object(IndexMap<SmolStr, Value>)` | `IndexMap<SmolStr, Value>` | Anahtar-değer nesnesi |
|
||||
| `List(Vec<Value>)` | `Vec<Value>` | Genel liste (object array dahil) |
|
||||
|
||||
### Serileştirme
|
||||
|
||||
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:**
|
||||
- Number: 16 byte (Decimal serialization)
|
||||
- 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
|
||||
- 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)
|
||||
- List: 2-byte count + her eleman için rekürsif serialize
|
||||
|
||||
`serialize()` ve `deserialize()` metodları bu dönüşümü gerçekleştirir.
|
||||
|
||||
|
||||
@@ -96,8 +96,9 @@ Parse ile birlikte pozisyon bilgisi de toplar ve `DebugInfo` üretir.
|
||||
2. Sağ operandı register'a derle
|
||||
3. Sonuç register'ı ayır
|
||||
4. Uygun opcode'u emit et (Add, Sub, Mul, vs.)
|
||||
5. **Özel durum:** String + String → `Concat` kullanılır
|
||||
6. Operand register'ları serbest bırak
|
||||
5. 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)
|
||||
1. Operandı register'a derle
|
||||
|
||||
@@ -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ı |
|
||||
| `items.` (config'de `StringList`) | StringList 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 |
|
||||
| `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 |
|
||||
|-----|----------|
|
||||
| `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ı |
|
||||
| `MethodInfo` | Metod metadata'sı |
|
||||
| `VariableInfo` | Değişken metadata'sı |
|
||||
|
||||
@@ -29,7 +29,7 @@ Editör entegrasyonu için dil metadata'sı üretir. Built-in fonksiyonlar, tipe
|
||||
| Alan | Tip | Açıklama |
|
||||
|------|-----|----------|
|
||||
| `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 |
|
||||
|
||||
### 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` |
|
||||
| `StringList` | `length`, `len`, `isEmpty`, `first`, `last`, `get`, `contains`, `indexOf`, `slice`, `reverse`, `sort`, `join` |
|
||||
| `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)`
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
`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.
|
||||
|
||||
26
docs/vm.md
26
docs/vm.md
@@ -11,7 +11,9 @@ Register tabanlı sanal makine. Bytecode'u çalıştırır, 8 register ve global
|
||||
| Dosya | İçerik |
|
||||
|-------|--------|
|
||||
| `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/debug_info.rs` | Bytecode offset → kaynak konum eşleştirme |
|
||||
|
||||
@@ -120,6 +122,7 @@ struct VM<'a> {
|
||||
### Aritmetik
|
||||
- **`binary_op(f, name)`** — Genel handler: iki operand register'ı oku, fonksiyonu uygula, sonucu kaydet
|
||||
- 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
|
||||
|
||||
### Karşılaştırma
|
||||
@@ -133,18 +136,19 @@ struct VM<'a> {
|
||||
- **`handle_jump_if_false()`** — Register `Boolean(false)` ise atla
|
||||
|
||||
### String, Nesne ve Metodlar
|
||||
- **`handle_concat()`** — İki String register'ını birleştir
|
||||
- **`handle_get_property()`** — Object register'ından alan oku, alan yoksa `Null` döndür
|
||||
- **`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. 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_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?)`
|
||||
- **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`
|
||||
- **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
|
||||
|
||||
### Ü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ç
|
||||
- **`handle_call_external()`** — İsimle harici fonksiyon çağır (HashMap lookup)
|
||||
@@ -153,6 +157,18 @@ struct VM<'a> {
|
||||
### Built-in
|
||||
- **`handle_log()`** — Register değerini stdout'a yazdır
|
||||
- **`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"`
|
||||
|
||||
---
|
||||
|
||||
|
||||
134
editor/bun.lock
134
editor/bun.lock
@@ -15,6 +15,7 @@
|
||||
"codemirror": "^6.0.2",
|
||||
"tsup": "^8.0.0",
|
||||
"typescript": "^5.0.0",
|
||||
"vitest": "^4.1.3",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
@@ -22,6 +23,7 @@
|
||||
"@codemirror/state": "^6.0.0",
|
||||
"@codemirror/view": "^6.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=="],
|
||||
|
||||
"@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/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=="],
|
||||
|
||||
"@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-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=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||
|
||||
"crelt": ["crelt@1.0.6", "", {}, "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"obug": ["obug@2.1.1", "", {}, "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ=="],
|
||||
|
||||
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"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
15
editor/dist/index.cjs
vendored
@@ -285,6 +285,15 @@ function inferMethodReturnType(method) {
|
||||
// depends on input type
|
||||
case "join":
|
||||
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:
|
||||
return null;
|
||||
}
|
||||
@@ -318,7 +327,7 @@ function dexprCompletion(info) {
|
||||
}
|
||||
const objectFieldCompletions = /* @__PURE__ */ new Map();
|
||||
for (const v of info.variables ?? []) {
|
||||
if (v.type === "Object" && v.fields) {
|
||||
if ((v.type === "Object" || v.type === "List") && v.fields) {
|
||||
const fieldItems = [];
|
||||
for (const f of v.fields) {
|
||||
configVarTypes.set(`${v.name}.${f.name}`, f.type);
|
||||
@@ -392,6 +401,10 @@ function dexprCompletion(info) {
|
||||
const fieldItems = objectFieldCompletions.get(rootVarName) ?? [];
|
||||
const objMethods = methodsByType["Object"] ?? [];
|
||||
options = [...fieldItems, ...objMethods];
|
||||
} else if (finalType === "List") {
|
||||
const rootVarName = path[0];
|
||||
const listMethods = methodsByType["List"] ?? [];
|
||||
options = [...listMethods];
|
||||
} else if (finalType) {
|
||||
options = methodsByType[finalType] ?? allMethods;
|
||||
} else {
|
||||
|
||||
2
editor/dist/index.d.cts
vendored
2
editor/dist/index.d.cts
vendored
@@ -2,7 +2,7 @@ import { Extension } from '@codemirror/state';
|
||||
import { Completion } from '@codemirror/autocomplete';
|
||||
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 {
|
||||
name: string;
|
||||
signature: string;
|
||||
|
||||
2
editor/dist/index.d.ts
vendored
2
editor/dist/index.d.ts
vendored
@@ -2,7 +2,7 @@ import { Extension } from '@codemirror/state';
|
||||
import { Completion } from '@codemirror/autocomplete';
|
||||
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 {
|
||||
name: string;
|
||||
signature: string;
|
||||
|
||||
15
editor/dist/index.js
vendored
15
editor/dist/index.js
vendored
@@ -258,6 +258,15 @@ function inferMethodReturnType(method) {
|
||||
// depends on input type
|
||||
case "join":
|
||||
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:
|
||||
return null;
|
||||
}
|
||||
@@ -291,7 +300,7 @@ function dexprCompletion(info) {
|
||||
}
|
||||
const objectFieldCompletions = /* @__PURE__ */ new Map();
|
||||
for (const v of info.variables ?? []) {
|
||||
if (v.type === "Object" && v.fields) {
|
||||
if ((v.type === "Object" || v.type === "List") && v.fields) {
|
||||
const fieldItems = [];
|
||||
for (const f of v.fields) {
|
||||
configVarTypes.set(`${v.name}.${f.name}`, f.type);
|
||||
@@ -365,6 +374,10 @@ function dexprCompletion(info) {
|
||||
const fieldItems = objectFieldCompletions.get(rootVarName) ?? [];
|
||||
const objMethods = methodsByType["Object"] ?? [];
|
||||
options = [...fieldItems, ...objMethods];
|
||||
} else if (finalType === "List") {
|
||||
const rootVarName = path[0];
|
||||
const listMethods = methodsByType["List"] ?? [];
|
||||
options = [...listMethods];
|
||||
} else if (finalType) {
|
||||
options = methodsByType[finalType] ?? allMethods;
|
||||
} else {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "codemirror-lang-dexpr",
|
||||
"version": "0.1.0",
|
||||
"name": "@duhanbalci/codemirror-lang-dexpr",
|
||||
"version": "0.3.0",
|
||||
"description": "CodeMirror 6 language support for dexpr",
|
||||
"type": "module",
|
||||
"main": "dist/index.cjs",
|
||||
@@ -17,7 +17,8 @@
|
||||
"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",
|
||||
"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": {
|
||||
"@codemirror/autocomplete": "^6.0.0",
|
||||
@@ -37,7 +38,8 @@
|
||||
"@lezer/lr": "^1.4.8",
|
||||
"codemirror": "^6.0.2",
|
||||
"tsup": "^8.0.0",
|
||||
"typescript": "^5.0.0"
|
||||
"typescript": "^5.0.0",
|
||||
"vitest": "^4.1.3"
|
||||
},
|
||||
"license": "MIT"
|
||||
}
|
||||
|
||||
149
editor/src/completions.test.ts
Normal file
149
editor/src/completions.test.ts
Normal 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");
|
||||
});
|
||||
});
|
||||
@@ -17,7 +17,8 @@ export type DexprType =
|
||||
| "Boolean"
|
||||
| "NumberList"
|
||||
| "StringList"
|
||||
| "Object";
|
||||
| "Object"
|
||||
| "List";
|
||||
|
||||
export interface FunctionInfo {
|
||||
name: string;
|
||||
@@ -171,15 +172,15 @@ function inferExprType(
|
||||
if (objNode.name === "VariableName") {
|
||||
const varName = doc.sliceString(objNode.from, objNode.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);
|
||||
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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
@@ -204,7 +205,7 @@ function findChild(
|
||||
}
|
||||
|
||||
/** Infer return type from known method names */
|
||||
function inferMethodReturnType(method: string): DexprType | null {
|
||||
export function inferMethodReturnType(method: string): DexprType | null {
|
||||
switch (method) {
|
||||
// String -> String
|
||||
case "upper":
|
||||
@@ -245,11 +246,29 @@ function inferMethodReturnType(method: string): DexprType | null {
|
||||
return null; // depends on input type
|
||||
case "join":
|
||||
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:
|
||||
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 ---
|
||||
|
||||
function dedup(items: Completion[]): Completion[] {
|
||||
@@ -290,7 +309,7 @@ export function dexprCompletion(info: DexprLanguageInfo): Extension {
|
||||
// and field completions per Object variable
|
||||
const objectFieldCompletions = new Map<string, Completion[]>();
|
||||
for (const v of info.variables ?? []) {
|
||||
if (v.type === "Object" && v.fields) {
|
||||
if ((v.type === "Object" || v.type === "List") && v.fields) {
|
||||
const fieldItems: Completion[] = [];
|
||||
for (const f of v.fields) {
|
||||
// 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
|
||||
let currentType = rootType;
|
||||
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 };
|
||||
}
|
||||
// "customer.name" key convention
|
||||
const key = `${path[i - 1]}.${path[i]}`;
|
||||
const fieldType = varTypes.get(key) ?? null;
|
||||
currentType = fieldType;
|
||||
}
|
||||
|
||||
return { type: currentType, path };
|
||||
@@ -408,6 +431,12 @@ export function dexprCompletion(info: DexprLanguageInfo): Extension {
|
||||
const fieldItems = objectFieldCompletions.get(rootVarName) ?? [];
|
||||
const objMethods = methodsByType["Object"] ?? [];
|
||||
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) {
|
||||
options = methodsByType[finalType] ?? allMethods;
|
||||
} else {
|
||||
|
||||
491
flamegraph.svg
491
flamegraph.svg
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 48 KiB |
74
gen.js
74
gen.js
@@ -1,74 +0,0 @@
|
||||
function randomBetween(min, max) {
|
||||
min = Math.round(min);
|
||||
max = Math.round(max);
|
||||
return Math.floor(Math.random() * (max - min + 1) + min);
|
||||
}
|
||||
|
||||
function shuffleArray(array) {
|
||||
for (let i = array.length - 1; i > 0; i--) {
|
||||
const j = Math.floor(Math.random() * (i + 1));
|
||||
[array[i], array[j]] = [array[j], array[i]];
|
||||
}
|
||||
}
|
||||
|
||||
function generateRandomArithmeticExpression() {
|
||||
const operators = ['+', '-', '*', '/']; // Array of arithmetic operators
|
||||
const maxDecimalPlaces = 5;
|
||||
const numOperands = randomBetween(10, 20);
|
||||
const numParentheses = randomBetween(4, 8);
|
||||
|
||||
// Generate random numbers with two decimal places
|
||||
let nums = [];
|
||||
for (let i = 0; i < numOperands; i++) {
|
||||
nums.push((Math.random() * 100).toFixed(randomBetween(1, maxDecimalPlaces)));
|
||||
}
|
||||
|
||||
let res = [];
|
||||
|
||||
shuffleArray(nums);
|
||||
|
||||
let numRemainingOperands = nums.length;
|
||||
let openParentheses = 0;
|
||||
|
||||
for (let i = 0; i < nums.length; i++) {
|
||||
// Open a parenthesis if there are enough operands remaining
|
||||
if (openParentheses < numParentheses && numRemainingOperands > 1 && Math.random() < 0.5) {
|
||||
res.push('(');
|
||||
openParentheses++;
|
||||
}
|
||||
|
||||
res.push(nums[i]);
|
||||
|
||||
// Close a parenthesis if there are enough operands preceding it
|
||||
if (openParentheses > 0 && numRemainingOperands > 2 && Math.random() < 0.5) {
|
||||
res.push(')');
|
||||
openParentheses--;
|
||||
}
|
||||
|
||||
if (i < nums.length - 1) {
|
||||
res.push(operators[randomBetween(0, 3)]);
|
||||
}
|
||||
|
||||
numRemainingOperands--;
|
||||
}
|
||||
|
||||
// Close any remaining open parentheses
|
||||
while (openParentheses > 0) {
|
||||
res.push(')');
|
||||
openParentheses--;
|
||||
}
|
||||
|
||||
return res.join('');
|
||||
}
|
||||
|
||||
let res = '';
|
||||
|
||||
for (let i = 0; i < 100; i++) {
|
||||
let expr = generateRandomArithmeticExpression();
|
||||
let val = eval(expr);
|
||||
|
||||
// res += `("${expr}", "${val}"),\n`;
|
||||
res += `"${expr}",\n`;
|
||||
}
|
||||
|
||||
console.log(res);
|
||||
18
justfile
18
justfile
@@ -12,15 +12,21 @@ bench:
|
||||
run:
|
||||
cargo run --release
|
||||
|
||||
# --- Publish ---
|
||||
|
||||
# Publish dexpr to Gitea cargo registry
|
||||
publish:
|
||||
cargo publish --registry gitea --allow-dirty
|
||||
|
||||
# --- WASM ---
|
||||
|
||||
# Build wasm package (web target)
|
||||
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)
|
||||
wasm-bundler:
|
||||
cd wasm && wasm-pack build --target bundler --release
|
||||
cd wasm && wasm-pack build --target bundler --release --scope duhanbalci
|
||||
|
||||
# --- Editor ---
|
||||
|
||||
@@ -36,6 +42,14 @@ editor-build: editor-grammar
|
||||
editor-demo: editor-build
|
||||
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 ---
|
||||
|
||||
# Build everything (wasm + editor)
|
||||
|
||||
BIN
profile.json.gz
BIN
profile.json.gz
Binary file not shown.
@@ -2,6 +2,7 @@ use indexmap::IndexMap;
|
||||
use rust_decimal::Decimal;
|
||||
use smol_str::SmolStr;
|
||||
use std::fmt;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// Value type for the dExpr language
|
||||
#[derive(Debug, Clone, PartialEq, Default)]
|
||||
@@ -11,9 +12,10 @@ pub enum Value {
|
||||
Number(Decimal),
|
||||
String(SmolStr),
|
||||
Boolean(bool),
|
||||
NumberList(Vec<Decimal>),
|
||||
StringList(Vec<SmolStr>),
|
||||
Object(IndexMap<SmolStr, Value>),
|
||||
NumberList(Rc<Vec<Decimal>>),
|
||||
StringList(Rc<Vec<SmolStr>>),
|
||||
Object(Rc<IndexMap<SmolStr, Value>>),
|
||||
List(Rc<Vec<Value>>),
|
||||
}
|
||||
|
||||
/// 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_STRING_LIST: u8 = 0x05;
|
||||
pub const TYPE_OBJECT: u8 = 0x06;
|
||||
pub const TYPE_LIST: u8 = 0x07;
|
||||
|
||||
impl fmt::Display for Value {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
@@ -62,6 +65,16 @@ impl fmt::Display for Value {
|
||||
}
|
||||
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::StringList(_) => TYPE_STRING_LIST,
|
||||
Value::Object(_) => TYPE_OBJECT,
|
||||
Value::List(_) => TYPE_LIST,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,6 +104,7 @@ impl Value {
|
||||
Value::NumberList(_) => "NumberList",
|
||||
Value::StringList(_) => "StringList",
|
||||
Value::Object(_) => "Object",
|
||||
Value::List(_) => "List",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -120,7 +135,7 @@ impl Value {
|
||||
bytes.push((list.len() >> 8) as u8);
|
||||
bytes.push(list.len() as u8);
|
||||
// List items
|
||||
for n in list {
|
||||
for n in list.iter() {
|
||||
bytes.extend_from_slice(&n.serialize());
|
||||
}
|
||||
}
|
||||
@@ -129,7 +144,7 @@ impl Value {
|
||||
bytes.push((list.len() >> 8) as u8);
|
||||
bytes.push(list.len() as u8);
|
||||
// List items
|
||||
for s in list {
|
||||
for s in list.iter() {
|
||||
// String length (2 bytes)
|
||||
bytes.push((s.len() >> 8) as u8);
|
||||
bytes.push(s.len() as u8);
|
||||
@@ -142,13 +157,22 @@ impl Value {
|
||||
bytes.push((map.len() >> 8) as u8);
|
||||
bytes.push(map.len() as u8);
|
||||
// Entries: key (string) + value (recursive)
|
||||
for (key, val) in map {
|
||||
for (key, val) in map.iter() {
|
||||
bytes.push((key.len() >> 8) as u8);
|
||||
bytes.push(key.len() as u8);
|
||||
bytes.extend_from_slice(key.as_bytes());
|
||||
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
|
||||
@@ -209,19 +233,25 @@ impl From<SmolStr> for Value {
|
||||
|
||||
impl From<Vec<Decimal>> for Value {
|
||||
fn from(v: Vec<Decimal>) -> Self {
|
||||
Value::NumberList(v)
|
||||
Value::NumberList(Rc::new(v))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<SmolStr>> for Value {
|
||||
fn from(v: Vec<SmolStr>) -> Self {
|
||||
Value::StringList(v)
|
||||
Value::StringList(Rc::new(v))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IndexMap<SmolStr, Value>> for Value {
|
||||
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));
|
||||
}
|
||||
|
||||
Ok((Value::NumberList(list), pos))
|
||||
Ok((Value::NumberList(Rc::new(list)), pos))
|
||||
}
|
||||
TYPE_STRING_LIST => {
|
||||
if bytes.len() < pos + 2 {
|
||||
@@ -321,7 +351,7 @@ impl Value {
|
||||
list.push(s.into());
|
||||
}
|
||||
|
||||
Ok((Value::StringList(list), pos))
|
||||
Ok((Value::StringList(Rc::new(list)), pos))
|
||||
}
|
||||
TYPE_OBJECT => {
|
||||
if bytes.len() < pos + 2 {
|
||||
@@ -354,7 +384,23 @@ impl Value {
|
||||
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)),
|
||||
}
|
||||
@@ -407,7 +453,7 @@ impl Value {
|
||||
serde_json::Value::String(s) => Ok(Value::String(SmolStr::new(s))),
|
||||
serde_json::Value::Array(arr) => {
|
||||
if arr.is_empty() {
|
||||
return Ok(Value::StringList(Vec::new()));
|
||||
return Ok(Value::StringList(Rc::new(Vec::new())));
|
||||
}
|
||||
// Check if all elements are the same type
|
||||
let first = &arr[0];
|
||||
@@ -418,14 +464,18 @@ impl Value {
|
||||
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()) {
|
||||
let strings: Vec<SmolStr> = arr.iter()
|
||||
.filter_map(|v| v.as_str().map(SmolStr::new))
|
||||
.collect();
|
||||
Ok(Value::StringList(strings))
|
||||
Ok(Value::StringList(Rc::new(strings)))
|
||||
} 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) => {
|
||||
@@ -433,7 +483,7 @@ impl Value {
|
||||
for (k, v) in obj {
|
||||
map.insert(SmolStr::new(k), Self::from_json_value(v)?);
|
||||
}
|
||||
Ok(Value::Object(map))
|
||||
Ok(Value::Object(Rc::new(map)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,9 +126,9 @@ impl<'a> BytecodeReader<'a> {
|
||||
self.read_byte()
|
||||
}
|
||||
|
||||
/// Read a string
|
||||
/// Read a string as a borrowed slice (zero-copy from bytecode)
|
||||
#[inline(always)]
|
||||
pub fn read_string(&mut self) -> Result<SmolStr, String> {
|
||||
pub fn read_str(&mut self) -> Result<&'a str, String> {
|
||||
let length = self.read_u16()? as usize;
|
||||
|
||||
if self.position + length > self.bytecode.len() {
|
||||
@@ -139,7 +139,13 @@ impl<'a> BytecodeReader<'a> {
|
||||
.map_err(|_| "Invalid UTF-8 string".to_string())?;
|
||||
self.position += length;
|
||||
|
||||
Ok(s.into())
|
||||
Ok(s)
|
||||
}
|
||||
|
||||
/// Read a string as SmolStr (allocating — use read_str when possible)
|
||||
#[inline(always)]
|
||||
pub fn read_string(&mut self) -> Result<SmolStr, String> {
|
||||
self.read_str().map(SmolStr::from)
|
||||
}
|
||||
|
||||
/// Read a value
|
||||
|
||||
@@ -223,6 +223,7 @@ pub fn disassemble_bytecode(bytecode: &[u8]) -> Vec<String> {
|
||||
Ok(reg) => format!("{:04x}: SetResult r{}", start_position, reg),
|
||||
Err(_) => format!("{:04x}: SetResult (truncated)", start_position),
|
||||
},
|
||||
OpCodeByte::ClearResult => format!("{:04x}: ClearResult", start_position),
|
||||
OpCodeByte::End => format!("{:04x}: End", start_position),
|
||||
};
|
||||
|
||||
|
||||
@@ -170,6 +170,7 @@ impl Compiler {
|
||||
let expr_reg = self.compile_expr(expr)?;
|
||||
self.emit_store_global(name, expr_reg);
|
||||
self.free_register(expr_reg);
|
||||
self.emit_byte(OpCodeByte::ClearResult.to_byte());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -229,6 +230,7 @@ impl Compiler {
|
||||
// Store root back to global
|
||||
self.emit_store_global(root, root_reg);
|
||||
self.free_register(root_reg);
|
||||
self.emit_byte(OpCodeByte::ClearResult.to_byte());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -319,13 +321,7 @@ impl Compiler {
|
||||
let result_reg = self.allocate_register()?;
|
||||
|
||||
let opcode = match op {
|
||||
Op::Add => {
|
||||
if self.is_string_concatenation(left, right) {
|
||||
OpCodeByte::Concat
|
||||
} else {
|
||||
OpCodeByte::Add
|
||||
}
|
||||
}
|
||||
Op::Add => OpCodeByte::Add,
|
||||
Op::Sub => OpCodeByte::Sub,
|
||||
Op::Mul => OpCodeByte::Mul,
|
||||
Op::Div => OpCodeByte::Div,
|
||||
@@ -359,12 +355,6 @@ impl Compiler {
|
||||
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
|
||||
fn compile_unary_op(&mut self, op: &Op, operand: &Expr) -> Result<u8, CompileError> {
|
||||
let operand_reg = self.compile_expr(operand)?;
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
//! use indexmap::IndexMap;
|
||||
//! use smol_str::SmolStr;
|
||||
//! use rust_decimal_macros::dec;
|
||||
//! use std::rc::Rc;
|
||||
//!
|
||||
//! let mut info = LanguageInfo::builtin();
|
||||
//!
|
||||
@@ -24,7 +25,7 @@
|
||||
//! let mut customer = IndexMap::new();
|
||||
//! customer.insert(SmolStr::new("name"), Value::String("Alice".into()));
|
||||
//! customer.insert(SmolStr::new("age"), Value::Number(dec!(30)));
|
||||
//! info.add_value("customer", &Value::Object(customer), None);
|
||||
//! info.add_value("customer", &Value::Object(Rc::new(customer)), None);
|
||||
//! info.add_value("price", &Value::Number(dec!(100)), None);
|
||||
//!
|
||||
//! let json = info.to_json();
|
||||
@@ -120,6 +121,19 @@ impl LanguageInfo {
|
||||
type_name: v.type_name().to_string(),
|
||||
}).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,
|
||||
};
|
||||
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: "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![
|
||||
MethodInfo { name: "length", signature: "() -> Number", doc: None },
|
||||
MethodInfo { name: "len", signature: "() -> Number", doc: None },
|
||||
|
||||
@@ -2,7 +2,7 @@ use dexpr::{ast::value::Value, compiler::Compiler, parser, vm::VM};
|
||||
use rust_decimal_macros::dec;
|
||||
|
||||
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)?;
|
||||
|
||||
|
||||
@@ -5,10 +5,31 @@ pub struct Register(pub u8);
|
||||
/// Default (built-in) function IDs for CallDefault opcode
|
||||
pub mod default_fn {
|
||||
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
|
||||
pub const NAMES: &[(&str, u8)] = &[("rand", RAND)];
|
||||
/// Lookup table: function name <EFBFBD><EFBFBD><EFBFBD> ID
|
||||
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
|
||||
pub fn name(id: u8) -> Option<&'static str> {
|
||||
@@ -77,7 +98,8 @@ pub enum OpCodeByte {
|
||||
CallDefault = 0xA2, // Call default (built-in) function by ID
|
||||
|
||||
// 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 = 0xFF, // End of program
|
||||
@@ -128,6 +150,7 @@ impl OpCodeByte {
|
||||
0xA1 => Some(OpCodeByte::CallExternal),
|
||||
0xA2 => Some(OpCodeByte::CallDefault),
|
||||
0xB0 => Some(OpCodeByte::SetResult),
|
||||
0xB1 => Some(OpCodeByte::ClearResult),
|
||||
0xFF => Some(OpCodeByte::End),
|
||||
_ => None,
|
||||
};
|
||||
@@ -178,6 +201,7 @@ impl OpCodeByte {
|
||||
OpCodeByte::CallExternal => "CallExternal",
|
||||
OpCodeByte::CallDefault => "Rand",
|
||||
OpCodeByte::SetResult => "SetResult",
|
||||
OpCodeByte::ClearResult => "ClearResult",
|
||||
OpCodeByte::End => "End",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
fib:
|
||||
push rbp
|
||||
movrr rbp, rsp
|
||||
movsr rbp, r1
|
||||
cmpsi rbp, 0
|
||||
jg .L0
|
||||
movri eax, 0
|
||||
ret
|
||||
.L0:
|
||||
cmpsi rbp, 2
|
||||
jg .L1
|
||||
movri eax, 1
|
||||
ret
|
||||
.L1:
|
||||
movrs r1, rbp
|
||||
|
||||
|
||||
|
||||
main:
|
||||
add rsp, 16
|
||||
movsi 16(byte) rbp, 10
|
||||
movrs r1, rbp
|
||||
call fib
|
||||
230
src/vm/builtins.rs
Normal file
230
src/vm/builtins.rs
Normal 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 ® in &arg_regs[1..] {
|
||||
let n = extract_number("min", &self.registers[reg])?;
|
||||
if n < result {
|
||||
result = n;
|
||||
}
|
||||
}
|
||||
self.registers[dest] = Value::Number(result);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn builtin_max(&mut self, dest: usize, arg_regs: &[usize]) -> Result<(), VMError> {
|
||||
if arg_regs.is_empty() {
|
||||
return Err(VMError::RuntimeError(
|
||||
"max() requires at least one argument".to_string(),
|
||||
));
|
||||
}
|
||||
let mut result = extract_number("max", &self.registers[arg_regs[0]])?;
|
||||
for ® in &arg_regs[1..] {
|
||||
let n = extract_number("max", &self.registers[reg])?;
|
||||
if n > result {
|
||||
result = n;
|
||||
}
|
||||
}
|
||||
self.registers[dest] = Value::Number(result);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn builtin_floor(&mut self, dest: usize, arg_regs: &[usize]) -> Result<(), VMError> {
|
||||
require_args("floor", arg_regs, 1)?;
|
||||
let n = extract_number("floor", &self.registers[arg_regs[0]])?;
|
||||
self.registers[dest] = Value::Number(n.floor());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn builtin_ceil(&mut self, dest: usize, arg_regs: &[usize]) -> Result<(), VMError> {
|
||||
require_args("ceil", arg_regs, 1)?;
|
||||
let n = extract_number("ceil", &self.registers[arg_regs[0]])?;
|
||||
self.registers[dest] = Value::Number(n.ceil());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn builtin_round(&mut self, dest: usize, arg_regs: &[usize]) -> Result<(), VMError> {
|
||||
require_args("round", arg_regs, 1)?;
|
||||
let n = extract_number("round", &self.registers[arg_regs[0]])?;
|
||||
// Optional second argument: decimal places (default 0)
|
||||
let places = if arg_regs.len() > 1 {
|
||||
extract_number("round", &self.registers[arg_regs[1]])?
|
||||
.to_u32()
|
||||
.unwrap_or(0)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
self.registers[dest] = Value::Number(n.round_dp(places));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn builtin_sqrt(&mut self, dest: usize, arg_regs: &[usize]) -> Result<(), VMError> {
|
||||
require_args("sqrt", arg_regs, 1)?;
|
||||
let n = extract_number("sqrt", &self.registers[arg_regs[0]])?;
|
||||
if n.is_sign_negative() {
|
||||
return Err(VMError::RuntimeError(
|
||||
"sqrt() argument must be non-negative".to_string(),
|
||||
));
|
||||
}
|
||||
let result = n.sqrt().ok_or_else(|| {
|
||||
VMError::RuntimeError("sqrt() failed to compute".to_string())
|
||||
})?;
|
||||
self.registers[dest] = Value::Number(result);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn builtin_len(&mut self, dest: usize, arg_regs: &[usize]) -> Result<(), VMError> {
|
||||
require_args("len", arg_regs, 1)?;
|
||||
let len = match &self.registers[arg_regs[0]] {
|
||||
Value::String(s) => Decimal::from(s.len()),
|
||||
Value::StringList(l) => Decimal::from(l.len()),
|
||||
Value::NumberList(l) => Decimal::from(l.len()),
|
||||
Value::Object(m) => Decimal::from(m.len()),
|
||||
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
749
src/vm/methods.rs
Normal 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),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
mod builtins;
|
||||
mod debug_info;
|
||||
pub mod error;
|
||||
mod methods;
|
||||
mod vm;
|
||||
|
||||
pub use debug_info::DebugInfo;
|
||||
|
||||
745
src/vm/vm.rs
745
src/vm/vm.rs
@@ -1,9 +1,10 @@
|
||||
use crate::{ast::value::Value, bytecode::BytecodeReader, opcodes::OpCodeByte};
|
||||
use bumpalo::Bump;
|
||||
use std::rc::Rc;
|
||||
use micromap::Map;
|
||||
use rust_decimal::{prelude::ToPrimitive, Decimal, MathematicalOps};
|
||||
use rust_decimal::{Decimal, MathematicalOps};
|
||||
use rustc_hash::FxHashMap;
|
||||
use smol_str::{SmolStr, StrExt};
|
||||
use smallvec::SmallVec;
|
||||
use smol_str::SmolStr;
|
||||
|
||||
/// Type alias for external (host) functions
|
||||
pub type ExternalFn = Box<dyn Fn(&[Value]) -> Result<Value, String>>;
|
||||
@@ -33,36 +34,25 @@ macro_rules! log_debug {
|
||||
|
||||
/// Virtual Machine for executing dExpr bytecode
|
||||
pub struct VM<'a> {
|
||||
bytecode: &'a [u8], // Bytecode to execute
|
||||
reader: BytecodeReader<'a>, // Bytecode reader
|
||||
pc: usize, // Program counter
|
||||
bytecode: &'a [u8],
|
||||
pub(super) reader: BytecodeReader<'a>,
|
||||
pc: usize,
|
||||
|
||||
// Registers for computation
|
||||
registers: [Value; MAX_REGISTERS],
|
||||
pub(super) registers: [Value; MAX_REGISTERS],
|
||||
|
||||
// Global variables
|
||||
globals: Map<SmolStr, Value, 64>,
|
||||
|
||||
// Last expression result (returned by execute)
|
||||
last_result: Value,
|
||||
|
||||
// External (host) functions — lazily allocated
|
||||
external_functions: Option<FxHashMap<SmolStr, ExternalFn>>,
|
||||
pub(super) external_functions: Option<FxHashMap<SmolStr, ExternalFn>>,
|
||||
|
||||
// External (host) methods per type — lazily allocated
|
||||
external_methods: Option<FxHashMap<(SmolStr, SmolStr), ExternalMethod>>,
|
||||
pub(super) 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 flag
|
||||
#[cfg(debug_assertions)]
|
||||
debug: bool,
|
||||
pub(super) debug: bool,
|
||||
|
||||
// Profiling counts
|
||||
#[cfg(debug_assertions)]
|
||||
opcode_counts: [usize; 256],
|
||||
}
|
||||
@@ -79,7 +69,6 @@ impl<'a> VM<'a> {
|
||||
last_result: Value::Null,
|
||||
external_functions: None,
|
||||
external_methods: None,
|
||||
heap: Bump::new(),
|
||||
debug_info: None,
|
||||
#[cfg(debug_assertions)]
|
||||
debug: false,
|
||||
@@ -153,7 +142,6 @@ impl<'a> VM<'a> {
|
||||
self.registers = [const { Value::Null }; MAX_REGISTERS];
|
||||
self.last_result = Value::Null;
|
||||
// Preserve globals
|
||||
self.heap = Bump::new();
|
||||
}
|
||||
|
||||
/// Execute the bytecode program and return the last expression result
|
||||
@@ -195,7 +183,7 @@ impl<'a> VM<'a> {
|
||||
OpCodeByte::StoreLocal => self.handle_store_local(),
|
||||
OpCodeByte::LoadGlobal => self.handle_load_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::Mul => self.binary_op(|a, b| Ok(a * b), "multiply"),
|
||||
OpCodeByte::Div => self.binary_op(
|
||||
@@ -240,6 +228,11 @@ impl<'a> VM<'a> {
|
||||
OpCodeByte::CallExternal => self.handle_call_external(),
|
||||
OpCodeByte::CallDefault => self.handle_call_default(),
|
||||
OpCodeByte::SetResult => self.handle_set_result(),
|
||||
OpCodeByte::ClearResult => {
|
||||
self.last_result = Value::Null;
|
||||
log_debug!(self, "ClearResult");
|
||||
Ok(())
|
||||
}
|
||||
OpCodeByte::End => {
|
||||
log_debug!(self, "End of program");
|
||||
#[cfg(debug_assertions)]
|
||||
@@ -406,12 +399,12 @@ impl<'a> VM<'a> {
|
||||
#[inline]
|
||||
fn handle_load_global(&mut self) -> Result<(), VMError> {
|
||||
let reg = self.read_register_checked()?;
|
||||
let name = self.reader.read_string().map_err(VMError::BytecodeError)?;
|
||||
let name = self.reader.read_str().map_err(VMError::BytecodeError)?;
|
||||
|
||||
let value = self
|
||||
.globals
|
||||
.get(&name)
|
||||
.ok_or_else(|| VMError::UndefinedVariable(name.clone()))?;
|
||||
.get(name)
|
||||
.ok_or_else(|| VMError::UndefinedVariable(SmolStr::from(name)))?;
|
||||
self.registers[reg] = value.clone();
|
||||
|
||||
log_debug!(self,
|
||||
@@ -424,12 +417,12 @@ impl<'a> VM<'a> {
|
||||
/// Handle StoreGlobal opcode - store register to global variable
|
||||
#[inline]
|
||||
fn handle_store_global(&mut self) -> Result<(), VMError> {
|
||||
let name = self.reader.read_string().map_err(VMError::BytecodeError)?;
|
||||
let name = self.reader.read_str().map_err(VMError::BytecodeError)?;
|
||||
let reg = self.read_register_checked()?;
|
||||
|
||||
self
|
||||
.globals
|
||||
.insert(name.clone(), self.registers[reg].clone());
|
||||
.insert(SmolStr::from(name), self.registers[reg].clone());
|
||||
|
||||
log_debug!(self,
|
||||
"StoreGlobal global.{} = r{} ({})",
|
||||
@@ -464,6 +457,54 @@ impl<'a> VM<'a> {
|
||||
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
|
||||
// ============================================================================
|
||||
@@ -556,6 +597,8 @@ impl<'a> VM<'a> {
|
||||
}
|
||||
// String in Object (key check)
|
||||
(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) => {
|
||||
return Err(VMError::InvalidOperation {
|
||||
operation: "in",
|
||||
@@ -614,26 +657,19 @@ impl<'a> VM<'a> {
|
||||
// Opcode Handlers - String Operations
|
||||
// ============================================================================
|
||||
|
||||
/// Handle Concat opcode - string concatenation
|
||||
/// Handle Concat opcode - string concatenation with auto-coercion
|
||||
#[inline]
|
||||
fn handle_concat(&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::String(a_str), Value::String(b_str)) => {
|
||||
let result = format!("{}{}", a_str, b_str);
|
||||
self.registers[dest] = Value::String(result.into());
|
||||
}
|
||||
(a_val, b_val) => {
|
||||
return Err(VMError::InvalidOperation {
|
||||
operation: "concat",
|
||||
left_type: a_val.type_name(),
|
||||
right_type: b_val.type_name(),
|
||||
});
|
||||
}
|
||||
}
|
||||
let a_cow = value_to_string(&self.registers[a]);
|
||||
let b_cow = value_to_string(&self.registers[b]);
|
||||
let mut result = String::with_capacity(a_cow.len() + b_cow.len());
|
||||
result.push_str(&a_cow);
|
||||
result.push_str(&b_cow);
|
||||
self.registers[dest] = Value::String(result.into());
|
||||
|
||||
log_debug!(self, "Concat r{} = r{} + r{}", dest, a, b);
|
||||
Ok(())
|
||||
@@ -643,13 +679,49 @@ impl<'a> VM<'a> {
|
||||
fn handle_get_property(&mut self) -> Result<(), VMError> {
|
||||
let dest = self.read_register_checked()?;
|
||||
let obj = self.read_register_checked()?;
|
||||
let prop = self.reader.read_string().map_err(VMError::BytecodeError)?;
|
||||
let prop = self.reader.read_str().map_err(VMError::BytecodeError)?;
|
||||
|
||||
match &self.registers[obj] {
|
||||
Value::Object(map) => {
|
||||
let value = map.get(&prop).cloned().unwrap_or(Value::Null);
|
||||
let value = map.get(prop).cloned().unwrap_or(Value::Null);
|
||||
self.registers[dest] = value;
|
||||
}
|
||||
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 => {
|
||||
return Err(VMError::RuntimeError(format!(
|
||||
"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)
|
||||
fn handle_set_property(&mut self) -> Result<(), VMError> {
|
||||
let obj = self.read_register_checked()?;
|
||||
let prop = self.reader.read_string().map_err(VMError::BytecodeError)?;
|
||||
let prop = self.reader.read_str().map_err(VMError::BytecodeError)?;
|
||||
let val = self.read_register_checked()?;
|
||||
|
||||
let value = self.registers[val].clone();
|
||||
match &mut self.registers[obj] {
|
||||
Value::Object(map) => {
|
||||
map.insert(prop.clone(), value);
|
||||
Rc::make_mut(map).insert(SmolStr::from(prop), value);
|
||||
}
|
||||
other => {
|
||||
return Err(VMError::RuntimeError(format!(
|
||||
@@ -689,538 +761,21 @@ impl<'a> VM<'a> {
|
||||
fn handle_method_call(&mut self) -> Result<(), VMError> {
|
||||
let dest = self.read_register_checked()?;
|
||||
let obj = self.read_register_checked()?;
|
||||
let method = self.reader.read_string().map_err(VMError::BytecodeError)?;
|
||||
let method = self.reader.read_str().map_err(VMError::BytecodeError)?;
|
||||
|
||||
// Read argument registers
|
||||
let arg_count = self
|
||||
.reader
|
||||
.read_byte()
|
||||
.map_err(VMError::BytecodeError)? as usize;
|
||||
let mut args = Vec::with_capacity(arg_count);
|
||||
let mut args: SmallVec<[Value; 4]> = SmallVec::with_capacity(arg_count);
|
||||
|
||||
for _ in 0..arg_count {
|
||||
let reg = self.read_register_checked()?;
|
||||
args.push(self.registers[reg].clone());
|
||||
}
|
||||
|
||||
// Dispatch method call
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
self.dispatch_method(dest, obj, method, &args)?;
|
||||
|
||||
log_debug!(self, "MethodCall r{} = r{}.{}(...)", dest, obj, method);
|
||||
Ok(())
|
||||
@@ -1232,48 +787,16 @@ impl<'a> VM<'a> {
|
||||
|
||||
/// Handle CallDefault opcode - call a default (built-in) function by ID
|
||||
fn handle_call_default(&mut self) -> Result<(), VMError> {
|
||||
use crate::opcodes::default_fn;
|
||||
|
||||
let dest = self.read_register_checked()?;
|
||||
let fn_id = self.reader.read_byte().map_err(VMError::BytecodeError)?;
|
||||
let arg_count = self.reader.read_byte().map_err(VMError::BytecodeError)? as usize;
|
||||
|
||||
let mut arg_regs = Vec::with_capacity(arg_count);
|
||||
let mut arg_regs: SmallVec<[usize; 4]> = SmallVec::with_capacity(arg_count);
|
||||
for _ in 0..arg_count {
|
||||
arg_regs.push(self.read_register_checked()?);
|
||||
}
|
||||
|
||||
match fn_id {
|
||||
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)));
|
||||
}
|
||||
}
|
||||
self.dispatch_builtin(dest, fn_id, &arg_regs)?;
|
||||
|
||||
log_debug!(self, "CallDefault r{} = fn#{}(...)", dest, fn_id);
|
||||
Ok(())
|
||||
@@ -1282,20 +805,21 @@ impl<'a> VM<'a> {
|
||||
/// Handle CallExternal opcode - call host function by name
|
||||
fn handle_call_external(&mut self) -> Result<(), VMError> {
|
||||
let dest = self.read_register_checked()?;
|
||||
let name = self.reader.read_string().map_err(VMError::BytecodeError)?;
|
||||
let name = self.reader.read_str().map_err(VMError::BytecodeError)?;
|
||||
let arg_count = self.reader.read_byte().map_err(VMError::BytecodeError)? as usize;
|
||||
|
||||
let mut args = Vec::with_capacity(arg_count);
|
||||
let mut args: SmallVec<[Value; 4]> = SmallVec::with_capacity(arg_count);
|
||||
for _ in 0..arg_count {
|
||||
let reg = self.read_register_checked()?;
|
||||
args.push(self.registers[reg].clone());
|
||||
}
|
||||
|
||||
let name_key = SmolStr::from(name);
|
||||
let func = self
|
||||
.external_functions
|
||||
.as_ref()
|
||||
.and_then(|m| m.get(&name))
|
||||
.ok_or_else(|| VMError::RuntimeError(format!("Undefined function: {}", name)))?;
|
||||
.and_then(|m| m.get(&name_key))
|
||||
.ok_or_else(|| VMError::RuntimeError(format!("Undefined function: {}", name_key)))?;
|
||||
|
||||
let result = func(&args).map_err(VMError::RuntimeError)?;
|
||||
self.registers[dest] = result;
|
||||
@@ -1415,3 +939,14 @@ impl<'a> VM<'a> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a Value to its string representation for concatenation (no quotes around strings)
|
||||
pub(super) fn value_to_string(val: &Value) -> std::borrow::Cow<'_, str> {
|
||||
match val {
|
||||
Value::String(s) => std::borrow::Cow::Borrowed(s.as_str()),
|
||||
Value::Number(n) => std::borrow::Cow::Owned(n.to_string()),
|
||||
Value::Boolean(b) => std::borrow::Cow::Borrowed(if *b { "true" } else { "false" }),
|
||||
Value::Null => std::borrow::Cow::Borrowed("null"),
|
||||
other => std::borrow::Cow::Owned(format!("{}", other)),
|
||||
}
|
||||
}
|
||||
|
||||
144
tests/data_driven_tests.rs
Normal file
144
tests/data_driven_tests.rs
Normal file
@@ -0,0 +1,144 @@
|
||||
use dexpr::{ast::value::Value, compiler::Compiler, parser, vm::VM};
|
||||
use indexmap::IndexMap;
|
||||
use rust_decimal::Decimal;
|
||||
use smol_str::SmolStr;
|
||||
use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct TestCase {
|
||||
name: String,
|
||||
code: String,
|
||||
#[serde(default)]
|
||||
globals: HashMap<String, ValueDef>,
|
||||
expected: ValueDef,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
struct ValueDef {
|
||||
#[serde(rename = "type")]
|
||||
typ: String,
|
||||
#[serde(default)]
|
||||
value: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
fn value_def_to_value(def: &ValueDef) -> Value {
|
||||
match def.typ.as_str() {
|
||||
"null" => Value::Null,
|
||||
"number" => {
|
||||
let s = def.value.as_ref().unwrap().as_str().unwrap();
|
||||
Value::Number(Decimal::from_str(s).unwrap())
|
||||
}
|
||||
"string" => {
|
||||
let s = def.value.as_ref().unwrap().as_str().unwrap();
|
||||
Value::String(s.into())
|
||||
}
|
||||
"boolean" => {
|
||||
let b = def.value.as_ref().unwrap().as_bool().unwrap();
|
||||
Value::Boolean(b)
|
||||
}
|
||||
"object" => {
|
||||
fn json_obj_to_value(obj: &serde_json::Map<String, serde_json::Value>) -> Value {
|
||||
let mut map = IndexMap::new();
|
||||
for (k, v) in obj {
|
||||
let val = match v {
|
||||
serde_json::Value::String(s) => {
|
||||
if let Ok(d) = Decimal::from_str(s) {
|
||||
Value::Number(d)
|
||||
} else {
|
||||
Value::String(SmolStr::from(s.as_str()))
|
||||
}
|
||||
}
|
||||
serde_json::Value::Bool(b) => Value::Boolean(*b),
|
||||
serde_json::Value::Object(nested) => json_obj_to_value(nested),
|
||||
_ => Value::String(SmolStr::from(v.to_string())),
|
||||
};
|
||||
map.insert(SmolStr::from(k.as_str()), val);
|
||||
}
|
||||
Value::Object(Rc::new(map))
|
||||
}
|
||||
let obj = def.value.as_ref().unwrap().as_object().unwrap();
|
||||
json_obj_to_value(obj)
|
||||
}
|
||||
other => panic!("Unknown type: {other}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_all_cases() {
|
||||
let json = include_str!("test_cases.json");
|
||||
let cases: Vec<TestCase> =
|
||||
serde_json::from_str(json).expect("Failed to parse test_cases.json");
|
||||
|
||||
let mut failures = Vec::new();
|
||||
let total = cases.len();
|
||||
|
||||
for case in &cases {
|
||||
let ast = match parser::program(&case.code) {
|
||||
Ok(ast) => ast,
|
||||
Err(e) => {
|
||||
failures.push(format!(
|
||||
"FAIL: {}\n code: {}\n error: parse failed: {e}",
|
||||
case.name,
|
||||
case.code.replace('\n', "\\n"),
|
||||
));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let mut compiler = Compiler::new();
|
||||
let bytecode = match compiler.compile(ast) {
|
||||
Ok(bc) => bc,
|
||||
Err(e) => {
|
||||
failures.push(format!(
|
||||
"FAIL: {}\n code: {}\n error: compile failed: {e}",
|
||||
case.name,
|
||||
case.code.replace('\n', "\\n"),
|
||||
));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let mut vm = VM::new(&bytecode);
|
||||
|
||||
for (name, def) in &case.globals {
|
||||
vm.set_global(name, value_def_to_value(def));
|
||||
}
|
||||
|
||||
let result = match vm.execute() {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
failures.push(format!(
|
||||
"FAIL: {}\n code: {}\n error: execute failed: {e}",
|
||||
case.name,
|
||||
case.code.replace('\n', "\\n"),
|
||||
));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let expected = value_def_to_value(&case.expected);
|
||||
|
||||
if result != expected {
|
||||
failures.push(format!(
|
||||
"FAIL: {}\n code: {}\n expected: {:?}\n got: {:?}",
|
||||
case.name,
|
||||
case.code.replace('\n', "\\n"),
|
||||
expected,
|
||||
result
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if !failures.is_empty() {
|
||||
panic!(
|
||||
"\n{} / {} test cases failed:\n\n{}\n",
|
||||
failures.len(),
|
||||
total,
|
||||
failures.join("\n\n")
|
||||
);
|
||||
}
|
||||
|
||||
eprintln!("All {total} test cases passed.");
|
||||
}
|
||||
@@ -3,6 +3,7 @@ use indexmap::IndexMap;
|
||||
use rust_decimal::prelude::ToPrimitive;
|
||||
use rust_decimal_macros::dec;
|
||||
use smol_str::SmolStr;
|
||||
use std::rc::Rc;
|
||||
|
||||
/// Helper to run code and get the value of "result" variable
|
||||
fn run_and_get_result(code: &str) -> Value {
|
||||
@@ -628,7 +629,7 @@ fn test_numberlist_contains() {
|
||||
let mut compiler = Compiler::new();
|
||||
let bytecode = compiler.compile(ast).expect("compile");
|
||||
let mut vm = VM::new(&bytecode);
|
||||
vm.set_global("nums", Value::NumberList(vec![dec!(1), dec!(2), dec!(3), dec!(4)]));
|
||||
vm.set_global("nums", Value::NumberList(Rc::new(vec![dec!(1), dec!(2), dec!(3), dec!(4)])));
|
||||
vm.execute().expect("execute");
|
||||
assert_eq!(vm.get_global("result").unwrap(), &Value::Boolean(true));
|
||||
}
|
||||
@@ -639,7 +640,7 @@ fn test_numberlist_indexof() {
|
||||
let mut compiler = Compiler::new();
|
||||
let bytecode = compiler.compile(ast).expect("compile");
|
||||
let mut vm = VM::new(&bytecode);
|
||||
vm.set_global("nums", Value::NumberList(vec![dec!(1), dec!(2), dec!(3)]));
|
||||
vm.set_global("nums", Value::NumberList(Rc::new(vec![dec!(1), dec!(2), dec!(3)])));
|
||||
vm.execute().expect("execute");
|
||||
assert_eq!(vm.get_global("result").unwrap(), &Value::Number(dec!(2)));
|
||||
}
|
||||
@@ -650,7 +651,7 @@ fn test_numberlist_slice() {
|
||||
let mut compiler = Compiler::new();
|
||||
let bytecode = compiler.compile(ast).expect("compile");
|
||||
let mut vm = VM::new(&bytecode);
|
||||
vm.set_global("nums", Value::NumberList(vec![dec!(10), dec!(20), dec!(30), dec!(40)]));
|
||||
vm.set_global("nums", Value::NumberList(Rc::new(vec![dec!(10), dec!(20), dec!(30), dec!(40)])));
|
||||
vm.execute().expect("execute");
|
||||
// slice(1,3) = [20, 30], sum = 50
|
||||
assert_eq!(vm.get_global("result").unwrap(), &Value::Number(dec!(50)));
|
||||
@@ -662,7 +663,7 @@ fn test_numberlist_reverse() {
|
||||
let mut compiler = Compiler::new();
|
||||
let bytecode = compiler.compile(ast).expect("compile");
|
||||
let mut vm = VM::new(&bytecode);
|
||||
vm.set_global("nums", Value::NumberList(vec![dec!(1), dec!(2), dec!(3)]));
|
||||
vm.set_global("nums", Value::NumberList(Rc::new(vec![dec!(1), dec!(2), dec!(3)])));
|
||||
vm.execute().expect("execute");
|
||||
assert_eq!(vm.get_global("result").unwrap(), &Value::Number(dec!(3)));
|
||||
}
|
||||
@@ -673,7 +674,7 @@ fn test_numberlist_sort() {
|
||||
let mut compiler = Compiler::new();
|
||||
let bytecode = compiler.compile(ast).expect("compile");
|
||||
let mut vm = VM::new(&bytecode);
|
||||
vm.set_global("nums", Value::NumberList(vec![dec!(30), dec!(10), dec!(20)]));
|
||||
vm.set_global("nums", Value::NumberList(Rc::new(vec![dec!(30), dec!(10), dec!(20)])));
|
||||
vm.execute().expect("execute");
|
||||
assert_eq!(vm.get_global("result").unwrap(), &Value::Number(dec!(10)));
|
||||
}
|
||||
@@ -684,7 +685,7 @@ fn test_numberlist_isempty() {
|
||||
let mut compiler = Compiler::new();
|
||||
let bytecode = compiler.compile(ast).expect("compile");
|
||||
let mut vm = VM::new(&bytecode);
|
||||
vm.set_global("nums", Value::NumberList(vec![]));
|
||||
vm.set_global("nums", Value::NumberList(Rc::new(vec![])));
|
||||
vm.execute().expect("execute");
|
||||
assert_eq!(vm.get_global("result").unwrap(), &Value::Boolean(true));
|
||||
}
|
||||
@@ -959,27 +960,19 @@ result = x % y"#;
|
||||
}
|
||||
|
||||
#[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"
|
||||
y = 5
|
||||
result = x + y"#;
|
||||
x + y"#;
|
||||
|
||||
let ast = parser::program(code).unwrap();
|
||||
let mut compiler = Compiler::new();
|
||||
let (bytecode, debug_info) = compiler
|
||||
.compile_from_source(code)
|
||||
.expect("Failed to compile");
|
||||
|
||||
let bytecode = compiler.compile(ast).unwrap();
|
||||
let mut vm = VM::new(&bytecode);
|
||||
vm.set_debug_info(&debug_info);
|
||||
let result = vm.execute().unwrap();
|
||||
|
||||
let err = vm.execute().unwrap_err();
|
||||
let err_msg = err.to_string();
|
||||
|
||||
assert!(
|
||||
err_msg.contains("line 3"),
|
||||
"Error should contain line 3, got: {}",
|
||||
err_msg
|
||||
);
|
||||
assert_eq!(result, Value::String("hello5".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1036,7 +1029,7 @@ fn make_object(entries: Vec<(&str, Value)>) -> Value {
|
||||
for (k, v) in entries {
|
||||
map.insert(SmolStr::new(k), v);
|
||||
}
|
||||
Value::Object(map)
|
||||
Value::Object(Rc::new(map))
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1121,7 +1114,7 @@ fn test_object_method_keys() {
|
||||
let result = run_expr_with_globals("person.keys()", vec![("person", obj)]);
|
||||
assert_eq!(
|
||||
result,
|
||||
Value::StringList(vec![SmolStr::new("name"), SmolStr::new("age")])
|
||||
Value::StringList(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)]);
|
||||
assert_eq!(
|
||||
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))),
|
||||
]);
|
||||
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]
|
||||
@@ -1341,4 +1334,609 @@ fn test_from_json_full_workflow() {
|
||||
"#;
|
||||
let r = run_and_get_result_with_globals(code, vec![("customer", customer)]);
|
||||
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
1288
tests/test_cases.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "dexpr-wasm"
|
||||
version = "0.1.0"
|
||||
version = "0.3.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
|
||||
@@ -37,6 +37,9 @@ fn value_to_json(val: &Value) -> serde_json::Value {
|
||||
.collect();
|
||||
serde_json::Value::Object(obj)
|
||||
}
|
||||
Value::List(list) => {
|
||||
serde_json::Value::Array(list.iter().map(|v| value_to_json(v)).collect())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user