mirror of
https://github.com/duhanbalci/dexpr.git
synced 2026-08-30 07:51:41 +00:00
0.4.1
This commit is contained in:
@@ -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()`)
|
- 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)
|
- Arithmetic (`+`, `-`, `*`, `/`, `%`, `**`), comparison, and logical operators. `==`/`!=` work on all types (structural, different types → `false`); `<`/`<=`/`>`/`>=` work on Number and String (lexicographic)
|
||||||
- `null` literal (`x == null`)
|
- `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`)
|
- `in` operator for membership testing (`"finans" in categories`, `5 in numbers`, `"hello" in "hello world"`, `"key" in obj`)
|
||||||
- Compound assignments (`+=`, `-=`, `*=`, `/=`, `%=`)
|
- Compound assignments (`+=`, `-=`, `*=`, `/=`, `%=`)
|
||||||
- Built-in `log()` function for output and `rand(min, max)` for random integers
|
- Built-in `log()` function for output and `rand(min, max)` for random integers
|
||||||
|
|||||||
Generated
+1
-1
@@ -304,7 +304,7 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dexpr"
|
name = "dexpr"
|
||||||
version = "0.4.0"
|
version = "0.4.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bumpalo",
|
"bumpalo",
|
||||||
"criterion",
|
"criterion",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "dexpr"
|
name = "dexpr"
|
||||||
version = "0.4.0"
|
version = "0.4.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Embeddable expression evaluator and bytecode VM"
|
description = "Embeddable expression evaluator and bytecode VM"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
|
|||||||
@@ -69,6 +69,20 @@ Parse ile birlikte pozisyon bilgisi de toplar ve `DebugInfo` üretir.
|
|||||||
1. İfadeyi register'a derle
|
1. İfadeyi register'a derle
|
||||||
2. `StoreGlobal` emit et (tüm değişkenler global)
|
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):
|
||||||
|
|
||||||
|
```
|
||||||
|
<left> -> rL
|
||||||
|
JumpIfFalse rL, END (|| için JumpIfTrue)
|
||||||
|
<right> -> 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)
|
### If Statement (Koşullu Deyim)
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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` |
|
| `NumberList` | `length`, `len`, `isEmpty`, `first`, `last`, `get`, `contains`, `indexOf`, `slice`, `reverse`, `sort`, `sum`, `avg`, `min`, `max` |
|
||||||
| `StringList` | `length`, `len`, `isEmpty`, `first`, `last`, `get`, `contains`, `indexOf`, `slice`, `reverse`, `sort`, `join` |
|
| `StringList` | `length`, `len`, `isEmpty`, `first`, `last`, `get`, `contains`, `indexOf`, `slice`, `reverse`, `sort`, `join` |
|
||||||
| `Object` | `keys`, `values`, `length`, `len`, `contains`, `get` |
|
| `Object` | `keys`, `values`, `length`, `len`, `contains`, `get` |
|
||||||
| `List` | `length`, `len`, `isEmpty`, `first`, `last`, `get`, `contains`, `indexOf`, `slice`, `reverse`, `join`, `map`, `filter`, `find`, `sort` |
|
| `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)`
|
### `add_function(name, signature, doc)`
|
||||||
|
|
||||||
|
|||||||
+3
-2
@@ -53,8 +53,8 @@ Bytecode komut setini (instruction set) tanımlar. Her opcode bir `u8` değerine
|
|||||||
|
|
||||||
| Opcode | Değer | Açıklama |
|
| Opcode | Değer | Açıklama |
|
||||||
|--------|-------|----------|
|
|--------|-------|----------|
|
||||||
| `And` | `0x50` | Mantıksal VE |
|
| `And` | `0x50` | Mantıksal VE (compiler artık emit etmiyor; `&&` JumpIfFalse ile derlenir) |
|
||||||
| `Or` | `0x51` | Mantıksal VEYA |
|
| `Or` | `0x51` | Mantıksal VEYA (compiler artık emit etmiyor; `\|\|` JumpIfTrue ile derlenir) |
|
||||||
| `Not` | `0x52` | Mantıksal DEĞİL |
|
| `Not` | `0x52` | Mantıksal DEĞİL |
|
||||||
|
|
||||||
### Kontrol Akışı
|
### Kontrol Akışı
|
||||||
@@ -63,6 +63,7 @@ Bytecode komut setini (instruction set) tanımlar. Her opcode bir `u8` değerine
|
|||||||
|--------|-------|----------|
|
|--------|-------|----------|
|
||||||
| `Jump` | `0x60` | Koşulsuz atlama |
|
| `Jump` | `0x60` | Koşulsuz atlama |
|
||||||
| `JumpIfFalse` | `0x61` | Register false ise atla |
|
| `JumpIfFalse` | `0x61` | Register false ise atla |
|
||||||
|
| `JumpIfTrue` | `0x62` | Register true ise atla (`\|\|` short-circuit) |
|
||||||
|
|
||||||
### Üyelik Testi
|
### Üyelik Testi
|
||||||
|
|
||||||
|
|||||||
+11
-8
@@ -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:
|
En düşükten en yükseğe:
|
||||||
|
|
||||||
1. Method çağrıları, fonksiyon çağrıları
|
1. Fonksiyon çağrıları
|
||||||
2. Mantıksal AND (`&&`)
|
2. Mantıksal OR (`||`) — soldan birleşimli
|
||||||
3. Mantıksal OR (`||`)
|
3. Mantıksal AND (`&&`) — soldan birleşimli (`a && b || c` = `(a && b) || c`)
|
||||||
4. Karşılaştırma (`==`, `!=`, `<`, `<=`, `>`, `>=`, `in`)
|
4. Karşılaştırma (`==`, `!=`, `<`, `<=`, `>`, `>=`, `in`) — soldan birleşimli
|
||||||
5. Toplama/Çıkarma (`+`, `-`)
|
5. Toplama/Çıkarma (`+`, `-`) — soldan birleşimli (`10 - 3 - 2` = `5`)
|
||||||
6. Çarpma/Bölme/Mod (`*`, `/`, `%`)
|
6. Çarpma/Bölme/Mod (`*`, `/`, `%`) — soldan birleşimli
|
||||||
7. Üs alma (`**`) - sağdan birleşimli (right-associative)
|
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. Tekli operatörler (`-`, `!`), atomik ifadeler
|
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ı
|
### Postfix Kuralı
|
||||||
|
|
||||||
|
|||||||
+4
-3
@@ -134,18 +134,19 @@ struct VM<'a> {
|
|||||||
|
|
||||||
### Kontrol Akışı
|
### Kontrol Akışı
|
||||||
- **`handle_jump()`** — 4-byte adres oku, reader pozisyonunu ayarla
|
- **`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
|
- **`handle_jump_if_false()`** — Register `Boolean(false)` ise atla
|
||||||
|
|
||||||
### String, Nesne ve Metodlar
|
### 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_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_set_property()`** — Object register'ında alan değerini ayarla
|
||||||
- **`handle_method_call()`** — Nesne register'ı, metod adı, argümanlar
|
- **`handle_method_call()`** — Nesne register'ı, metod adı, argümanlar
|
||||||
- **String metodları:** `upper`, `lower`, `trim`, `trimStart`, `trimEnd`, `split(delimiter)`, `replace(old, new)`, `startsWith(prefix)`, `endsWith(suffix)`, `contains(substr)`, `length`, `charAt(index)`, `substring(start, end?)`
|
- **String metodları:** `upper`, `lower`, `trim`, `trimStart`, `trimEnd`, `split(delimiter)`, `replace(old, new)`, `startsWith(prefix)`, `endsWith(suffix)`, `contains(substr)`, `length`, `charAt(index)`, `substring(start, end?)`
|
||||||
- **StringList metodları:** `length`/`len`, `isEmpty`, `first`, `last`, `get(index)`, `contains(value)`, `indexOf(value)`, `slice(start, end?)`, `reverse()`, `sort()`, `join(delimiter?)`
|
- **StringList metodları:** `length`/`len`, `isEmpty`, `first`, `last`, `get(index)`, `contains(value)`, `indexOf(value)`, `slice(start, end?)`, `reverse()`, `sort()`, `join(delimiter?)`
|
||||||
- **NumberList metodları:** `length`/`len`, `isEmpty`, `first`, `last`, `get(index)`, `contains(value)`, `indexOf(value)`, `slice(start, end?)`, `reverse()`, `sort()`, `sum`, `avg`, `min`, `max`
|
- **NumberList metodları:** `length`/`len`, `isEmpty`, `first`, `last`, `get(index)`, `contains(value)`, `indexOf(value)`, `slice(start, end?)`, `reverse()`, `sort()`, `sum`, `avg`, `min`, `max`
|
||||||
- **Object metodları:** `keys()`, `values()`, `length`/`len()`, `contains(key)`, `get(key)`
|
- **Object metodları:** `keys()`, `values()`, `length`/`len()`, `contains(key)`, `get(key)`
|
||||||
- **List metodları:** `length`/`len`, `isEmpty`, `first`, `last`, `get(index)`, `contains(value)`, `indexOf(value)`, `slice(start, end?)`, `reverse()`, `join(delim?)`, `map(field)`, `filter(field, value?)`, `find(field, value?)`, `sort(field)`
|
- **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
|
- **Harici metodlar:** Yukarıdaki built-in metodlar bulunamazsa `external_methods` HashMap'inde aranır
|
||||||
|
|
||||||
### Üyelik Testi
|
### Üyelik Testi
|
||||||
@@ -163,7 +164,7 @@ struct VM<'a> {
|
|||||||
- **`max(a, b, ...)`** — Verilen değerlerin maksimumu
|
- **`max(a, b, ...)`** — Verilen değerlerin maksimumu
|
||||||
- **`floor(n)`** — Aşağı yuvarlama
|
- **`floor(n)`** — Aşağı yuvarlama
|
||||||
- **`ceil(n)`** — Yukarı 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
|
- **`sqrt(n)`** — Karekök
|
||||||
- **`len(v)`** — Değerin uzunluğu (String, List, Object)
|
- **`len(v)`** — Değerin uzunluğu (String, List, Object)
|
||||||
- **`toString(v)`** — Değeri String'e dönüştür
|
- **`toString(v)`** — Değeri String'e dönüştür
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@duhanbalci/codemirror-lang-dexpr",
|
"name": "@duhanbalci/codemirror-lang-dexpr",
|
||||||
"version": "0.4.0",
|
"version": "0.4.1",
|
||||||
"description": "CodeMirror 6 language support for dexpr",
|
"description": "CodeMirror 6 language support for dexpr",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "dist/index.cjs",
|
"main": "dist/index.cjs",
|
||||||
|
|||||||
+243
-235
@@ -1,235 +1,243 @@
|
|||||||
use crate::bytecode::BytecodeReader;
|
use crate::bytecode::BytecodeReader;
|
||||||
use crate::opcodes::OpCodeByte;
|
use crate::opcodes::OpCodeByte;
|
||||||
|
|
||||||
/// A utility function to disassemble bytecode for debugging
|
/// A utility function to disassemble bytecode for debugging
|
||||||
pub fn disassemble_bytecode(bytecode: &[u8]) -> Vec<String> {
|
pub fn disassemble_bytecode(bytecode: &[u8]) -> Vec<String> {
|
||||||
let mut result = Vec::new();
|
let mut result = Vec::new();
|
||||||
let mut reader = BytecodeReader::new(bytecode);
|
let mut reader = BytecodeReader::new(bytecode);
|
||||||
|
|
||||||
while reader.remaining() > 0 {
|
while reader.remaining() > 0 {
|
||||||
let start_position = reader.position();
|
let start_position = reader.position();
|
||||||
let opcode_byte = match reader.read_byte() {
|
let opcode_byte = match reader.read_byte() {
|
||||||
Ok(b) => b,
|
Ok(b) => b,
|
||||||
Err(_) => break,
|
Err(_) => break,
|
||||||
};
|
};
|
||||||
|
|
||||||
let opcode = match OpCodeByte::from_byte(opcode_byte) {
|
let opcode = match OpCodeByte::from_byte(opcode_byte) {
|
||||||
Some(op) => op,
|
Some(op) => op,
|
||||||
None => {
|
None => {
|
||||||
result.push(format!(
|
result.push(format!(
|
||||||
"{:04x}: Unknown opcode: 0x{:02x}",
|
"{:04x}: Unknown opcode: 0x{:02x}",
|
||||||
start_position, opcode_byte
|
start_position, opcode_byte
|
||||||
));
|
));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let instruction = match opcode {
|
let instruction = match opcode {
|
||||||
OpCodeByte::LoadConst => {
|
OpCodeByte::LoadConst => {
|
||||||
let reg = reader.read_byte();
|
let reg = reader.read_byte();
|
||||||
let value = reader.read_value();
|
let value = reader.read_value();
|
||||||
match (reg, value) {
|
match (reg, value) {
|
||||||
(Ok(r), Ok(v)) => format!("{:04x}: LoadConst r{}, {}", start_position, r, v),
|
(Ok(r), Ok(v)) => format!("{:04x}: LoadConst r{}, {}", start_position, r, v),
|
||||||
_ => format!("{:04x}: LoadConst (truncated)", start_position),
|
_ => format!("{:04x}: LoadConst (truncated)", start_position),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OpCodeByte::Move => {
|
OpCodeByte::Move => {
|
||||||
let dest = reader.read_byte();
|
let dest = reader.read_byte();
|
||||||
let src = reader.read_byte();
|
let src = reader.read_byte();
|
||||||
match (dest, src) {
|
match (dest, src) {
|
||||||
(Ok(d), Ok(s)) => format!("{:04x}: Move r{} = r{}", start_position, d, s),
|
(Ok(d), Ok(s)) => format!("{:04x}: Move r{} = r{}", start_position, d, s),
|
||||||
_ => format!("{:04x}: Move (truncated)", start_position),
|
_ => format!("{:04x}: Move (truncated)", start_position),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OpCodeByte::LoadLocal => {
|
OpCodeByte::LoadLocal => {
|
||||||
let reg = reader.read_byte();
|
let reg = reader.read_byte();
|
||||||
let offset = reader.read_byte();
|
let offset = reader.read_byte();
|
||||||
match (reg, offset) {
|
match (reg, offset) {
|
||||||
(Ok(r), Ok(o)) => format!("{:04x}: LoadLocal r{}, offset={}", start_position, r, o),
|
(Ok(r), Ok(o)) => format!("{:04x}: LoadLocal r{}, offset={}", start_position, r, o),
|
||||||
_ => format!("{:04x}: LoadLocal (truncated)", start_position),
|
_ => format!("{:04x}: LoadLocal (truncated)", start_position),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OpCodeByte::StoreLocal => {
|
OpCodeByte::StoreLocal => {
|
||||||
let offset = reader.read_byte();
|
let offset = reader.read_byte();
|
||||||
let reg = reader.read_byte();
|
let reg = reader.read_byte();
|
||||||
match (offset, reg) {
|
match (offset, reg) {
|
||||||
(Ok(o), Ok(r)) => format!("{:04x}: StoreLocal offset={}, r{}", start_position, o, r),
|
(Ok(o), Ok(r)) => format!("{:04x}: StoreLocal offset={}, r{}", start_position, o, r),
|
||||||
_ => format!("{:04x}: StoreLocal (truncated)", start_position),
|
_ => format!("{:04x}: StoreLocal (truncated)", start_position),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OpCodeByte::LoadGlobal => {
|
OpCodeByte::LoadGlobal => {
|
||||||
let reg = reader.read_byte();
|
let reg = reader.read_byte();
|
||||||
let name = reader.read_string();
|
let name = reader.read_string();
|
||||||
match (reg, name) {
|
match (reg, name) {
|
||||||
(Ok(r), Ok(n)) => format!("{:04x}: LoadGlobal r{}, \"{}\"", start_position, r, n),
|
(Ok(r), Ok(n)) => format!("{:04x}: LoadGlobal r{}, \"{}\"", start_position, r, n),
|
||||||
_ => format!("{:04x}: LoadGlobal (truncated)", start_position),
|
_ => format!("{:04x}: LoadGlobal (truncated)", start_position),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OpCodeByte::StoreGlobal => {
|
OpCodeByte::StoreGlobal => {
|
||||||
let name = reader.read_string();
|
let name = reader.read_string();
|
||||||
let reg = reader.read_byte();
|
let reg = reader.read_byte();
|
||||||
match (name, reg) {
|
match (name, reg) {
|
||||||
(Ok(n), Ok(r)) => format!("{:04x}: StoreGlobal \"{}\", r{}", start_position, n, r),
|
(Ok(n), Ok(r)) => format!("{:04x}: StoreGlobal \"{}\", r{}", start_position, n, r),
|
||||||
_ => format!("{:04x}: StoreGlobal (truncated)", start_position),
|
_ => format!("{:04x}: StoreGlobal (truncated)", start_position),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OpCodeByte::Add
|
OpCodeByte::Add
|
||||||
| OpCodeByte::Sub
|
| OpCodeByte::Sub
|
||||||
| OpCodeByte::Mul
|
| OpCodeByte::Mul
|
||||||
| OpCodeByte::Div
|
| OpCodeByte::Div
|
||||||
| OpCodeByte::Mod
|
| OpCodeByte::Mod
|
||||||
| OpCodeByte::Pow
|
| OpCodeByte::Pow
|
||||||
| OpCodeByte::Lt
|
| OpCodeByte::Lt
|
||||||
| OpCodeByte::Lte
|
| OpCodeByte::Lte
|
||||||
| OpCodeByte::Gt
|
| OpCodeByte::Gt
|
||||||
| OpCodeByte::Gte
|
| OpCodeByte::Gte
|
||||||
| OpCodeByte::Eq
|
| OpCodeByte::Eq
|
||||||
| OpCodeByte::Neq
|
| OpCodeByte::Neq
|
||||||
| OpCodeByte::And
|
| OpCodeByte::And
|
||||||
| OpCodeByte::Or
|
| OpCodeByte::Or
|
||||||
| OpCodeByte::Contains
|
| OpCodeByte::Contains
|
||||||
| OpCodeByte::Concat => {
|
| OpCodeByte::Concat => {
|
||||||
let res = reader.read_byte();
|
let res = reader.read_byte();
|
||||||
let left = reader.read_byte();
|
let left = reader.read_byte();
|
||||||
let right = reader.read_byte();
|
let right = reader.read_byte();
|
||||||
match (res, left, right) {
|
match (res, left, right) {
|
||||||
(Ok(r), Ok(l), Ok(rg)) => {
|
(Ok(r), Ok(l), Ok(rg)) => {
|
||||||
format!("{:04x}: {:?} r{}, r{}, r{}", start_position, opcode, r, l, rg)
|
format!("{:04x}: {:?} r{}, r{}, r{}", start_position, opcode, r, l, rg)
|
||||||
}
|
}
|
||||||
_ => format!("{:04x}: {:?} (truncated)", start_position, opcode),
|
_ => format!("{:04x}: {:?} (truncated)", start_position, opcode),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OpCodeByte::Neg | OpCodeByte::Not => {
|
OpCodeByte::Neg | OpCodeByte::Not => {
|
||||||
let res = reader.read_byte();
|
let res = reader.read_byte();
|
||||||
let operand = reader.read_byte();
|
let operand = reader.read_byte();
|
||||||
match (res, operand) {
|
match (res, operand) {
|
||||||
(Ok(r), Ok(o)) => format!("{:04x}: {:?} r{}, r{}", start_position, opcode, r, o),
|
(Ok(r), Ok(o)) => format!("{:04x}: {:?} r{}, r{}", start_position, opcode, r, o),
|
||||||
_ => format!("{:04x}: {:?} (truncated)", start_position, opcode),
|
_ => format!("{:04x}: {:?} (truncated)", start_position, opcode),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OpCodeByte::Jump => match reader.read_u32() {
|
OpCodeByte::Jump => match reader.read_u32() {
|
||||||
Ok(addr) => format!("{:04x}: Jump -> 0x{:04x}", start_position, addr),
|
Ok(addr) => format!("{:04x}: Jump -> 0x{:04x}", start_position, addr),
|
||||||
Err(_) => format!("{:04x}: Jump (truncated)", start_position),
|
Err(_) => format!("{:04x}: Jump (truncated)", start_position),
|
||||||
},
|
},
|
||||||
OpCodeByte::JumpIfFalse => {
|
OpCodeByte::JumpIfFalse => {
|
||||||
let reg = reader.read_byte();
|
let reg = reader.read_byte();
|
||||||
let addr = reader.read_u32();
|
let addr = reader.read_u32();
|
||||||
match (reg, addr) {
|
match (reg, addr) {
|
||||||
(Ok(r), Ok(a)) => format!("{:04x}: JumpIfFalse r{} -> 0x{:04x}", start_position, r, a),
|
(Ok(r), Ok(a)) => format!("{:04x}: JumpIfFalse r{} -> 0x{:04x}", start_position, r, a),
|
||||||
_ => format!("{:04x}: JumpIfFalse (truncated)", start_position),
|
_ => format!("{:04x}: JumpIfFalse (truncated)", start_position),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OpCodeByte::MethodCall => {
|
OpCodeByte::JumpIfTrue => {
|
||||||
let res = reader.read_byte();
|
let reg = reader.read_byte();
|
||||||
let obj = reader.read_byte();
|
let addr = reader.read_u32();
|
||||||
let method = reader.read_string();
|
match (reg, addr) {
|
||||||
let arg_count = reader.read_byte();
|
(Ok(r), Ok(a)) => format!("{:04x}: JumpIfTrue r{} -> 0x{:04x}", start_position, r, a),
|
||||||
match (res, obj, method, arg_count) {
|
_ => format!("{:04x}: JumpIfTrue (truncated)", start_position),
|
||||||
(Ok(r), Ok(o), Ok(m), Ok(count)) => {
|
}
|
||||||
let mut arg_regs = Vec::new();
|
}
|
||||||
let mut truncated = false;
|
OpCodeByte::MethodCall => {
|
||||||
for _ in 0..count {
|
let res = reader.read_byte();
|
||||||
match reader.read_byte() {
|
let obj = reader.read_byte();
|
||||||
Ok(reg) => arg_regs.push(format!("r{}", reg)),
|
let method = reader.read_string();
|
||||||
Err(_) => {
|
let arg_count = reader.read_byte();
|
||||||
truncated = true;
|
match (res, obj, method, arg_count) {
|
||||||
break;
|
(Ok(r), Ok(o), Ok(m), Ok(count)) => {
|
||||||
}
|
let mut arg_regs = Vec::new();
|
||||||
}
|
let mut truncated = false;
|
||||||
}
|
for _ in 0..count {
|
||||||
if truncated {
|
match reader.read_byte() {
|
||||||
format!(
|
Ok(reg) => arg_regs.push(format!("r{}", reg)),
|
||||||
"{:04x}: MethodCall r{} = r{}.{}(truncated args)",
|
Err(_) => {
|
||||||
start_position, r, o, m
|
truncated = true;
|
||||||
)
|
break;
|
||||||
} else {
|
}
|
||||||
format!(
|
}
|
||||||
"{:04x}: MethodCall r{} = r{}.{}({})",
|
}
|
||||||
start_position,
|
if truncated {
|
||||||
r,
|
format!(
|
||||||
o,
|
"{:04x}: MethodCall r{} = r{}.{}(truncated args)",
|
||||||
m,
|
start_position, r, o, m
|
||||||
arg_regs.join(", ")
|
)
|
||||||
)
|
} else {
|
||||||
}
|
format!(
|
||||||
}
|
"{:04x}: MethodCall r{} = r{}.{}({})",
|
||||||
_ => format!("{:04x}: MethodCall (truncated)", start_position),
|
start_position,
|
||||||
}
|
r,
|
||||||
}
|
o,
|
||||||
OpCodeByte::Log => match reader.read_byte() {
|
m,
|
||||||
Ok(reg) => format!("{:04x}: Log r{}", start_position, reg),
|
arg_regs.join(", ")
|
||||||
Err(_) => format!("{:04x}: Log (truncated)", start_position),
|
)
|
||||||
},
|
}
|
||||||
OpCodeByte::CallDefault => {
|
}
|
||||||
let res = reader.read_byte();
|
_ => format!("{:04x}: MethodCall (truncated)", start_position),
|
||||||
let fn_id = reader.read_byte();
|
}
|
||||||
let arg_count = reader.read_byte();
|
}
|
||||||
match (res, fn_id, arg_count) {
|
OpCodeByte::Log => match reader.read_byte() {
|
||||||
(Ok(r), Ok(id), Ok(count)) => {
|
Ok(reg) => format!("{:04x}: Log r{}", start_position, reg),
|
||||||
let fn_name = crate::opcodes::default_fn::name(id).unwrap_or("?");
|
Err(_) => format!("{:04x}: Log (truncated)", start_position),
|
||||||
let mut arg_regs = Vec::new();
|
},
|
||||||
for _ in 0..count {
|
OpCodeByte::CallDefault => {
|
||||||
if let Ok(reg) = reader.read_byte() {
|
let res = reader.read_byte();
|
||||||
arg_regs.push(format!("r{}", reg));
|
let fn_id = reader.read_byte();
|
||||||
}
|
let arg_count = reader.read_byte();
|
||||||
}
|
match (res, fn_id, arg_count) {
|
||||||
format!(
|
(Ok(r), Ok(id), Ok(count)) => {
|
||||||
"{:04x}: CallDefault r{} = {}({})",
|
let fn_name = crate::opcodes::default_fn::name(id).unwrap_or("?");
|
||||||
start_position, r, fn_name, arg_regs.join(", ")
|
let mut arg_regs = Vec::new();
|
||||||
)
|
for _ in 0..count {
|
||||||
}
|
if let Ok(reg) = reader.read_byte() {
|
||||||
_ => format!("{:04x}: CallDefault (truncated)", start_position),
|
arg_regs.push(format!("r{}", reg));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OpCodeByte::CallExternal => {
|
format!(
|
||||||
let res = reader.read_byte();
|
"{:04x}: CallDefault r{} = {}({})",
|
||||||
let name = reader.read_string();
|
start_position, r, fn_name, arg_regs.join(", ")
|
||||||
let arg_count = reader.read_byte();
|
)
|
||||||
match (res, name, arg_count) {
|
}
|
||||||
(Ok(r), Ok(n), Ok(count)) => {
|
_ => format!("{:04x}: CallDefault (truncated)", start_position),
|
||||||
let mut arg_regs = Vec::new();
|
}
|
||||||
for _ in 0..count {
|
}
|
||||||
if let Ok(reg) = reader.read_byte() {
|
OpCodeByte::CallExternal => {
|
||||||
arg_regs.push(format!("r{}", reg));
|
let res = reader.read_byte();
|
||||||
}
|
let name = reader.read_string();
|
||||||
}
|
let arg_count = reader.read_byte();
|
||||||
format!(
|
match (res, name, arg_count) {
|
||||||
"{:04x}: CallExternal r{} = {}({})",
|
(Ok(r), Ok(n), Ok(count)) => {
|
||||||
start_position, r, n, arg_regs.join(", ")
|
let mut arg_regs = Vec::new();
|
||||||
)
|
for _ in 0..count {
|
||||||
}
|
if let Ok(reg) = reader.read_byte() {
|
||||||
_ => format!("{:04x}: CallExternal (truncated)", start_position),
|
arg_regs.push(format!("r{}", reg));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
OpCodeByte::GetProperty => {
|
format!(
|
||||||
let dest = reader.read_byte();
|
"{:04x}: CallExternal r{} = {}({})",
|
||||||
let obj = reader.read_byte();
|
start_position, r, n, arg_regs.join(", ")
|
||||||
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}: CallExternal (truncated)", start_position),
|
||||||
_ => format!("{:04x}: GetProperty (truncated)", start_position),
|
}
|
||||||
}
|
}
|
||||||
}
|
OpCodeByte::GetProperty => {
|
||||||
OpCodeByte::SetProperty => {
|
let dest = reader.read_byte();
|
||||||
let obj = reader.read_byte();
|
let obj = reader.read_byte();
|
||||||
let prop = reader.read_string();
|
let prop = reader.read_string();
|
||||||
let val = reader.read_byte();
|
match (dest, obj, prop) {
|
||||||
match (obj, prop, val) {
|
(Ok(d), Ok(o), Ok(p)) => format!("{:04x}: GetProperty r{} = r{}.{}", start_position, d, o, p),
|
||||||
(Ok(o), Ok(p), Ok(v)) => format!("{:04x}: SetProperty r{}.{} = r{}", start_position, o, p, v),
|
_ => format!("{:04x}: GetProperty (truncated)", start_position),
|
||||||
_ => format!("{:04x}: SetProperty (truncated)", start_position),
|
}
|
||||||
}
|
}
|
||||||
}
|
OpCodeByte::SetProperty => {
|
||||||
OpCodeByte::SetResult => match reader.read_byte() {
|
let obj = reader.read_byte();
|
||||||
Ok(reg) => format!("{:04x}: SetResult r{}", start_position, reg),
|
let prop = reader.read_string();
|
||||||
Err(_) => format!("{:04x}: SetResult (truncated)", start_position),
|
let val = reader.read_byte();
|
||||||
},
|
match (obj, prop, val) {
|
||||||
OpCodeByte::ClearResult => format!("{:04x}: ClearResult", start_position),
|
(Ok(o), Ok(p), Ok(v)) => format!("{:04x}: SetProperty r{}.{} = r{}", start_position, o, p, v),
|
||||||
OpCodeByte::End => format!("{:04x}: End", start_position),
|
_ => format!("{:04x}: SetProperty (truncated)", start_position),
|
||||||
};
|
}
|
||||||
|
}
|
||||||
result.push(instruction);
|
OpCodeByte::SetResult => match reader.read_byte() {
|
||||||
}
|
Ok(reg) => format!("{:04x}: SetResult r{}", start_position, reg),
|
||||||
|
Err(_) => format!("{:04x}: SetResult (truncated)", start_position),
|
||||||
result
|
},
|
||||||
}
|
OpCodeByte::ClearResult => format!("{:04x}: ClearResult", start_position),
|
||||||
|
OpCodeByte::End => format!("{:04x}: End", start_position),
|
||||||
|
};
|
||||||
|
|
||||||
|
result.push(instruction);
|
||||||
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -316,6 +316,10 @@ impl Compiler {
|
|||||||
op: &Op,
|
op: &Op,
|
||||||
right: &Expr,
|
right: &Expr,
|
||||||
) -> Result<u8, CompileError> {
|
) -> Result<u8, CompileError> {
|
||||||
|
if matches!(op, Op::And | Op::Or) {
|
||||||
|
return self.compile_logical_op(left, op, right);
|
||||||
|
}
|
||||||
|
|
||||||
let left_reg = self.compile_expr(left)?;
|
let left_reg = self.compile_expr(left)?;
|
||||||
let right_reg = self.compile_expr(right)?;
|
let right_reg = self.compile_expr(right)?;
|
||||||
let result_reg = self.allocate_register()?;
|
let result_reg = self.allocate_register()?;
|
||||||
@@ -355,6 +359,49 @@ impl Compiler {
|
|||||||
Ok(result_reg)
|
Ok(result_reg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Compile `&&` / `||` with short-circuit evaluation.
|
||||||
|
///
|
||||||
|
/// Layout (`&&`):
|
||||||
|
/// ```text
|
||||||
|
/// <left> -> rL
|
||||||
|
/// JumpIfFalse rL, END ; left false → result is rL (false), skip right
|
||||||
|
/// <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<u8, CompileError> {
|
||||||
|
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
|
/// Compile a unary operation
|
||||||
fn compile_unary_op(&mut self, op: &Op, operand: &Expr) -> Result<u8, CompileError> {
|
fn compile_unary_op(&mut self, op: &Op, operand: &Expr) -> Result<u8, CompileError> {
|
||||||
let operand_reg = self.compile_expr(operand)?;
|
let operand_reg = self.compile_expr(operand)?;
|
||||||
|
|||||||
@@ -305,6 +305,10 @@ fn builtin_methods() -> Vec<(&'static str, Vec<MethodInfo>)> {
|
|||||||
MethodInfo { name: "filter", signature: "(field: String, value?: any) -> List", doc: Some("Filter by field value or truthy field") },
|
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: "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: "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![
|
("StringList", vec![
|
||||||
MethodInfo { name: "length", signature: "() -> Number", doc: None },
|
MethodInfo { name: "length", signature: "() -> Number", doc: None },
|
||||||
|
|||||||
+211
-208
@@ -1,208 +1,211 @@
|
|||||||
/// Register identifier
|
/// Register identifier
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub struct Register(pub u8);
|
pub struct Register(pub u8);
|
||||||
|
|
||||||
/// Default (built-in) function IDs for CallDefault opcode
|
/// Default (built-in) function IDs for CallDefault opcode
|
||||||
pub mod default_fn {
|
pub mod default_fn {
|
||||||
pub const RAND: u8 = 0;
|
pub const RAND: u8 = 0;
|
||||||
pub const ABS: u8 = 1;
|
pub const ABS: u8 = 1;
|
||||||
pub const MIN: u8 = 2;
|
pub const MIN: u8 = 2;
|
||||||
pub const MAX: u8 = 3;
|
pub const MAX: u8 = 3;
|
||||||
pub const FLOOR: u8 = 4;
|
pub const FLOOR: u8 = 4;
|
||||||
pub const CEIL: u8 = 5;
|
pub const CEIL: u8 = 5;
|
||||||
pub const ROUND: u8 = 6;
|
pub const ROUND: u8 = 6;
|
||||||
pub const SQRT: u8 = 7;
|
pub const SQRT: u8 = 7;
|
||||||
pub const LEN: u8 = 8;
|
pub const LEN: u8 = 8;
|
||||||
pub const TO_STRING: u8 = 9;
|
pub const TO_STRING: u8 = 9;
|
||||||
pub const TO_NUMBER: u8 = 10;
|
pub const TO_NUMBER: u8 = 10;
|
||||||
|
|
||||||
/// Lookup table: function name ��� ID
|
/// Lookup table: function name ��� ID
|
||||||
pub const NAMES: &[(&str, u8)] = &[
|
pub const NAMES: &[(&str, u8)] = &[
|
||||||
("rand", RAND),
|
("rand", RAND),
|
||||||
("abs", ABS),
|
("abs", ABS),
|
||||||
("min", MIN),
|
("min", MIN),
|
||||||
("max", MAX),
|
("max", MAX),
|
||||||
("floor", FLOOR),
|
("floor", FLOOR),
|
||||||
("ceil", CEIL),
|
("ceil", CEIL),
|
||||||
("round", ROUND),
|
("round", ROUND),
|
||||||
("sqrt", SQRT),
|
("sqrt", SQRT),
|
||||||
("len", LEN),
|
("len", LEN),
|
||||||
("toString", TO_STRING),
|
("toString", TO_STRING),
|
||||||
("toNumber", TO_NUMBER),
|
("toNumber", TO_NUMBER),
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Get function name by ID
|
/// Get function name by ID
|
||||||
pub fn name(id: u8) -> Option<&'static str> {
|
pub fn name(id: u8) -> Option<&'static str> {
|
||||||
NAMES.iter().find(|(_, i)| *i == id).map(|(n, _)| *n)
|
NAMES.iter().find(|(_, i)| *i == id).map(|(n, _)| *n)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get function ID by name
|
/// Get function ID by name
|
||||||
pub fn id(name: &str) -> Option<u8> {
|
pub fn id(name: &str) -> Option<u8> {
|
||||||
NAMES.iter().find(|(n, _)| *n == name).map(|(_, i)| *i)
|
NAMES.iter().find(|(n, _)| *n == name).map(|(_, i)| *i)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Bytecode opcodes
|
/// Bytecode opcodes
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum OpCodeByte {
|
pub enum OpCodeByte {
|
||||||
// Register operations
|
// Register operations
|
||||||
LoadConst = 0x10, // Load constant to register
|
LoadConst = 0x10, // Load constant to register
|
||||||
Move = 0x11, // Move value between registers
|
Move = 0x11, // Move value between registers
|
||||||
|
|
||||||
// Memory operations
|
// Memory operations
|
||||||
LoadLocal = 0x20, // Load local variable to register
|
LoadLocal = 0x20, // Load local variable to register
|
||||||
StoreLocal = 0x21, // Store register to local variable
|
StoreLocal = 0x21, // Store register to local variable
|
||||||
LoadGlobal = 0x22, // Load global variable to register
|
LoadGlobal = 0x22, // Load global variable to register
|
||||||
StoreGlobal = 0x23, // Store register to global variable
|
StoreGlobal = 0x23, // Store register to global variable
|
||||||
|
|
||||||
// Arithmetic operations
|
// Arithmetic operations
|
||||||
Add = 0x30, // Addition
|
Add = 0x30, // Addition
|
||||||
Sub = 0x31, // Subtraction
|
Sub = 0x31, // Subtraction
|
||||||
Mul = 0x32, // Multiplication
|
Mul = 0x32, // Multiplication
|
||||||
Div = 0x33, // Division
|
Div = 0x33, // Division
|
||||||
Neg = 0x34, // Negation
|
Neg = 0x34, // Negation
|
||||||
Mod = 0x35, // Modulo
|
Mod = 0x35, // Modulo
|
||||||
Pow = 0x36, // Power
|
Pow = 0x36, // Power
|
||||||
|
|
||||||
// Comparison operations
|
// Comparison operations
|
||||||
Lt = 0x40, // Less than
|
Lt = 0x40, // Less than
|
||||||
Lte = 0x41, // Less than or equal
|
Lte = 0x41, // Less than or equal
|
||||||
Gt = 0x42, // Greater than
|
Gt = 0x42, // Greater than
|
||||||
Gte = 0x43, // Greater than or equal
|
Gte = 0x43, // Greater than or equal
|
||||||
Eq = 0x44, // Equal
|
Eq = 0x44, // Equal
|
||||||
Neq = 0x45, // Not equal
|
Neq = 0x45, // Not equal
|
||||||
|
|
||||||
// Boolean operations
|
// Boolean operations
|
||||||
And = 0x50, // Logical AND
|
And = 0x50, // Logical AND
|
||||||
Or = 0x51, // Logical OR
|
Or = 0x51, // Logical OR
|
||||||
Not = 0x52, // Logical NOT
|
Not = 0x52, // Logical NOT
|
||||||
|
|
||||||
// Membership test
|
// Membership test
|
||||||
Contains = 0x53, // Check if value is in list/string
|
Contains = 0x53, // Check if value is in list/string
|
||||||
|
|
||||||
// Control flow
|
// Control flow
|
||||||
Jump = 0x60, // Should read 4-byte address
|
Jump = 0x60, // Should read 4-byte address
|
||||||
JumpIfFalse = 0x61, // Should read register + 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
|
// String operations
|
||||||
|
Concat = 0x80, // String concatenation
|
||||||
// Property & method calls
|
|
||||||
GetProperty = 0x91, // Get object property: dest, obj, name
|
// Property & method calls
|
||||||
SetProperty = 0x92, // Set object property: obj, name, value
|
GetProperty = 0x91, // Get object property: dest, obj, name
|
||||||
MethodCall = 0x90, // Call method on object
|
SetProperty = 0x92, // Set object property: obj, name, value
|
||||||
|
MethodCall = 0x90, // Call method on object
|
||||||
// Built-in functions
|
|
||||||
Log = 0xA0, // Print a value
|
// Built-in functions
|
||||||
CallExternal = 0xA1, // Call external (host) function
|
Log = 0xA0, // Print a value
|
||||||
CallDefault = 0xA2, // Call default (built-in) function by ID
|
CallExternal = 0xA1, // Call external (host) function
|
||||||
|
CallDefault = 0xA2, // Call default (built-in) function by ID
|
||||||
// Result
|
|
||||||
SetResult = 0xB0, // Set expression result (for return value)
|
// Result
|
||||||
ClearResult = 0xB1, // Clear expression result (assignment resets last 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
|
// End marker
|
||||||
}
|
End = 0xFF, // End of program
|
||||||
|
}
|
||||||
impl OpCodeByte {
|
|
||||||
/// Convert opcode to byte
|
impl OpCodeByte {
|
||||||
pub fn to_byte(self) -> u8 {
|
/// Convert opcode to byte
|
||||||
self as u8
|
pub fn to_byte(self) -> u8 {
|
||||||
}
|
self as u8
|
||||||
|
}
|
||||||
/// Static lookup table for fast byte to opcode conversion
|
|
||||||
const LOOKUP: [Option<OpCodeByte>; 256] = {
|
/// Static lookup table for fast byte to opcode conversion
|
||||||
let mut table = [None; 256];
|
const LOOKUP: [Option<OpCodeByte>; 256] = {
|
||||||
let mut i = 0;
|
let mut table = [None; 256];
|
||||||
while i < 256 {
|
let mut i = 0;
|
||||||
table[i] = match i as u8 {
|
while i < 256 {
|
||||||
0x10 => Some(OpCodeByte::LoadConst),
|
table[i] = match i as u8 {
|
||||||
0x11 => Some(OpCodeByte::Move),
|
0x10 => Some(OpCodeByte::LoadConst),
|
||||||
0x20 => Some(OpCodeByte::LoadLocal),
|
0x11 => Some(OpCodeByte::Move),
|
||||||
0x21 => Some(OpCodeByte::StoreLocal),
|
0x20 => Some(OpCodeByte::LoadLocal),
|
||||||
0x22 => Some(OpCodeByte::LoadGlobal),
|
0x21 => Some(OpCodeByte::StoreLocal),
|
||||||
0x23 => Some(OpCodeByte::StoreGlobal),
|
0x22 => Some(OpCodeByte::LoadGlobal),
|
||||||
0x30 => Some(OpCodeByte::Add),
|
0x23 => Some(OpCodeByte::StoreGlobal),
|
||||||
0x31 => Some(OpCodeByte::Sub),
|
0x30 => Some(OpCodeByte::Add),
|
||||||
0x32 => Some(OpCodeByte::Mul),
|
0x31 => Some(OpCodeByte::Sub),
|
||||||
0x33 => Some(OpCodeByte::Div),
|
0x32 => Some(OpCodeByte::Mul),
|
||||||
0x34 => Some(OpCodeByte::Neg),
|
0x33 => Some(OpCodeByte::Div),
|
||||||
0x35 => Some(OpCodeByte::Mod),
|
0x34 => Some(OpCodeByte::Neg),
|
||||||
0x36 => Some(OpCodeByte::Pow),
|
0x35 => Some(OpCodeByte::Mod),
|
||||||
0x40 => Some(OpCodeByte::Lt),
|
0x36 => Some(OpCodeByte::Pow),
|
||||||
0x41 => Some(OpCodeByte::Lte),
|
0x40 => Some(OpCodeByte::Lt),
|
||||||
0x42 => Some(OpCodeByte::Gt),
|
0x41 => Some(OpCodeByte::Lte),
|
||||||
0x43 => Some(OpCodeByte::Gte),
|
0x42 => Some(OpCodeByte::Gt),
|
||||||
0x44 => Some(OpCodeByte::Eq),
|
0x43 => Some(OpCodeByte::Gte),
|
||||||
0x45 => Some(OpCodeByte::Neq),
|
0x44 => Some(OpCodeByte::Eq),
|
||||||
0x50 => Some(OpCodeByte::And),
|
0x45 => Some(OpCodeByte::Neq),
|
||||||
0x51 => Some(OpCodeByte::Or),
|
0x50 => Some(OpCodeByte::And),
|
||||||
0x52 => Some(OpCodeByte::Not),
|
0x51 => Some(OpCodeByte::Or),
|
||||||
0x53 => Some(OpCodeByte::Contains),
|
0x52 => Some(OpCodeByte::Not),
|
||||||
0x60 => Some(OpCodeByte::Jump),
|
0x53 => Some(OpCodeByte::Contains),
|
||||||
0x61 => Some(OpCodeByte::JumpIfFalse),
|
0x60 => Some(OpCodeByte::Jump),
|
||||||
0x80 => Some(OpCodeByte::Concat),
|
0x61 => Some(OpCodeByte::JumpIfFalse),
|
||||||
0x90 => Some(OpCodeByte::MethodCall),
|
0x62 => Some(OpCodeByte::JumpIfTrue),
|
||||||
0x91 => Some(OpCodeByte::GetProperty),
|
0x80 => Some(OpCodeByte::Concat),
|
||||||
0x92 => Some(OpCodeByte::SetProperty),
|
0x90 => Some(OpCodeByte::MethodCall),
|
||||||
0xA0 => Some(OpCodeByte::Log),
|
0x91 => Some(OpCodeByte::GetProperty),
|
||||||
0xA1 => Some(OpCodeByte::CallExternal),
|
0x92 => Some(OpCodeByte::SetProperty),
|
||||||
0xA2 => Some(OpCodeByte::CallDefault),
|
0xA0 => Some(OpCodeByte::Log),
|
||||||
0xB0 => Some(OpCodeByte::SetResult),
|
0xA1 => Some(OpCodeByte::CallExternal),
|
||||||
0xB1 => Some(OpCodeByte::ClearResult),
|
0xA2 => Some(OpCodeByte::CallDefault),
|
||||||
0xFF => Some(OpCodeByte::End),
|
0xB0 => Some(OpCodeByte::SetResult),
|
||||||
_ => None,
|
0xB1 => Some(OpCodeByte::ClearResult),
|
||||||
};
|
0xFF => Some(OpCodeByte::End),
|
||||||
i += 1;
|
_ => None,
|
||||||
}
|
};
|
||||||
table
|
i += 1;
|
||||||
};
|
}
|
||||||
|
table
|
||||||
/// Convert byte to opcode
|
};
|
||||||
#[inline(always)]
|
|
||||||
pub fn from_byte(byte: u8) -> Option<Self> {
|
/// Convert byte to opcode
|
||||||
Self::LOOKUP[byte as usize]
|
#[inline(always)]
|
||||||
}
|
pub fn from_byte(byte: u8) -> Option<Self> {
|
||||||
|
Self::LOOKUP[byte as usize]
|
||||||
/// Get opcode name
|
}
|
||||||
pub fn name(&self) -> &'static str {
|
|
||||||
match self {
|
/// Get opcode name
|
||||||
OpCodeByte::LoadConst => "LoadConst",
|
pub fn name(&self) -> &'static str {
|
||||||
OpCodeByte::Move => "Move",
|
match self {
|
||||||
OpCodeByte::LoadLocal => "LoadLocal",
|
OpCodeByte::LoadConst => "LoadConst",
|
||||||
OpCodeByte::StoreLocal => "StoreLocal",
|
OpCodeByte::Move => "Move",
|
||||||
OpCodeByte::LoadGlobal => "LoadGlobal",
|
OpCodeByte::LoadLocal => "LoadLocal",
|
||||||
OpCodeByte::StoreGlobal => "StoreGlobal",
|
OpCodeByte::StoreLocal => "StoreLocal",
|
||||||
OpCodeByte::Add => "Add",
|
OpCodeByte::LoadGlobal => "LoadGlobal",
|
||||||
OpCodeByte::Sub => "Sub",
|
OpCodeByte::StoreGlobal => "StoreGlobal",
|
||||||
OpCodeByte::Mul => "Mul",
|
OpCodeByte::Add => "Add",
|
||||||
OpCodeByte::Div => "Div",
|
OpCodeByte::Sub => "Sub",
|
||||||
OpCodeByte::Neg => "Neg",
|
OpCodeByte::Mul => "Mul",
|
||||||
OpCodeByte::Mod => "Mod",
|
OpCodeByte::Div => "Div",
|
||||||
OpCodeByte::Pow => "Pow",
|
OpCodeByte::Neg => "Neg",
|
||||||
OpCodeByte::Lt => "Lt",
|
OpCodeByte::Mod => "Mod",
|
||||||
OpCodeByte::Lte => "Lte",
|
OpCodeByte::Pow => "Pow",
|
||||||
OpCodeByte::Gt => "Gt",
|
OpCodeByte::Lt => "Lt",
|
||||||
OpCodeByte::Gte => "Gte",
|
OpCodeByte::Lte => "Lte",
|
||||||
OpCodeByte::Eq => "Eq",
|
OpCodeByte::Gt => "Gt",
|
||||||
OpCodeByte::Neq => "Neq",
|
OpCodeByte::Gte => "Gte",
|
||||||
OpCodeByte::And => "And",
|
OpCodeByte::Eq => "Eq",
|
||||||
OpCodeByte::Or => "Or",
|
OpCodeByte::Neq => "Neq",
|
||||||
OpCodeByte::Not => "Not",
|
OpCodeByte::And => "And",
|
||||||
OpCodeByte::Contains => "Contains",
|
OpCodeByte::Or => "Or",
|
||||||
OpCodeByte::Jump => "Jump",
|
OpCodeByte::Not => "Not",
|
||||||
OpCodeByte::JumpIfFalse => "JumpIfFalse",
|
OpCodeByte::Contains => "Contains",
|
||||||
OpCodeByte::Concat => "Concat",
|
OpCodeByte::Jump => "Jump",
|
||||||
OpCodeByte::MethodCall => "MethodCall",
|
OpCodeByte::JumpIfFalse => "JumpIfFalse",
|
||||||
OpCodeByte::GetProperty => "GetProperty",
|
OpCodeByte::JumpIfTrue => "JumpIfTrue",
|
||||||
OpCodeByte::SetProperty => "SetProperty",
|
OpCodeByte::Concat => "Concat",
|
||||||
OpCodeByte::Log => "Log",
|
OpCodeByte::MethodCall => "MethodCall",
|
||||||
OpCodeByte::CallExternal => "CallExternal",
|
OpCodeByte::GetProperty => "GetProperty",
|
||||||
OpCodeByte::CallDefault => "Rand",
|
OpCodeByte::SetProperty => "SetProperty",
|
||||||
OpCodeByte::SetResult => "SetResult",
|
OpCodeByte::Log => "Log",
|
||||||
OpCodeByte::ClearResult => "ClearResult",
|
OpCodeByte::CallExternal => "CallExternal",
|
||||||
OpCodeByte::End => "End",
|
OpCodeByte::CallDefault => "Rand",
|
||||||
}
|
OpCodeByte::SetResult => "SetResult",
|
||||||
}
|
OpCodeByte::ClearResult => "ClearResult",
|
||||||
}
|
OpCodeByte::End => "End",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+77
-48
@@ -41,8 +41,8 @@ pub grammar parser() for str {
|
|||||||
= binary_op()
|
= binary_op()
|
||||||
|
|
||||||
pub rule mul_div() -> Expr =
|
pub rule mul_div() -> Expr =
|
||||||
left:power() mul_div_right:(
|
left:unary() mul_div_right:(
|
||||||
_ op:$("*" / "/" / "%") _ right:power()
|
_ op:$("*" / "/" / "%") _ right:unary()
|
||||||
{ (op, right) }
|
{ (op, right) }
|
||||||
)* {
|
)* {
|
||||||
let mut result = left;
|
let mut result = left;
|
||||||
@@ -57,31 +57,39 @@ pub grammar parser() for str {
|
|||||||
result
|
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 =
|
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 }
|
/ a:postfix() { a }
|
||||||
|
|
||||||
|
|
||||||
pub rule binary_op() -> Expr = precedence!{
|
pub rule binary_op() -> Expr = precedence!{
|
||||||
i:identifier() _ "(" args:((_ e:expression() _ {e}) ** ",") ")" { Expr::FunctionCall(i, args) }
|
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::And, 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::Add, Box::new(y)) }
|
x:(@) _ "==" _ y:@ { Expr::BinaryOp(Box::new(x), Op::Eq, Box::new(y)) }
|
||||||
x:@ _ "-" _ y:(@) { Expr::BinaryOp(Box::new(x),Op::Sub, 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 }
|
x:mul_div() { x }
|
||||||
--
|
--
|
||||||
p:postfix() { p }
|
p:unary() { p }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Postfix operations: property access and method calls with chaining
|
/// 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:number() { Expr::Value(Value::Number(i)) }
|
||||||
/ i:boolean_literal() { Expr::Value(i) }
|
/ i:boolean_literal() { Expr::Value(i) }
|
||||||
/ "(" e:expression() ")" { e }
|
/ "(" 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
|
pub rule string() -> SmolStr
|
||||||
= "\"" s:$(([^'"'] / "\\\"")*) "\"" {
|
= "\"" s:$(([^'"'] / "\\\"")*) "\"" {
|
||||||
@@ -197,40 +203,63 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_binary_op() {
|
fn test_binary_op() {
|
||||||
let res = parser::binary_op("1 + 2 * 3 - 4 / 5");
|
// Left-associative: ((1 + (2 * 3)) - (4 / 5))
|
||||||
if let Err(e) = &res {
|
let expr = parser::binary_op("1 + 2 * 3 - 4 / 5").expect("Failed to parse expression");
|
||||||
println!("{}", e);
|
match expr {
|
||||||
}
|
Expr::BinaryOp(left, Op::Sub, right) => {
|
||||||
if let Ok(expr) = res {
|
// right: 4 / 5
|
||||||
match expr {
|
match *right {
|
||||||
Expr::BinaryOp(left, Op::Add, right) => {
|
Expr::BinaryOp(l, Op::Div, r) => {
|
||||||
assert!(matches!(*left, Expr::Value(_)));
|
assert!(matches!(*l, Expr::Value(_)));
|
||||||
match *right {
|
assert!(matches!(*r, Expr::Value(_)));
|
||||||
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"),
|
|
||||||
}
|
}
|
||||||
|
_ => 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!("Expected subtraction at top level"),
|
||||||
panic!("Failed to parse expression");
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+5
-2
@@ -1,6 +1,6 @@
|
|||||||
use crate::ast::value::Value;
|
use crate::ast::value::Value;
|
||||||
use crate::opcodes::default_fn;
|
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::error::VMError;
|
||||||
use super::vm::VM;
|
use super::vm::VM;
|
||||||
@@ -134,7 +134,10 @@ impl<'a> VM<'a> {
|
|||||||
} else {
|
} else {
|
||||||
0
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+32
-1
@@ -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" => {
|
"charAt" => {
|
||||||
if args.is_empty() {
|
if args.is_empty() {
|
||||||
return Err(VMError::RuntimeError(
|
return Err(VMError::RuntimeError(
|
||||||
@@ -733,6 +733,37 @@ impl<'a> VM<'a> {
|
|||||||
_ => Err(VMError::RuntimeError("sort() requires a string field name".to_string())),
|
_ => 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<Decimal> = 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));
|
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)) {
|
if let Some(ext_method) = self.external_methods.as_ref().and_then(|m| m.get(&key)) {
|
||||||
|
|||||||
@@ -221,6 +221,7 @@ impl<'a> VM<'a> {
|
|||||||
OpCodeByte::Not => self.handle_not(),
|
OpCodeByte::Not => self.handle_not(),
|
||||||
OpCodeByte::Jump => self.handle_jump(),
|
OpCodeByte::Jump => self.handle_jump(),
|
||||||
OpCodeByte::JumpIfFalse => self.handle_jump_if_false(),
|
OpCodeByte::JumpIfFalse => self.handle_jump_if_false(),
|
||||||
|
OpCodeByte::JumpIfTrue => self.handle_jump_if_true(),
|
||||||
OpCodeByte::Concat => self.handle_concat(),
|
OpCodeByte::Concat => self.handle_concat(),
|
||||||
OpCodeByte::GetProperty => self.handle_get_property(),
|
OpCodeByte::GetProperty => self.handle_get_property(),
|
||||||
OpCodeByte::SetProperty => self.handle_set_property(),
|
OpCodeByte::SetProperty => self.handle_set_property(),
|
||||||
@@ -654,6 +655,32 @@ impl<'a> VM<'a> {
|
|||||||
Ok(())
|
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
|
// Opcode Handlers - String Operations
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -687,6 +714,22 @@ impl<'a> VM<'a> {
|
|||||||
let value = map.get(prop).cloned().unwrap_or(Value::Null);
|
let value = map.get(prop).cloned().unwrap_or(Value::Null);
|
||||||
self.registers[dest] = value;
|
self.registers[dest] = value;
|
||||||
}
|
}
|
||||||
|
// `.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) => {
|
Value::List(list) => {
|
||||||
// Property projection: list.field → extract field from each Object element
|
// Property projection: list.field → extract field from each Object element
|
||||||
let mut values: Vec<Value> = Vec::with_capacity(list.len());
|
let mut values: Vec<Value> = Vec::with_capacity(list.len());
|
||||||
|
|||||||
@@ -2069,3 +2069,138 @@ fn test_string_compare_in_condition() {
|
|||||||
"#;
|
"#;
|
||||||
assert_eq!(run_and_get_result(code), Value::Number(dec!(80)));
|
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)));
|
||||||
|
}
|
||||||
|
|||||||
@@ -1458,5 +1458,326 @@
|
|||||||
"o": {"type": "object", "value": {"name": "ali"}}
|
"o": {"type": "object", "value": {"name": "ali"}}
|
||||||
},
|
},
|
||||||
"expected": { "type": "boolean", "value": true }
|
"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 }
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
Generated
+2
-2
@@ -141,7 +141,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dexpr"
|
name = "dexpr"
|
||||||
version = "0.4.0"
|
version = "0.4.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bumpalo",
|
"bumpalo",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
@@ -159,7 +159,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dexpr-wasm"
|
name = "dexpr-wasm"
|
||||||
version = "0.4.0"
|
version = "0.4.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"dexpr",
|
"dexpr",
|
||||||
"getrandom 0.3.4",
|
"getrandom 0.3.4",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "dexpr-wasm"
|
name = "dexpr-wasm"
|
||||||
version = "0.4.0"
|
version = "0.4.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
|
|||||||
Reference in New Issue
Block a user