From ce46aad1458f84638165fbb78d3648e17f626d2b Mon Sep 17 00:00:00 2001 From: Duhan BALCI Date: Tue, 18 Aug 2026 15:16:14 +0300 Subject: [PATCH] 0.4.1 --- CLAUDE.md | 4 + Cargo.lock | 2 +- Cargo.toml | 2 +- docs/compiler.md | 14 ++ docs/language_info.md | 2 +- docs/opcodes.md | 5 +- docs/parser.md | 19 +- docs/vm.md | 7 +- editor/package.json | 2 +- src/bytecode_dump.rs | 478 +++++++++++++++++++------------------ src/compiler.rs | 47 ++++ src/language_info.rs | 4 + src/opcodes.rs | 419 ++++++++++++++++---------------- src/parser/grammar.rs | 125 ++++++---- src/vm/builtins.rs | 7 +- src/vm/methods.rs | 33 ++- src/vm/vm.rs | 43 ++++ tests/integration_tests.rs | 135 +++++++++++ tests/test_cases.json | 321 +++++++++++++++++++++++++ wasm/Cargo.lock | 4 +- wasm/Cargo.toml | 2 +- 21 files changed, 1161 insertions(+), 514 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index ea812a3..465e755 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -64,6 +64,10 @@ The dexpr language supports: - String methods (e.g., `.upper()`, `.lower()`, `.trim()`, `.trimStart()`, `.trimEnd()`, `.split()`, `.replace()`, `.contains()`, `.startsWith()`, `.endsWith()`, `.length`, `.charAt()`, `.substring()`) - Arithmetic (`+`, `-`, `*`, `/`, `%`, `**`), comparison, and logical operators. `==`/`!=` work on all types (structural, different types → `false`); `<`/`<=`/`>`/`>=` work on Number and String (lexicographic) - `null` literal (`x == null`) +- `&&`/`||` short-circuit (`x != null && x.name == "a"` is safe when `x` is null); left-associative, `&&` binds tighter than `||` +- Unary `-`/`!` bind looser than postfix: `!o.active` == `!(o.active)`, `-2 ** 2` == `-4` +- `.length` property works on String and lists (same as `.length()`) +- `round()` uses half-away-from-zero (`round(2.5)` == `3`) - `in` operator for membership testing (`"finans" in categories`, `5 in numbers`, `"hello" in "hello world"`, `"key" in obj`) - Compound assignments (`+=`, `-=`, `*=`, `/=`, `%=`) - Built-in `log()` function for output and `rand(min, max)` for random integers diff --git a/Cargo.lock b/Cargo.lock index a01b1c8..50dc884 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -304,7 +304,7 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" [[package]] name = "dexpr" -version = "0.4.0" +version = "0.4.1" dependencies = [ "bumpalo", "criterion", diff --git a/Cargo.toml b/Cargo.toml index 03b2155..a7bf89d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dexpr" -version = "0.4.0" +version = "0.4.1" edition = "2021" description = "Embeddable expression evaluator and bytecode VM" license = "MIT" diff --git a/docs/compiler.md b/docs/compiler.md index a7e4250..f9dc035 100644 --- a/docs/compiler.md +++ b/docs/compiler.md @@ -69,6 +69,20 @@ Parse ile birlikte pozisyon bilgisi de toplar ve `DebugInfo` üretir. 1. İfadeyi register'a derle 2. `StoreGlobal` emit et (tüm değişkenler global) +### Mantıksal Operatörler (`&&`, `\|\|`) — Short-Circuit + +`compile_logical_op()`; `And`/`Or` opcode'u yerine atlama ile derlenir, sağ taraf gerekmedikçe çalışmaz (`x != null && x.name == "a"` x null iken hata vermez): + +``` + -> rL + JumpIfFalse rL, END (|| için JumpIfTrue) + -> rR + Move rL, rR (rR == rL ise atlanır) +END: sonuç rL'de +``` + +Sol register sağ taraf derlenmeden önce serbest bırakılır (değeri sadece atlama yolunda lazım), böylece sağ taraf çoğunlukla aynı register'a düşer ve `Move` gerekmez. Sol operand Boolean olmak zorundadır (JumpIf* tip kontrolü yapar); sağ operandın değeri sonuç olarak geçer (`true && 1` → `1`). + ### If Statement (Koşullu Deyim) ``` diff --git a/docs/language_info.md b/docs/language_info.md index c14f71b..ad4559b 100644 --- a/docs/language_info.md +++ b/docs/language_info.md @@ -62,7 +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` | +| `List` | `length`, `len`, `isEmpty`, `first`, `last`, `get`, `contains`, `indexOf`, `slice`, `reverse`, `join`, `map`, `filter`, `find`, `sort`, `sum`, `avg`, `min`, `max` | ### `add_function(name, signature, doc)` diff --git a/docs/opcodes.md b/docs/opcodes.md index fd041df..62f0c60 100644 --- a/docs/opcodes.md +++ b/docs/opcodes.md @@ -53,8 +53,8 @@ Bytecode komut setini (instruction set) tanımlar. Her opcode bir `u8` değerine | Opcode | Değer | Açıklama | |--------|-------|----------| -| `And` | `0x50` | Mantıksal VE | -| `Or` | `0x51` | Mantıksal VEYA | +| `And` | `0x50` | Mantıksal VE (compiler artık emit etmiyor; `&&` JumpIfFalse ile derlenir) | +| `Or` | `0x51` | Mantıksal VEYA (compiler artık emit etmiyor; `\|\|` JumpIfTrue ile derlenir) | | `Not` | `0x52` | Mantıksal DEĞİL | ### Kontrol Akışı @@ -63,6 +63,7 @@ Bytecode komut setini (instruction set) tanımlar. Her opcode bir `u8` değerine |--------|-------|----------| | `Jump` | `0x60` | Koşulsuz atlama | | `JumpIfFalse` | `0x61` | Register false ise atla | +| `JumpIfTrue` | `0x62` | Register true ise atla (`\|\|` short-circuit) | ### Üyelik Testi diff --git a/docs/parser.md b/docs/parser.md index a9bcf65..6781e9b 100644 --- a/docs/parser.md +++ b/docs/parser.md @@ -45,14 +45,17 @@ Byte offset'ini 1-indexed satır ve sütuna dönüştürür. UTF-8 karakter sın En düşükten en yükseğe: -1. Method çağrıları, fonksiyon çağrıları -2. Mantıksal AND (`&&`) -3. Mantıksal OR (`||`) -4. Karşılaştırma (`==`, `!=`, `<`, `<=`, `>`, `>=`, `in`) -5. Toplama/Çıkarma (`+`, `-`) -6. Çarpma/Bölme/Mod (`*`, `/`, `%`) -7. Üs alma (`**`) - sağdan birleşimli (right-associative) -8. Tekli operatörler (`-`, `!`), atomik ifadeler +1. Fonksiyon çağrıları +2. Mantıksal OR (`||`) — soldan birleşimli +3. Mantıksal AND (`&&`) — soldan birleşimli (`a && b || c` = `(a && b) || c`) +4. Karşılaştırma (`==`, `!=`, `<`, `<=`, `>`, `>=`, `in`) — soldan birleşimli +5. Toplama/Çıkarma (`+`, `-`) — soldan birleşimli (`10 - 3 - 2` = `5`) +6. Çarpma/Bölme/Mod (`*`, `/`, `%`) — soldan birleşimli +7. Tekli operatörler (`-`, `!`) — `unary()` kuralı; postfix ve `**`'dan gevşek bağlanır: `!o.active` = `!(o.active)`, `-2 ** 2` = `-(2 ** 2)`, `- -1` geçerli +8. Üs alma (`**`) — sağdan birleşimli, üs kısmı `unary()` (`2 ** -1` geçerli) +9. Postfix (`.prop`, `.method()`), atomik ifadeler + +`&&` ve `||` VM'de kısa devre (short-circuit) çalışır; sağ taraf sadece gerektiğinde değerlendirilir (bkz. compiler.md). ### Postfix Kuralı diff --git a/docs/vm.md b/docs/vm.md index e76ef4e..a96cd5e 100644 --- a/docs/vm.md +++ b/docs/vm.md @@ -134,18 +134,19 @@ struct VM<'a> { ### Kontrol Akışı - **`handle_jump()`** — 4-byte adres oku, reader pozisyonunu ayarla +- **`handle_jump_if_true()`** — `||` short-circuit için; register true ise atla, Boolean değilse `TypeMismatch` - **`handle_jump_if_false()`** — Register `Boolean(false)` ise atla ### String, Nesne ve Metodlar - **`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_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. `.length` property'si String/NumberList/StringList'te ve elemanları Object olmayan List'te `.length()` ile eşdeğerdir (Object listesinde `length` alanı projection'ı önceliklidir) - **`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)` + - **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)`, `sum`/`avg`/`min`/`max` (elemanların hepsi Number ise; boş liste → sum 0, diğerleri null. Boş projection `items.amount` List döndürdüğü için gerekli) - **Harici metodlar:** Yukarıdaki built-in metodlar bulunamazsa `external_methods` HashMap'inde aranır ### Üyelik Testi @@ -163,7 +164,7 @@ struct VM<'a> { - **`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ı) +- **`round(n[, places])`** — Yuvarlama (opsiyonel ondalık basamak sayısı). Yarımlar sıfırdan uzağa (`MidpointAwayFromZero`): `round(2.5)` = 3, `round(-2.5)` = -3, `round(0.125, 2)` = 0.13 - **`sqrt(n)`** — Karekök - **`len(v)`** — Değerin uzunluğu (String, List, Object) - **`toString(v)`** — Değeri String'e dönüştür diff --git a/editor/package.json b/editor/package.json index aed6138..9ef1dd5 100644 --- a/editor/package.json +++ b/editor/package.json @@ -1,6 +1,6 @@ { "name": "@duhanbalci/codemirror-lang-dexpr", - "version": "0.4.0", + "version": "0.4.1", "description": "CodeMirror 6 language support for dexpr", "type": "module", "main": "dist/index.cjs", diff --git a/src/bytecode_dump.rs b/src/bytecode_dump.rs index afe0ece..05fd12c 100644 --- a/src/bytecode_dump.rs +++ b/src/bytecode_dump.rs @@ -1,235 +1,243 @@ -use crate::bytecode::BytecodeReader; -use crate::opcodes::OpCodeByte; - -/// A utility function to disassemble bytecode for debugging -pub fn disassemble_bytecode(bytecode: &[u8]) -> Vec { - let mut result = Vec::new(); - let mut reader = BytecodeReader::new(bytecode); - - while reader.remaining() > 0 { - let start_position = reader.position(); - let opcode_byte = match reader.read_byte() { - Ok(b) => b, - Err(_) => break, - }; - - let opcode = match OpCodeByte::from_byte(opcode_byte) { - Some(op) => op, - None => { - result.push(format!( - "{:04x}: Unknown opcode: 0x{:02x}", - start_position, opcode_byte - )); - continue; - } - }; - - let instruction = match opcode { - OpCodeByte::LoadConst => { - let reg = reader.read_byte(); - let value = reader.read_value(); - match (reg, value) { - (Ok(r), Ok(v)) => format!("{:04x}: LoadConst r{}, {}", start_position, r, v), - _ => format!("{:04x}: LoadConst (truncated)", start_position), - } - } - OpCodeByte::Move => { - let dest = reader.read_byte(); - let src = reader.read_byte(); - match (dest, src) { - (Ok(d), Ok(s)) => format!("{:04x}: Move r{} = r{}", start_position, d, s), - _ => format!("{:04x}: Move (truncated)", start_position), - } - } - OpCodeByte::LoadLocal => { - let reg = reader.read_byte(); - let offset = reader.read_byte(); - match (reg, offset) { - (Ok(r), Ok(o)) => format!("{:04x}: LoadLocal r{}, offset={}", start_position, r, o), - _ => format!("{:04x}: LoadLocal (truncated)", start_position), - } - } - OpCodeByte::StoreLocal => { - let offset = reader.read_byte(); - let reg = reader.read_byte(); - match (offset, reg) { - (Ok(o), Ok(r)) => format!("{:04x}: StoreLocal offset={}, r{}", start_position, o, r), - _ => format!("{:04x}: StoreLocal (truncated)", start_position), - } - } - OpCodeByte::LoadGlobal => { - let reg = reader.read_byte(); - let name = reader.read_string(); - match (reg, name) { - (Ok(r), Ok(n)) => format!("{:04x}: LoadGlobal r{}, \"{}\"", start_position, r, n), - _ => format!("{:04x}: LoadGlobal (truncated)", start_position), - } - } - OpCodeByte::StoreGlobal => { - let name = reader.read_string(); - let reg = reader.read_byte(); - match (name, reg) { - (Ok(n), Ok(r)) => format!("{:04x}: StoreGlobal \"{}\", r{}", start_position, n, r), - _ => format!("{:04x}: StoreGlobal (truncated)", start_position), - } - } - OpCodeByte::Add - | OpCodeByte::Sub - | OpCodeByte::Mul - | OpCodeByte::Div - | OpCodeByte::Mod - | OpCodeByte::Pow - | OpCodeByte::Lt - | OpCodeByte::Lte - | OpCodeByte::Gt - | OpCodeByte::Gte - | OpCodeByte::Eq - | OpCodeByte::Neq - | OpCodeByte::And - | OpCodeByte::Or - | OpCodeByte::Contains - | OpCodeByte::Concat => { - let res = reader.read_byte(); - let left = reader.read_byte(); - let right = reader.read_byte(); - match (res, left, right) { - (Ok(r), Ok(l), Ok(rg)) => { - format!("{:04x}: {:?} r{}, r{}, r{}", start_position, opcode, r, l, rg) - } - _ => format!("{:04x}: {:?} (truncated)", start_position, opcode), - } - } - OpCodeByte::Neg | OpCodeByte::Not => { - let res = reader.read_byte(); - let operand = reader.read_byte(); - match (res, operand) { - (Ok(r), Ok(o)) => format!("{:04x}: {:?} r{}, r{}", start_position, opcode, r, o), - _ => format!("{:04x}: {:?} (truncated)", start_position, opcode), - } - } - OpCodeByte::Jump => match reader.read_u32() { - Ok(addr) => format!("{:04x}: Jump -> 0x{:04x}", start_position, addr), - Err(_) => format!("{:04x}: Jump (truncated)", start_position), - }, - OpCodeByte::JumpIfFalse => { - let reg = reader.read_byte(); - let addr = reader.read_u32(); - match (reg, addr) { - (Ok(r), Ok(a)) => format!("{:04x}: JumpIfFalse r{} -> 0x{:04x}", start_position, r, a), - _ => format!("{:04x}: JumpIfFalse (truncated)", start_position), - } - } - OpCodeByte::MethodCall => { - let res = reader.read_byte(); - let obj = reader.read_byte(); - let method = reader.read_string(); - let arg_count = reader.read_byte(); - match (res, obj, method, arg_count) { - (Ok(r), Ok(o), Ok(m), Ok(count)) => { - let mut arg_regs = Vec::new(); - let mut truncated = false; - for _ in 0..count { - match reader.read_byte() { - Ok(reg) => arg_regs.push(format!("r{}", reg)), - Err(_) => { - truncated = true; - break; - } - } - } - if truncated { - format!( - "{:04x}: MethodCall r{} = r{}.{}(truncated args)", - start_position, r, o, m - ) - } else { - format!( - "{:04x}: MethodCall r{} = r{}.{}({})", - start_position, - r, - o, - m, - arg_regs.join(", ") - ) - } - } - _ => format!("{:04x}: MethodCall (truncated)", start_position), - } - } - OpCodeByte::Log => match reader.read_byte() { - Ok(reg) => format!("{:04x}: Log r{}", start_position, reg), - Err(_) => format!("{:04x}: Log (truncated)", start_position), - }, - OpCodeByte::CallDefault => { - let res = reader.read_byte(); - let fn_id = reader.read_byte(); - let arg_count = reader.read_byte(); - match (res, fn_id, arg_count) { - (Ok(r), Ok(id), Ok(count)) => { - let fn_name = crate::opcodes::default_fn::name(id).unwrap_or("?"); - let mut arg_regs = Vec::new(); - for _ in 0..count { - if let Ok(reg) = reader.read_byte() { - arg_regs.push(format!("r{}", reg)); - } - } - format!( - "{:04x}: CallDefault r{} = {}({})", - start_position, r, fn_name, arg_regs.join(", ") - ) - } - _ => format!("{:04x}: CallDefault (truncated)", start_position), - } - } - OpCodeByte::CallExternal => { - let res = reader.read_byte(); - let name = reader.read_string(); - let arg_count = reader.read_byte(); - match (res, name, arg_count) { - (Ok(r), Ok(n), Ok(count)) => { - let mut arg_regs = Vec::new(); - for _ in 0..count { - if let Ok(reg) = reader.read_byte() { - arg_regs.push(format!("r{}", reg)); - } - } - format!( - "{:04x}: CallExternal r{} = {}({})", - start_position, r, n, arg_regs.join(", ") - ) - } - _ => format!("{:04x}: CallExternal (truncated)", start_position), - } - } - OpCodeByte::GetProperty => { - let dest = reader.read_byte(); - let obj = reader.read_byte(); - let prop = reader.read_string(); - match (dest, obj, prop) { - (Ok(d), Ok(o), Ok(p)) => format!("{:04x}: GetProperty r{} = r{}.{}", start_position, d, o, p), - _ => format!("{:04x}: GetProperty (truncated)", start_position), - } - } - OpCodeByte::SetProperty => { - let obj = reader.read_byte(); - let prop = reader.read_string(); - let val = reader.read_byte(); - match (obj, prop, val) { - (Ok(o), Ok(p), Ok(v)) => format!("{:04x}: SetProperty r{}.{} = r{}", start_position, o, p, v), - _ => format!("{:04x}: SetProperty (truncated)", start_position), - } - } - OpCodeByte::SetResult => match reader.read_byte() { - 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), - }; - - result.push(instruction); - } - - result -} - +use crate::bytecode::BytecodeReader; +use crate::opcodes::OpCodeByte; + +/// A utility function to disassemble bytecode for debugging +pub fn disassemble_bytecode(bytecode: &[u8]) -> Vec { + let mut result = Vec::new(); + let mut reader = BytecodeReader::new(bytecode); + + while reader.remaining() > 0 { + let start_position = reader.position(); + let opcode_byte = match reader.read_byte() { + Ok(b) => b, + Err(_) => break, + }; + + let opcode = match OpCodeByte::from_byte(opcode_byte) { + Some(op) => op, + None => { + result.push(format!( + "{:04x}: Unknown opcode: 0x{:02x}", + start_position, opcode_byte + )); + continue; + } + }; + + let instruction = match opcode { + OpCodeByte::LoadConst => { + let reg = reader.read_byte(); + let value = reader.read_value(); + match (reg, value) { + (Ok(r), Ok(v)) => format!("{:04x}: LoadConst r{}, {}", start_position, r, v), + _ => format!("{:04x}: LoadConst (truncated)", start_position), + } + } + OpCodeByte::Move => { + let dest = reader.read_byte(); + let src = reader.read_byte(); + match (dest, src) { + (Ok(d), Ok(s)) => format!("{:04x}: Move r{} = r{}", start_position, d, s), + _ => format!("{:04x}: Move (truncated)", start_position), + } + } + OpCodeByte::LoadLocal => { + let reg = reader.read_byte(); + let offset = reader.read_byte(); + match (reg, offset) { + (Ok(r), Ok(o)) => format!("{:04x}: LoadLocal r{}, offset={}", start_position, r, o), + _ => format!("{:04x}: LoadLocal (truncated)", start_position), + } + } + OpCodeByte::StoreLocal => { + let offset = reader.read_byte(); + let reg = reader.read_byte(); + match (offset, reg) { + (Ok(o), Ok(r)) => format!("{:04x}: StoreLocal offset={}, r{}", start_position, o, r), + _ => format!("{:04x}: StoreLocal (truncated)", start_position), + } + } + OpCodeByte::LoadGlobal => { + let reg = reader.read_byte(); + let name = reader.read_string(); + match (reg, name) { + (Ok(r), Ok(n)) => format!("{:04x}: LoadGlobal r{}, \"{}\"", start_position, r, n), + _ => format!("{:04x}: LoadGlobal (truncated)", start_position), + } + } + OpCodeByte::StoreGlobal => { + let name = reader.read_string(); + let reg = reader.read_byte(); + match (name, reg) { + (Ok(n), Ok(r)) => format!("{:04x}: StoreGlobal \"{}\", r{}", start_position, n, r), + _ => format!("{:04x}: StoreGlobal (truncated)", start_position), + } + } + OpCodeByte::Add + | OpCodeByte::Sub + | OpCodeByte::Mul + | OpCodeByte::Div + | OpCodeByte::Mod + | OpCodeByte::Pow + | OpCodeByte::Lt + | OpCodeByte::Lte + | OpCodeByte::Gt + | OpCodeByte::Gte + | OpCodeByte::Eq + | OpCodeByte::Neq + | OpCodeByte::And + | OpCodeByte::Or + | OpCodeByte::Contains + | OpCodeByte::Concat => { + let res = reader.read_byte(); + let left = reader.read_byte(); + let right = reader.read_byte(); + match (res, left, right) { + (Ok(r), Ok(l), Ok(rg)) => { + format!("{:04x}: {:?} r{}, r{}, r{}", start_position, opcode, r, l, rg) + } + _ => format!("{:04x}: {:?} (truncated)", start_position, opcode), + } + } + OpCodeByte::Neg | OpCodeByte::Not => { + let res = reader.read_byte(); + let operand = reader.read_byte(); + match (res, operand) { + (Ok(r), Ok(o)) => format!("{:04x}: {:?} r{}, r{}", start_position, opcode, r, o), + _ => format!("{:04x}: {:?} (truncated)", start_position, opcode), + } + } + OpCodeByte::Jump => match reader.read_u32() { + Ok(addr) => format!("{:04x}: Jump -> 0x{:04x}", start_position, addr), + Err(_) => format!("{:04x}: Jump (truncated)", start_position), + }, + OpCodeByte::JumpIfFalse => { + let reg = reader.read_byte(); + let addr = reader.read_u32(); + match (reg, addr) { + (Ok(r), Ok(a)) => format!("{:04x}: JumpIfFalse r{} -> 0x{:04x}", start_position, r, a), + _ => format!("{:04x}: JumpIfFalse (truncated)", start_position), + } + } + OpCodeByte::JumpIfTrue => { + let reg = reader.read_byte(); + let addr = reader.read_u32(); + match (reg, addr) { + (Ok(r), Ok(a)) => format!("{:04x}: JumpIfTrue r{} -> 0x{:04x}", start_position, r, a), + _ => format!("{:04x}: JumpIfTrue (truncated)", start_position), + } + } + OpCodeByte::MethodCall => { + let res = reader.read_byte(); + let obj = reader.read_byte(); + let method = reader.read_string(); + let arg_count = reader.read_byte(); + match (res, obj, method, arg_count) { + (Ok(r), Ok(o), Ok(m), Ok(count)) => { + let mut arg_regs = Vec::new(); + let mut truncated = false; + for _ in 0..count { + match reader.read_byte() { + Ok(reg) => arg_regs.push(format!("r{}", reg)), + Err(_) => { + truncated = true; + break; + } + } + } + if truncated { + format!( + "{:04x}: MethodCall r{} = r{}.{}(truncated args)", + start_position, r, o, m + ) + } else { + format!( + "{:04x}: MethodCall r{} = r{}.{}({})", + start_position, + r, + o, + m, + arg_regs.join(", ") + ) + } + } + _ => format!("{:04x}: MethodCall (truncated)", start_position), + } + } + OpCodeByte::Log => match reader.read_byte() { + Ok(reg) => format!("{:04x}: Log r{}", start_position, reg), + Err(_) => format!("{:04x}: Log (truncated)", start_position), + }, + OpCodeByte::CallDefault => { + let res = reader.read_byte(); + let fn_id = reader.read_byte(); + let arg_count = reader.read_byte(); + match (res, fn_id, arg_count) { + (Ok(r), Ok(id), Ok(count)) => { + let fn_name = crate::opcodes::default_fn::name(id).unwrap_or("?"); + let mut arg_regs = Vec::new(); + for _ in 0..count { + if let Ok(reg) = reader.read_byte() { + arg_regs.push(format!("r{}", reg)); + } + } + format!( + "{:04x}: CallDefault r{} = {}({})", + start_position, r, fn_name, arg_regs.join(", ") + ) + } + _ => format!("{:04x}: CallDefault (truncated)", start_position), + } + } + OpCodeByte::CallExternal => { + let res = reader.read_byte(); + let name = reader.read_string(); + let arg_count = reader.read_byte(); + match (res, name, arg_count) { + (Ok(r), Ok(n), Ok(count)) => { + let mut arg_regs = Vec::new(); + for _ in 0..count { + if let Ok(reg) = reader.read_byte() { + arg_regs.push(format!("r{}", reg)); + } + } + format!( + "{:04x}: CallExternal r{} = {}({})", + start_position, r, n, arg_regs.join(", ") + ) + } + _ => format!("{:04x}: CallExternal (truncated)", start_position), + } + } + OpCodeByte::GetProperty => { + let dest = reader.read_byte(); + let obj = reader.read_byte(); + let prop = reader.read_string(); + match (dest, obj, prop) { + (Ok(d), Ok(o), Ok(p)) => format!("{:04x}: GetProperty r{} = r{}.{}", start_position, d, o, p), + _ => format!("{:04x}: GetProperty (truncated)", start_position), + } + } + OpCodeByte::SetProperty => { + let obj = reader.read_byte(); + let prop = reader.read_string(); + let val = reader.read_byte(); + match (obj, prop, val) { + (Ok(o), Ok(p), Ok(v)) => format!("{:04x}: SetProperty r{}.{} = r{}", start_position, o, p, v), + _ => format!("{:04x}: SetProperty (truncated)", start_position), + } + } + OpCodeByte::SetResult => match reader.read_byte() { + 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), + }; + + result.push(instruction); + } + + result +} + diff --git a/src/compiler.rs b/src/compiler.rs index 585c826..2cca80a 100644 --- a/src/compiler.rs +++ b/src/compiler.rs @@ -316,6 +316,10 @@ impl Compiler { op: &Op, right: &Expr, ) -> Result { + if matches!(op, Op::And | Op::Or) { + return self.compile_logical_op(left, op, right); + } + let left_reg = self.compile_expr(left)?; let right_reg = self.compile_expr(right)?; let result_reg = self.allocate_register()?; @@ -355,6 +359,49 @@ impl Compiler { Ok(result_reg) } + /// Compile `&&` / `||` with short-circuit evaluation. + /// + /// Layout (`&&`): + /// ```text + /// -> rL + /// JumpIfFalse rL, END ; left false → result is rL (false), skip right + /// -> rR + /// Move rL, rR ; (omitted when rR == rL) + /// END: + /// ``` + /// `||` is identical with `JumpIfTrue`. The result always lives in `rL`. + /// The left register is released before compiling the right side because its + /// value is only needed on the jump path, where nothing else executes; this + /// lets the right side usually land in the same register and skip the Move. + fn compile_logical_op(&mut self, left: &Expr, op: &Op, right: &Expr) -> Result { + let left_reg = self.compile_expr(left)?; + let end_label = self.create_label(); + + let jump_op = if matches!(op, Op::And) { + OpCodeByte::JumpIfFalse + } else { + OpCodeByte::JumpIfTrue + }; + self.emit_byte(jump_op.to_byte()); + self.emit_byte(left_reg); + self.emit_jump_address(end_label); + + self.free_register(left_reg); + let right_reg = self.compile_expr(right)?; + if right_reg != left_reg { + self.emit_byte(OpCodeByte::Move.to_byte()); + self.emit_byte(left_reg); + self.emit_byte(right_reg); + self.free_register(right_reg); + } + // Re-claim left_reg as the result register (it may have been reused/freed + // while compiling the right side). + self.used_registers[left_reg as usize] = true; + + self.set_label(end_label); + Ok(left_reg) + } + /// Compile a unary operation fn compile_unary_op(&mut self, op: &Op, operand: &Expr) -> Result { let operand_reg = self.compile_expr(operand)?; diff --git a/src/language_info.rs b/src/language_info.rs index a6d66eb..cf5a17c 100644 --- a/src/language_info.rs +++ b/src/language_info.rs @@ -305,6 +305,10 @@ fn builtin_methods() -> Vec<(&'static str, Vec)> { 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") }, + MethodInfo { name: "sum", signature: "() -> Number", doc: Some("Sum of numeric elements (0 when empty)") }, + MethodInfo { name: "avg", signature: "() -> Number", doc: Some("Average of numeric elements") }, + MethodInfo { name: "min", signature: "() -> Number", doc: Some("Smallest numeric element") }, + MethodInfo { name: "max", signature: "() -> Number", doc: Some("Largest numeric element") }, ]), ("StringList", vec![ MethodInfo { name: "length", signature: "() -> Number", doc: None }, diff --git a/src/opcodes.rs b/src/opcodes.rs index 0a2e717..26637e4 100644 --- a/src/opcodes.rs +++ b/src/opcodes.rs @@ -1,208 +1,211 @@ -/// Register identifier -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct Register(pub u8); - -/// Default (built-in) function IDs for CallDefault opcode -pub mod default_fn { - pub const RAND: u8 = 0; - 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), - ("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> { - NAMES.iter().find(|(_, i)| *i == id).map(|(n, _)| *n) - } - - /// Get function ID by name - pub fn id(name: &str) -> Option { - NAMES.iter().find(|(n, _)| *n == name).map(|(_, i)| *i) - } -} - -/// Bytecode opcodes -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum OpCodeByte { - // Register operations - LoadConst = 0x10, // Load constant to register - Move = 0x11, // Move value between registers - - // Memory operations - LoadLocal = 0x20, // Load local variable to register - StoreLocal = 0x21, // Store register to local variable - LoadGlobal = 0x22, // Load global variable to register - StoreGlobal = 0x23, // Store register to global variable - - // Arithmetic operations - Add = 0x30, // Addition - Sub = 0x31, // Subtraction - Mul = 0x32, // Multiplication - Div = 0x33, // Division - Neg = 0x34, // Negation - Mod = 0x35, // Modulo - Pow = 0x36, // Power - - // Comparison operations - Lt = 0x40, // Less than - Lte = 0x41, // Less than or equal - Gt = 0x42, // Greater than - Gte = 0x43, // Greater than or equal - Eq = 0x44, // Equal - Neq = 0x45, // Not equal - - // Boolean operations - And = 0x50, // Logical AND - Or = 0x51, // Logical OR - Not = 0x52, // Logical NOT - - // Membership test - Contains = 0x53, // Check if value is in list/string - - // Control flow - Jump = 0x60, // Should read 4-byte address - JumpIfFalse = 0x61, // Should read register + 4-byte address - - // String operations - Concat = 0x80, // String concatenation - - // Property & method calls - GetProperty = 0x91, // Get object property: dest, obj, name - SetProperty = 0x92, // Set object property: obj, name, value - MethodCall = 0x90, // Call method on object - - // Built-in functions - Log = 0xA0, // Print a value - CallExternal = 0xA1, // Call external (host) function - CallDefault = 0xA2, // Call default (built-in) function by ID - - // Result - SetResult = 0xB0, // Set expression result (for return value) - ClearResult = 0xB1, // Clear expression result (assignment resets last result) - - // End marker - End = 0xFF, // End of program -} - -impl OpCodeByte { - /// Convert opcode to byte - pub fn to_byte(self) -> u8 { - self as u8 - } - - /// Static lookup table for fast byte to opcode conversion - const LOOKUP: [Option; 256] = { - let mut table = [None; 256]; - let mut i = 0; - while i < 256 { - table[i] = match i as u8 { - 0x10 => Some(OpCodeByte::LoadConst), - 0x11 => Some(OpCodeByte::Move), - 0x20 => Some(OpCodeByte::LoadLocal), - 0x21 => Some(OpCodeByte::StoreLocal), - 0x22 => Some(OpCodeByte::LoadGlobal), - 0x23 => Some(OpCodeByte::StoreGlobal), - 0x30 => Some(OpCodeByte::Add), - 0x31 => Some(OpCodeByte::Sub), - 0x32 => Some(OpCodeByte::Mul), - 0x33 => Some(OpCodeByte::Div), - 0x34 => Some(OpCodeByte::Neg), - 0x35 => Some(OpCodeByte::Mod), - 0x36 => Some(OpCodeByte::Pow), - 0x40 => Some(OpCodeByte::Lt), - 0x41 => Some(OpCodeByte::Lte), - 0x42 => Some(OpCodeByte::Gt), - 0x43 => Some(OpCodeByte::Gte), - 0x44 => Some(OpCodeByte::Eq), - 0x45 => Some(OpCodeByte::Neq), - 0x50 => Some(OpCodeByte::And), - 0x51 => Some(OpCodeByte::Or), - 0x52 => Some(OpCodeByte::Not), - 0x53 => Some(OpCodeByte::Contains), - 0x60 => Some(OpCodeByte::Jump), - 0x61 => Some(OpCodeByte::JumpIfFalse), - 0x80 => Some(OpCodeByte::Concat), - 0x90 => Some(OpCodeByte::MethodCall), - 0x91 => Some(OpCodeByte::GetProperty), - 0x92 => Some(OpCodeByte::SetProperty), - 0xA0 => Some(OpCodeByte::Log), - 0xA1 => Some(OpCodeByte::CallExternal), - 0xA2 => Some(OpCodeByte::CallDefault), - 0xB0 => Some(OpCodeByte::SetResult), - 0xB1 => Some(OpCodeByte::ClearResult), - 0xFF => Some(OpCodeByte::End), - _ => None, - }; - i += 1; - } - table - }; - - /// Convert byte to opcode - #[inline(always)] - pub fn from_byte(byte: u8) -> Option { - Self::LOOKUP[byte as usize] - } - - /// Get opcode name - pub fn name(&self) -> &'static str { - match self { - OpCodeByte::LoadConst => "LoadConst", - OpCodeByte::Move => "Move", - OpCodeByte::LoadLocal => "LoadLocal", - OpCodeByte::StoreLocal => "StoreLocal", - OpCodeByte::LoadGlobal => "LoadGlobal", - OpCodeByte::StoreGlobal => "StoreGlobal", - OpCodeByte::Add => "Add", - OpCodeByte::Sub => "Sub", - OpCodeByte::Mul => "Mul", - OpCodeByte::Div => "Div", - OpCodeByte::Neg => "Neg", - OpCodeByte::Mod => "Mod", - OpCodeByte::Pow => "Pow", - OpCodeByte::Lt => "Lt", - OpCodeByte::Lte => "Lte", - OpCodeByte::Gt => "Gt", - OpCodeByte::Gte => "Gte", - OpCodeByte::Eq => "Eq", - OpCodeByte::Neq => "Neq", - OpCodeByte::And => "And", - OpCodeByte::Or => "Or", - OpCodeByte::Not => "Not", - OpCodeByte::Contains => "Contains", - OpCodeByte::Jump => "Jump", - OpCodeByte::JumpIfFalse => "JumpIfFalse", - OpCodeByte::Concat => "Concat", - OpCodeByte::MethodCall => "MethodCall", - OpCodeByte::GetProperty => "GetProperty", - OpCodeByte::SetProperty => "SetProperty", - OpCodeByte::Log => "Log", - OpCodeByte::CallExternal => "CallExternal", - OpCodeByte::CallDefault => "Rand", - OpCodeByte::SetResult => "SetResult", - OpCodeByte::ClearResult => "ClearResult", - OpCodeByte::End => "End", - } - } -} +/// Register identifier +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct Register(pub u8); + +/// Default (built-in) function IDs for CallDefault opcode +pub mod default_fn { + pub const RAND: u8 = 0; + 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), + ("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> { + NAMES.iter().find(|(_, i)| *i == id).map(|(n, _)| *n) + } + + /// Get function ID by name + pub fn id(name: &str) -> Option { + NAMES.iter().find(|(n, _)| *n == name).map(|(_, i)| *i) + } +} + +/// Bytecode opcodes +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum OpCodeByte { + // Register operations + LoadConst = 0x10, // Load constant to register + Move = 0x11, // Move value between registers + + // Memory operations + LoadLocal = 0x20, // Load local variable to register + StoreLocal = 0x21, // Store register to local variable + LoadGlobal = 0x22, // Load global variable to register + StoreGlobal = 0x23, // Store register to global variable + + // Arithmetic operations + Add = 0x30, // Addition + Sub = 0x31, // Subtraction + Mul = 0x32, // Multiplication + Div = 0x33, // Division + Neg = 0x34, // Negation + Mod = 0x35, // Modulo + Pow = 0x36, // Power + + // Comparison operations + Lt = 0x40, // Less than + Lte = 0x41, // Less than or equal + Gt = 0x42, // Greater than + Gte = 0x43, // Greater than or equal + Eq = 0x44, // Equal + Neq = 0x45, // Not equal + + // Boolean operations + And = 0x50, // Logical AND + Or = 0x51, // Logical OR + Not = 0x52, // Logical NOT + + // Membership test + Contains = 0x53, // Check if value is in list/string + + // Control flow + Jump = 0x60, // Should read 4-byte address + JumpIfFalse = 0x61, // Should read register + 4-byte address + JumpIfTrue = 0x62, // Should read register + 4-byte address + + // String operations + Concat = 0x80, // String concatenation + + // Property & method calls + GetProperty = 0x91, // Get object property: dest, obj, name + SetProperty = 0x92, // Set object property: obj, name, value + MethodCall = 0x90, // Call method on object + + // Built-in functions + Log = 0xA0, // Print a value + CallExternal = 0xA1, // Call external (host) function + CallDefault = 0xA2, // Call default (built-in) function by ID + + // Result + SetResult = 0xB0, // Set expression result (for return value) + ClearResult = 0xB1, // Clear expression result (assignment resets last result) + + // End marker + End = 0xFF, // End of program +} + +impl OpCodeByte { + /// Convert opcode to byte + pub fn to_byte(self) -> u8 { + self as u8 + } + + /// Static lookup table for fast byte to opcode conversion + const LOOKUP: [Option; 256] = { + let mut table = [None; 256]; + let mut i = 0; + while i < 256 { + table[i] = match i as u8 { + 0x10 => Some(OpCodeByte::LoadConst), + 0x11 => Some(OpCodeByte::Move), + 0x20 => Some(OpCodeByte::LoadLocal), + 0x21 => Some(OpCodeByte::StoreLocal), + 0x22 => Some(OpCodeByte::LoadGlobal), + 0x23 => Some(OpCodeByte::StoreGlobal), + 0x30 => Some(OpCodeByte::Add), + 0x31 => Some(OpCodeByte::Sub), + 0x32 => Some(OpCodeByte::Mul), + 0x33 => Some(OpCodeByte::Div), + 0x34 => Some(OpCodeByte::Neg), + 0x35 => Some(OpCodeByte::Mod), + 0x36 => Some(OpCodeByte::Pow), + 0x40 => Some(OpCodeByte::Lt), + 0x41 => Some(OpCodeByte::Lte), + 0x42 => Some(OpCodeByte::Gt), + 0x43 => Some(OpCodeByte::Gte), + 0x44 => Some(OpCodeByte::Eq), + 0x45 => Some(OpCodeByte::Neq), + 0x50 => Some(OpCodeByte::And), + 0x51 => Some(OpCodeByte::Or), + 0x52 => Some(OpCodeByte::Not), + 0x53 => Some(OpCodeByte::Contains), + 0x60 => Some(OpCodeByte::Jump), + 0x61 => Some(OpCodeByte::JumpIfFalse), + 0x62 => Some(OpCodeByte::JumpIfTrue), + 0x80 => Some(OpCodeByte::Concat), + 0x90 => Some(OpCodeByte::MethodCall), + 0x91 => Some(OpCodeByte::GetProperty), + 0x92 => Some(OpCodeByte::SetProperty), + 0xA0 => Some(OpCodeByte::Log), + 0xA1 => Some(OpCodeByte::CallExternal), + 0xA2 => Some(OpCodeByte::CallDefault), + 0xB0 => Some(OpCodeByte::SetResult), + 0xB1 => Some(OpCodeByte::ClearResult), + 0xFF => Some(OpCodeByte::End), + _ => None, + }; + i += 1; + } + table + }; + + /// Convert byte to opcode + #[inline(always)] + pub fn from_byte(byte: u8) -> Option { + Self::LOOKUP[byte as usize] + } + + /// Get opcode name + pub fn name(&self) -> &'static str { + match self { + OpCodeByte::LoadConst => "LoadConst", + OpCodeByte::Move => "Move", + OpCodeByte::LoadLocal => "LoadLocal", + OpCodeByte::StoreLocal => "StoreLocal", + OpCodeByte::LoadGlobal => "LoadGlobal", + OpCodeByte::StoreGlobal => "StoreGlobal", + OpCodeByte::Add => "Add", + OpCodeByte::Sub => "Sub", + OpCodeByte::Mul => "Mul", + OpCodeByte::Div => "Div", + OpCodeByte::Neg => "Neg", + OpCodeByte::Mod => "Mod", + OpCodeByte::Pow => "Pow", + OpCodeByte::Lt => "Lt", + OpCodeByte::Lte => "Lte", + OpCodeByte::Gt => "Gt", + OpCodeByte::Gte => "Gte", + OpCodeByte::Eq => "Eq", + OpCodeByte::Neq => "Neq", + OpCodeByte::And => "And", + OpCodeByte::Or => "Or", + OpCodeByte::Not => "Not", + OpCodeByte::Contains => "Contains", + OpCodeByte::Jump => "Jump", + OpCodeByte::JumpIfFalse => "JumpIfFalse", + OpCodeByte::JumpIfTrue => "JumpIfTrue", + OpCodeByte::Concat => "Concat", + OpCodeByte::MethodCall => "MethodCall", + OpCodeByte::GetProperty => "GetProperty", + OpCodeByte::SetProperty => "SetProperty", + OpCodeByte::Log => "Log", + OpCodeByte::CallExternal => "CallExternal", + OpCodeByte::CallDefault => "Rand", + OpCodeByte::SetResult => "SetResult", + OpCodeByte::ClearResult => "ClearResult", + OpCodeByte::End => "End", + } + } +} diff --git a/src/parser/grammar.rs b/src/parser/grammar.rs index d7fbb71..c679f3d 100644 --- a/src/parser/grammar.rs +++ b/src/parser/grammar.rs @@ -41,8 +41,8 @@ pub grammar parser() for str { = binary_op() pub rule mul_div() -> Expr = - left:power() mul_div_right:( - _ op:$("*" / "/" / "%") _ right:power() + left:unary() mul_div_right:( + _ op:$("*" / "/" / "%") _ right:unary() { (op, right) } )* { let mut result = left; @@ -57,31 +57,39 @@ pub grammar parser() for str { result } + /// Unary operators bind looser than postfix (`.prop`, `.method()`) and `**`, + /// so `!o.active` == `!(o.active)` and `-2 ** 2` == `-(2 ** 2)`. + pub rule unary() -> Expr + = "-" _ e:unary() { Expr::UnaryOp(Op::Neg, Box::new(e)) } + / "!" _ e:unary() { Expr::UnaryOp(Op::Not, Box::new(e)) } + / power() + pub rule power() -> Expr = - base:postfix() _ "**" _ exp:power() { Expr::BinaryOp(Box::new(base), Op::Pow, Box::new(exp)) } + base:postfix() _ "**" _ exp:unary() { Expr::BinaryOp(Box::new(base), Op::Pow, Box::new(exp)) } / a:postfix() { a } pub rule binary_op() -> Expr = precedence!{ i:identifier() _ "(" args:((_ e:expression() _ {e}) ** ",") ")" { Expr::FunctionCall(i, args) } -- - x:@ _ "&&" _ y:(@) { Expr::BinaryOp(Box::new(x), Op::And, Box::new(y)) } - x:@ _ "||" _ y:(@) { Expr::BinaryOp(Box::new(x), Op::Or, Box::new(y)) } + x:(@) _ "||" _ y:@ { Expr::BinaryOp(Box::new(x), Op::Or, Box::new(y)) } -- - x:@ _ "==" _ y:(@) { Expr::BinaryOp(Box::new(x), Op::Eq, Box::new(y)) } - x:@ _ "!=" _ y:(@) { Expr::BinaryOp(Box::new(x), Op::Neq, Box::new(y)) } - x:@ _ "<" _ y:(@) { Expr::BinaryOp(Box::new(x), Op::Lt, Box::new(y)) } - x:@ _ "<=" _ y:(@) { Expr::BinaryOp(Box::new(x), Op::Lte, Box::new(y)) } - x:@ _ ">" _ y:(@) { Expr::BinaryOp(Box::new(x), Op::Gt, Box::new(y)) } - x:@ _ ">=" _ y:(@) { Expr::BinaryOp(Box::new(x), Op::Gte, Box::new(y)) } - x:@ _ "in" _ y:(@) { Expr::BinaryOp(Box::new(x), Op::In, Box::new(y)) } + x:(@) _ "&&" _ y:@ { Expr::BinaryOp(Box::new(x), Op::And, Box::new(y)) } -- - x:@ _ "+" _ y:(@) { Expr::BinaryOp(Box::new(x),Op::Add, Box::new(y)) } - x:@ _ "-" _ y:(@) { Expr::BinaryOp(Box::new(x),Op::Sub, Box::new(y)) } + x:(@) _ "==" _ y:@ { Expr::BinaryOp(Box::new(x), Op::Eq, Box::new(y)) } + x:(@) _ "!=" _ y:@ { Expr::BinaryOp(Box::new(x), Op::Neq, Box::new(y)) } + x:(@) _ "<=" _ y:@ { Expr::BinaryOp(Box::new(x), Op::Lte, Box::new(y)) } + x:(@) _ ">=" _ y:@ { Expr::BinaryOp(Box::new(x), Op::Gte, Box::new(y)) } + x:(@) _ "<" _ y:@ { Expr::BinaryOp(Box::new(x), Op::Lt, Box::new(y)) } + x:(@) _ ">" _ y:@ { Expr::BinaryOp(Box::new(x), Op::Gt, Box::new(y)) } + x:(@) _ "in" _ y:@ { Expr::BinaryOp(Box::new(x), Op::In, Box::new(y)) } + -- + x:(@) _ "+" _ y:@ { Expr::BinaryOp(Box::new(x),Op::Add, Box::new(y)) } + x:(@) _ "-" _ y:@ { Expr::BinaryOp(Box::new(x),Op::Sub, Box::new(y)) } -- x:mul_div() { x } -- - p:postfix() { p } + p:unary() { p } } /// Postfix operations: property access and method calls with chaining @@ -107,8 +115,6 @@ pub grammar parser() for str { / i:number() { Expr::Value(Value::Number(i)) } / i:boolean_literal() { Expr::Value(i) } / "(" e:expression() ")" { e } - / "-" e:atom() { Expr::UnaryOp(Op::Neg, Box::new(e)) } - / "!" e:atom() { Expr::UnaryOp(Op::Not, Box::new(e)) } pub rule string() -> SmolStr = "\"" s:$(([^'"'] / "\\\"")*) "\"" { @@ -197,40 +203,63 @@ mod tests { #[test] fn test_binary_op() { - let res = parser::binary_op("1 + 2 * 3 - 4 / 5"); - if let Err(e) = &res { - println!("{}", e); - } - if let Ok(expr) = res { - match expr { - Expr::BinaryOp(left, Op::Add, right) => { - assert!(matches!(*left, Expr::Value(_))); - match *right { - Expr::BinaryOp(left2, Op::Sub, right2) => { - // Check 2 * 3 - match *left2 { - Expr::BinaryOp(left3, Op::Mul, right3) => { - assert!(matches!(*left3, Expr::Value(_))); - assert!(matches!(*right3, Expr::Value(_))); - } - _ => panic!("Expected multiplication"), - } - // Check 4 / 5 - match *right2 { - Expr::BinaryOp(left3, Op::Div, right3) => { - assert!(matches!(*left3, Expr::Value(_))); - assert!(matches!(*right3, Expr::Value(_))); - } - _ => panic!("Expected division"), - } - } - _ => panic!("Expected subtraction"), + // Left-associative: ((1 + (2 * 3)) - (4 / 5)) + let expr = parser::binary_op("1 + 2 * 3 - 4 / 5").expect("Failed to parse expression"); + match expr { + Expr::BinaryOp(left, Op::Sub, right) => { + // right: 4 / 5 + match *right { + Expr::BinaryOp(l, Op::Div, r) => { + assert!(matches!(*l, Expr::Value(_))); + assert!(matches!(*r, Expr::Value(_))); } + _ => panic!("Expected division"), + } + // left: 1 + (2 * 3) + match *left { + Expr::BinaryOp(l, Op::Add, r) => { + assert!(matches!(*l, Expr::Value(_))); + match *r { + Expr::BinaryOp(l2, Op::Mul, r2) => { + assert!(matches!(*l2, Expr::Value(_))); + assert!(matches!(*r2, Expr::Value(_))); + } + _ => panic!("Expected multiplication"), + } + } + _ => panic!("Expected addition"), } - _ => panic!("Expected addition at top level"), } - } else { - panic!("Failed to parse expression"); + _ => panic!("Expected subtraction at top level"), + } + } + + #[test] + fn test_left_associativity() { + // 10 - 3 - 2 => (10 - 3) - 2 + match parser::binary_op("10 - 3 - 2").unwrap() { + Expr::BinaryOp(left, Op::Sub, right) => { + assert!(matches!(*right, Expr::Value(_))); + assert!(matches!(*left, Expr::BinaryOp(_, Op::Sub, _))); + } + _ => panic!("Expected subtraction at top level"), + } + // a && b || c => (a && b) || c + match parser::binary_op("true && false || true").unwrap() { + Expr::BinaryOp(left, Op::Or, _) => { + assert!(matches!(*left, Expr::BinaryOp(_, Op::And, _))); + } + _ => panic!("Expected || at top level"), + } + // !o.a => !(o.a) + match parser::binary_op("!o.a").unwrap() { + Expr::UnaryOp(Op::Not, inner) => assert!(matches!(*inner, Expr::PropertyAccess(_, _))), + _ => panic!("Expected unary not at top level"), + } + // -2 ** 2 => -(2 ** 2) + match parser::binary_op("-2 ** 2").unwrap() { + Expr::UnaryOp(Op::Neg, inner) => assert!(matches!(*inner, Expr::BinaryOp(_, Op::Pow, _))), + _ => panic!("Expected unary neg at top level"), } } diff --git a/src/vm/builtins.rs b/src/vm/builtins.rs index c4418c4..a2e5aa0 100644 --- a/src/vm/builtins.rs +++ b/src/vm/builtins.rs @@ -1,6 +1,6 @@ use crate::ast::value::Value; use crate::opcodes::default_fn; -use rust_decimal::{prelude::ToPrimitive, Decimal, MathematicalOps}; +use rust_decimal::{prelude::ToPrimitive, Decimal, MathematicalOps, RoundingStrategy}; use super::error::VMError; use super::vm::VM; @@ -134,7 +134,10 @@ impl<'a> VM<'a> { } else { 0 }; - self.registers[dest] = Value::Number(n.round_dp(places)); + // Half away from zero (2.5 → 3, -2.5 → -3), not banker's rounding. + self.registers[dest] = Value::Number( + n.round_dp_with_strategy(places, RoundingStrategy::MidpointAwayFromZero), + ); Ok(()) } diff --git a/src/vm/methods.rs b/src/vm/methods.rs index e20d719..af5ae3d 100644 --- a/src/vm/methods.rs +++ b/src/vm/methods.rs @@ -135,7 +135,7 @@ impl<'a> VM<'a> { )), } } - "length" => Ok(Value::Number(Decimal::from(s.len()))), + "length" => Ok(Value::Number(Decimal::from(s.chars().count()))), "charAt" => { if args.is_empty() { return Err(VMError::RuntimeError( @@ -733,6 +733,37 @@ impl<'a> VM<'a> { _ => Err(VMError::RuntimeError("sort() requires a string field name".to_string())), } } + // Numeric aggregates: a List holding only Numbers behaves like a + // NumberList (an empty projection like `items.amount` yields an empty + // List, so `items.amount.sum()` must still work). + "sum" | "avg" | "min" | "max" => { + let mut nums: Vec = Vec::with_capacity(list.len()); + for item in list.iter() { + match item { + Value::Number(n) => nums.push(*n), + other => { + return Err(VMError::RuntimeError(format!( + "{}() requires a list of numbers, found {}", + method, + other.type_name() + ))); + } + } + } + match method { + "sum" => Ok(Value::Number(nums.iter().sum())), + "avg" => { + if nums.is_empty() { + Ok(Value::Null) + } else { + let sum: Decimal = nums.iter().sum(); + Ok(Value::Number(sum / Decimal::from(nums.len()))) + } + } + "min" => Ok(nums.iter().min().map(|n| Value::Number(*n)).unwrap_or(Value::Null)), + _ => Ok(nums.iter().max().map(|n| Value::Number(*n)).unwrap_or(Value::Null)), + } + } _ => { 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)) { diff --git a/src/vm/vm.rs b/src/vm/vm.rs index 4ce698c..de3d604 100644 --- a/src/vm/vm.rs +++ b/src/vm/vm.rs @@ -221,6 +221,7 @@ impl<'a> VM<'a> { OpCodeByte::Not => self.handle_not(), OpCodeByte::Jump => self.handle_jump(), OpCodeByte::JumpIfFalse => self.handle_jump_if_false(), + OpCodeByte::JumpIfTrue => self.handle_jump_if_true(), OpCodeByte::Concat => self.handle_concat(), OpCodeByte::GetProperty => self.handle_get_property(), OpCodeByte::SetProperty => self.handle_set_property(), @@ -654,6 +655,32 @@ impl<'a> VM<'a> { Ok(()) } + /// Handle JumpIfTrue opcode - conditional jump (used for `||` short-circuit) + #[inline] + fn handle_jump_if_true(&mut self) -> Result<(), VMError> { + let cond_reg = self.read_register_checked()?; + let addr = self.read_jump_address()?; + + match &self.registers[cond_reg] { + Value::Boolean(condition) => { + if *condition { + self.set_position(addr)?; + log_debug!(self, "JumpIfTrue to {} (condition=true)", addr); + } else { + log_debug!(self, "JumpIfTrue not taken (condition=false)"); + } + } + v => { + return Err(VMError::TypeMismatch { + expected: "Boolean".to_string(), + got: v.type_name().to_string(), + }); + } + } + + Ok(()) + } + // ============================================================================ // Opcode Handlers - String Operations // ============================================================================ @@ -687,6 +714,22 @@ impl<'a> VM<'a> { let value = map.get(prop).cloned().unwrap_or(Value::Null); self.registers[dest] = value; } + // `.length` as a property (sugar for `.length()`) on strings and lists. + Value::String(s) if prop == "length" => { + self.registers[dest] = Value::Number(Decimal::from(s.chars().count())); + } + Value::NumberList(list) if prop == "length" => { + self.registers[dest] = Value::Number(Decimal::from(list.len())); + } + Value::StringList(list) if prop == "length" => { + self.registers[dest] = Value::Number(Decimal::from(list.len())); + } + Value::List(list) + if prop == "length" && !matches!(list.first(), Some(Value::Object(_))) => + { + // Only when this can't be a projection of an Object field named "length" + self.registers[dest] = Value::Number(Decimal::from(list.len())); + } Value::List(list) => { // Property projection: list.field → extract field from each Object element let mut values: Vec = Vec::with_capacity(list.len()); diff --git a/tests/integration_tests.rs b/tests/integration_tests.rs index e12c0ad..a2563d6 100644 --- a/tests/integration_tests.rs +++ b/tests/integration_tests.rs @@ -2069,3 +2069,138 @@ fn test_string_compare_in_condition() { "#; assert_eq!(run_and_get_result(code), Value::Number(dec!(80))); } + +// ==================== SHORT-CIRCUIT / LIST AGGREGATE / LENGTH TESTS ==================== + +fn num_list(v: &[i64]) -> Value { + Value::NumberList(Rc::new(v.iter().map(|n| rust_decimal::Decimal::from(*n)).collect())) +} + +fn obj(fields: Vec<(&str, Value)>) -> Value { + let mut m = IndexMap::new(); + for (k, v) in fields { + m.insert(SmolStr::new(k), v); + } + Value::Object(Rc::new(m)) +} + +#[test] +fn test_short_circuit_and_skips_failing_right() { + // Right side would blow up (property on Null) if evaluated + assert_eq!( + run_expr_with_globals("x != null && x.a.b.c > 1", vec![("x", Value::Null)]), + Value::Boolean(false) + ); +} + +#[test] +fn test_short_circuit_or_skips_failing_right() { + assert_eq!( + run_expr_with_globals("x == null || x.a.b.c > 1", vec![("x", Value::Null)]), + Value::Boolean(true) + ); +} + +#[test] +fn test_short_circuit_left_must_be_boolean() { + let err = run_expr_err("1 && true", vec![]); + assert!(err.contains("Boolean"), "got: {err}"); + let err = run_expr_err("\"a\" || true", vec![]); + assert!(err.contains("Boolean"), "got: {err}"); +} + +#[test] +fn test_short_circuit_right_value_passthrough() { + // Right operand's value is the result when left doesn't short-circuit + // (JS-like); using it in `if` still type-checks. + assert_eq!(run_expr("true && 1"), Value::Number(dec!(1))); + let err = run_expr_err("if true && 1 then 1 end", vec![]); + assert!(err.contains("Boolean"), "got: {err}"); +} + +#[test] +fn test_short_circuit_register_pressure() { + // Deep nesting: exercise register reuse in compile_logical_op + let code = "a = 1\nb = 2\nc = 3\n(a < b && b < c && c > a) || (a > b && b > c) || (a == 1 && (b == 2 || c == 9) && !(a > c))"; + assert_eq!(run_expr(code), Value::Boolean(true)); +} + +#[test] +fn test_short_circuit_side_effect_not_run() { + // Host fn records calls; must not be called when short-circuited + let ast = parser::program("false && hit() == 1").expect("parse"); + let mut compiler = Compiler::new(); + let bytecode = compiler.compile(ast).expect("compile"); + let mut vm = VM::new(&bytecode); + let called = Rc::new(std::cell::Cell::new(false)); + let c2 = called.clone(); + vm.register_function("hit", move |_| { + c2.set(true); + Ok(Value::Number(dec!(1))) + }); + assert_eq!(vm.execute().unwrap(), Value::Boolean(false)); + assert!(!called.get(), "right side must not run"); +} + +#[test] +fn test_empty_list_aggregates() { + let el = Value::List(Rc::new(vec![])); + assert_eq!(run_expr_with_globals("l.sum()", vec![("l", el.clone())]), Value::Number(dec!(0))); + assert_eq!(run_expr_with_globals("l.avg()", vec![("l", el.clone())]), Value::Null); + assert_eq!(run_expr_with_globals("l.min()", vec![("l", el.clone())]), Value::Null); + assert_eq!(run_expr_with_globals("l.max()", vec![("l", el)]), Value::Null); +} + +#[test] +fn test_empty_projection_sum() { + // items.amount on an empty items list must still sum to 0 + let items = Value::List(Rc::new(vec![])); + assert_eq!( + run_expr_with_globals("items.amount.sum()", vec![("items", items.clone())]), + Value::Number(dec!(0)) + ); + assert_eq!( + run_expr_with_globals("items.amount.length", vec![("items", items)]), + Value::Number(dec!(0)) + ); +} + +#[test] +fn test_numeric_list_aggregates() { + let l = Value::List(Rc::new(vec![ + Value::Number(dec!(4)), + Value::Number(dec!(1)), + Value::Number(dec!(7)), + ])); + assert_eq!(run_expr_with_globals("l.sum()", vec![("l", l.clone())]), Value::Number(dec!(12))); + assert_eq!(run_expr_with_globals("l.avg()", vec![("l", l.clone())]), Value::Number(dec!(4))); + assert_eq!(run_expr_with_globals("l.min()", vec![("l", l.clone())]), Value::Number(dec!(1))); + assert_eq!(run_expr_with_globals("l.max()", vec![("l", l)]), Value::Number(dec!(7))); +} + +#[test] +fn test_mixed_list_aggregate_errors() { + let l = Value::List(Rc::new(vec![Value::Number(dec!(1)), Value::String("x".into())])); + let err = run_expr_err("l.sum()", vec![("l", l)]); + assert!(err.contains("list of numbers"), "got: {err}"); +} + +#[test] +fn test_length_property_on_lists() { + assert_eq!(run_expr_with_globals("l.length", vec![("l", num_list(&[1, 2, 3]))]), Value::Number(dec!(3))); + let sl = Value::StringList(Rc::new(vec!["a".into()])); + assert_eq!(run_expr_with_globals("l.length", vec![("l", sl)]), Value::Number(dec!(1))); + let ml = Value::List(Rc::new(vec![Value::Number(dec!(1)), Value::String("x".into())])); + assert_eq!(run_expr_with_globals("l.length", vec![("l", ml)]), Value::Number(dec!(2))); +} + +#[test] +fn test_length_property_projection_on_object_list() { + // List of objects with a `length` field → projection, not count + let l = Value::List(Rc::new(vec![ + obj(vec![("length", Value::Number(dec!(10)))]), + obj(vec![("length", Value::Number(dec!(20)))]), + ])); + assert_eq!(run_expr_with_globals("l.length", vec![("l", l.clone())]), num_list(&[10, 20])); + assert_eq!(run_expr_with_globals("l.length()", vec![("l", l)]), Value::Number(dec!(2))); +} diff --git a/tests/test_cases.json b/tests/test_cases.json index 855c32c..3d4bd50 100644 --- a/tests/test_cases.json +++ b/tests/test_cases.json @@ -1458,5 +1458,326 @@ "o": {"type": "object", "value": {"name": "ali"}} }, "expected": { "type": "boolean", "value": true } + }, + + { + "name": "unary: not on property", + "code": "!o.active", + "globals": { + "o": {"type": "object", "value": {"active": false}} + }, + "expected": { "type": "boolean", "value": true } + }, + { + "name": "unary: not on method call", + "code": "!o.keys().isEmpty()", + "globals": { + "o": {"type": "object", "value": {"a": "1"}} + }, + "expected": { "type": "boolean", "value": true } + }, + { + "name": "unary: neg on property", + "code": "-o.age", + "globals": { + "o": {"type": "object", "value": {"age": "30"}} + }, + "expected": { "type": "number", "value": "-30" } + }, + { + "name": "unary: neg on method call", + "code": "-\"5\".length()", + "expected": { "type": "number", "value": "-1" } + }, + { + "name": "unary: double neg", + "code": "- -1", + "expected": { "type": "number", "value": "1" } + }, + { + "name": "unary: double not", + "code": "!!true", + "expected": { "type": "boolean", "value": true } + }, + { + "name": "unary: neg binds looser than power", + "code": "-2 ** 2", + "expected": { "type": "number", "value": "-4" } + }, + { + "name": "unary: neg exponent", + "code": "2 ** -1", + "expected": { "type": "number", "value": "0.5" } + }, + { + "name": "unary: neg in arithmetic", + "code": "3 - -2", + "expected": { "type": "number", "value": "5" } + }, + { + "name": "unary: neg times", + "code": "-2 * 3", + "expected": { "type": "number", "value": "-6" } + }, + { + "name": "unary: not with and", + "code": "!false && true", + "expected": { "type": "boolean", "value": true } + }, + { + "name": "unary: not with comparison", + "code": "!(1 > 2)", + "expected": { "type": "boolean", "value": true } + }, + { + "name": "unary: not on property in if", + "code": "if !o.iptal then \"ok\" else \"iptal\" end", + "globals": { + "o": {"type": "object", "value": {"iptal": false}} + }, + "expected": { "type": "string", "value": "ok" } + }, + { + "name": "short-circuit: and skips right when left false", + "code": "x != null && x.name == \"a\"", + "globals": { + "x": {"type": "null"} + }, + "expected": { "type": "boolean", "value": false } + }, + { + "name": "short-circuit: and evaluates right when left true", + "code": "x != null && x.name == \"a\"", + "globals": { + "x": {"type": "object", "value": {"name": "a"}} + }, + "expected": { "type": "boolean", "value": true } + }, + { + "name": "short-circuit: or skips right when left true", + "code": "x == null || x.name == \"a\"", + "globals": { + "x": {"type": "null"} + }, + "expected": { "type": "boolean", "value": true } + }, + { + "name": "short-circuit: or evaluates right when left false", + "code": "x == null || x.name == \"a\"", + "globals": { + "x": {"type": "object", "value": {"name": "b"}} + }, + "expected": { "type": "boolean", "value": false } + }, + { + "name": "short-circuit: and true true", + "code": "true && true", + "expected": { "type": "boolean", "value": true } + }, + { + "name": "short-circuit: and true false", + "code": "true && false", + "expected": { "type": "boolean", "value": false } + }, + { + "name": "short-circuit: or false false", + "code": "false || false", + "expected": { "type": "boolean", "value": false } + }, + { + "name": "short-circuit: or false true", + "code": "false || true", + "expected": { "type": "boolean", "value": true } + }, + { + "name": "short-circuit: chained and", + "code": "true && true && false", + "expected": { "type": "boolean", "value": false } + }, + { + "name": "short-circuit: chained or", + "code": "false || false || true", + "expected": { "type": "boolean", "value": true } + }, + { + "name": "short-circuit: mixed precedence", + "code": "true || false && false", + "expected": { "type": "boolean", "value": true } + }, + { + "name": "short-circuit: mixed precedence 2", + "code": "false && true || true", + "expected": { "type": "boolean", "value": true } + }, + { + "name": "short-circuit: nested with comparisons", + "code": "a > 1 && (b < 5 || c == 3)", + "globals": { + "a": {"type": "number", "value": "2"}, + "b": {"type": "number", "value": "9"}, + "c": {"type": "number", "value": "3"} + }, + "expected": { "type": "boolean", "value": true } + }, + { + "name": "short-circuit: result assigned", + "code": "r = x != null && x.v > 1\nr", + "globals": { + "x": {"type": "null"} + }, + "expected": { "type": "boolean", "value": false } + }, + { + "name": "short-circuit: in if condition", + "code": "if x != null && x.v > 1 then 1 else 2 end", + "globals": { + "x": {"type": "null"} + }, + "expected": { "type": "number", "value": "2" } + }, + { + "name": "short-circuit: skips method on null", + "code": "x != null && x.upper() == \"A\"", + "globals": { + "x": {"type": "null"} + }, + "expected": { "type": "boolean", "value": false } + }, + { + "name": "short-circuit: right side complex", + "code": "1 == 1 && (2 + 3) * 2 == 10", + "expected": { "type": "boolean", "value": true } + }, + { + "name": "short-circuit: left side complex", + "code": "(2 + 3) * 2 == 10 && 1 == 1", + "expected": { "type": "boolean", "value": true } + }, + { + "name": "short-circuit: string concat after", + "code": "(true && false) + \"\"", + "expected": { "type": "string", "value": "false" } + }, + { + "name": "round: half away from zero 2.5", + "code": "round(2.5)", + "expected": { "type": "number", "value": "3" } + }, + { + "name": "round: half away from zero 1.5", + "code": "round(1.5)", + "expected": { "type": "number", "value": "2" } + }, + { + "name": "round: half away from zero 0.5", + "code": "round(0.5)", + "expected": { "type": "number", "value": "1" } + }, + { + "name": "round: negative half", + "code": "round(-2.5)", + "expected": { "type": "number", "value": "-3" } + }, + { + "name": "round: dp half", + "code": "round(0.125, 2)", + "expected": { "type": "number", "value": "0.13" } + }, + { + "name": "round: dp", + "code": "round(1.234, 2)", + "expected": { "type": "number", "value": "1.23" } + }, + { + "name": "round: dp up", + "code": "round(1.235, 2)", + "expected": { "type": "number", "value": "1.24" } + }, + { + "name": "length prop: string", + "code": "\"hello\".length", + "expected": { "type": "number", "value": "5" } + }, + { + "name": "length prop: unicode string", + "code": "\"üşi\".length", + "expected": { "type": "number", "value": "3" } + }, + { + "name": "length method: unicode string", + "code": "\"üşi\".length()", + "expected": { "type": "number", "value": "3" } + }, + { + "name": "length prop: string global", + "code": "s.length", + "globals": { + "s": {"type": "string", "value": "Hello World"} + }, + "expected": { "type": "number", "value": "11" } + }, + { + "name": "length prop: string in expression", + "code": "s.length > 5", + "globals": { + "s": {"type": "string", "value": "Hello World"} + }, + "expected": { "type": "boolean", "value": true } + }, + { + "name": "length prop: object field named length still works", + "code": "o.length", + "globals": { + "o": {"type": "object", "value": {"length": "7"}} + }, + "expected": { "type": "number", "value": "7" } + }, + { + "name": "length prop: object field string", + "code": "o.name.length", + "globals": { + "o": {"type": "object", "value": {"name": "ali"}} + }, + "expected": { "type": "number", "value": "3" } + }, + { + "name": "assoc: and binds tighter than or", + "code": "false && true || true", + "expected": { "type": "boolean", "value": true } + }, + { + "name": "assoc: and binds tighter than or 2", + "code": "true || true && false", + "expected": { "type": "boolean", "value": true } + }, + { + "name": "assoc: or chain left", + "code": "false || false || true && false", + "expected": { "type": "boolean", "value": false } + }, + { + "name": "assoc: equality left assoc", + "code": "1 == 1 == true", + "expected": { "type": "boolean", "value": true } + }, + { + "name": "assoc: subtraction left assoc", + "code": "10 - 3 - 2", + "expected": { "type": "number", "value": "5" } + }, + { + "name": "assoc: add sub mixed", + "code": "10 - 3 + 2", + "expected": { "type": "number", "value": "9" } + }, + { + "name": "assoc: comparison then and", + "code": "1 < 2 && 3 >= 3 && \"a\" != \"b\"", + "expected": { "type": "boolean", "value": true } + }, + { + "name": "assoc: in with and", + "code": "\"a\" in \"abc\" && \"z\" in \"abc\" == false", + "expected": { "type": "boolean", "value": true } } ] diff --git a/wasm/Cargo.lock b/wasm/Cargo.lock index 51c0489..f7f316c 100644 --- a/wasm/Cargo.lock +++ b/wasm/Cargo.lock @@ -141,7 +141,7 @@ dependencies = [ [[package]] name = "dexpr" -version = "0.4.0" +version = "0.4.1" dependencies = [ "bumpalo", "indexmap", @@ -159,7 +159,7 @@ dependencies = [ [[package]] name = "dexpr-wasm" -version = "0.4.0" +version = "0.4.1" dependencies = [ "dexpr", "getrandom 0.3.4", diff --git a/wasm/Cargo.toml b/wasm/Cargo.toml index d154498..58faa7e 100644 --- a/wasm/Cargo.toml +++ b/wasm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "dexpr-wasm" -version = "0.4.0" +version = "0.4.1" edition = "2021" [lib]