This commit is contained in:
2026-04-07 15:16:19 +03:00
parent 953b39d433
commit 0fa388abeb
25 changed files with 1370 additions and 38 deletions

2
.gitignore vendored
View File

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

View File

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

2
Cargo.lock generated
View File

@@ -304,7 +304,7 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
[[package]]
name = "dexpr"
version = "0.1.0"
version = "0.3.0"
dependencies = [
"bumpalo",
"criterion",

View File

@@ -1,6 +1,6 @@
[package]
name = "dexpr"
version = "0.1.0"
version = "0.3.0"
edition = "2021"
description = "Embeddable expression evaluator and bytecode VM"
license = "MIT"

View File

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

View File

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

View File

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

View File

@@ -137,17 +137,18 @@ struct VM<'a> {
### 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
- **`handle_get_property()`** — Object register'ından alan oku, alan yoksa `Null` döndür. List register'ında property projection yapar: her Object elemanından ilgili alanı çıkarıp NumberList/StringList/List döndürür
- **`handle_set_property()`** — Object register'ında alan değerini ayarla
- **`handle_method_call()`** — Nesne register'ı, metod adı, argümanlar
- **String metodları:** `upper`, `lower`, `trim`, `trimStart`, `trimEnd`, `split(delimiter)`, `replace(old, new)`, `startsWith(prefix)`, `endsWith(suffix)`, `contains(substr)`, `length`, `charAt(index)`, `substring(start, end?)`
- **StringList metodları:** `length`/`len`, `isEmpty`, `first`, `last`, `get(index)`, `contains(value)`, `indexOf(value)`, `slice(start, end?)`, `reverse()`, `sort()`, `join(delimiter?)`
- **NumberList metodları:** `length`/`len`, `isEmpty`, `first`, `last`, `get(index)`, `contains(value)`, `indexOf(value)`, `slice(start, end?)`, `reverse()`, `sort()`, `sum`, `avg`, `min`, `max`
- **Object metodları:** `keys()`, `values()`, `length`/`len()`, `contains(key)`, `get(key)`
- **List metodları:** `length`/`len`, `isEmpty`, `first`, `last`, `get(index)`, `contains(value)`, `indexOf(value)`, `slice(start, end?)`, `reverse()`, `join(delim?)`, `map(field)`, `filter(field, value?)`, `find(field, value?)`, `sort(field)`
- **Harici metodlar:** Yukarıdaki built-in metodlar bulunamazsa `external_methods` HashMap'inde aranır
### Üyelik Testi
- **`handle_contains()`** — `in` operatörü: String in StringList, Number in NumberList, String in String (substring), String in Object (anahtar varlığı kontrolü)
- **`handle_contains()`** — `in` operatörü: String in StringList, Number in NumberList, String in String (substring), String in Object (anahtar varlığı kontrolü), Value in List
### Harici Fonksiyonlar ve Sonuç
- **`handle_call_external()`** — İsimle harici fonksiyon çağır (HashMap lookup)
@@ -203,7 +204,7 @@ vm.register_method("Number", "format", |this, args| {
});
```
**Tip isimleri:** `"Number"`, `"String"`, `"Boolean"`, `"NumberList"`, `"StringList"`, `"Object"`, `"Null"`
**Tip isimleri:** `"Number"`, `"String"`, `"Boolean"`, `"NumberList"`, `"StringList"`, `"Object"`, `"List"`, `"Null"`
---

View File

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

15
editor/dist/index.cjs vendored
View File

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

View File

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

View File

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

15
editor/dist/index.js vendored
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -15,6 +15,7 @@ pub enum Value {
NumberList(Rc<Vec<Decimal>>),
StringList(Rc<Vec<SmolStr>>),
Object(Rc<IndexMap<SmolStr, Value>>),
List(Rc<Vec<Value>>),
}
/// Type tag constants for serialization
@@ -25,6 +26,7 @@ pub const TYPE_BOOLEAN: u8 = 0x03;
pub const TYPE_NUMBER_LIST: u8 = 0x04;
pub const TYPE_STRING_LIST: u8 = 0x05;
pub const TYPE_OBJECT: u8 = 0x06;
pub const TYPE_LIST: u8 = 0x07;
impl fmt::Display for Value {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@@ -63,6 +65,16 @@ impl fmt::Display for Value {
}
write!(f, "}}")
}
Value::List(list) => {
write!(f, "[")?;
for (i, val) in list.iter().enumerate() {
if i > 0 {
write!(f, ", ")?;
}
write!(f, "{}", val)?;
}
write!(f, "]")
}
}
}
}
@@ -78,6 +90,7 @@ impl Value {
Value::NumberList(_) => TYPE_NUMBER_LIST,
Value::StringList(_) => TYPE_STRING_LIST,
Value::Object(_) => TYPE_OBJECT,
Value::List(_) => TYPE_LIST,
}
}
@@ -91,6 +104,7 @@ impl Value {
Value::NumberList(_) => "NumberList",
Value::StringList(_) => "StringList",
Value::Object(_) => "Object",
Value::List(_) => "List",
}
}
@@ -150,6 +164,15 @@ impl Value {
bytes.extend_from_slice(&val.serialize());
}
}
Value::List(list) => {
// List length (2 bytes)
bytes.push((list.len() >> 8) as u8);
bytes.push(list.len() as u8);
// List items (recursive serialization)
for val in list.iter() {
bytes.extend_from_slice(&val.serialize());
}
}
}
bytes
@@ -226,6 +249,12 @@ impl From<IndexMap<SmolStr, Value>> for Value {
}
}
impl From<Vec<Value>> for Value {
fn from(v: Vec<Value>) -> Self {
Value::List(Rc::new(v))
}
}
impl Value {
/// Deserialize a value from bytes
pub fn deserialize(bytes: &[u8]) -> Result<(Value, usize), String> {
@@ -357,6 +386,22 @@ impl Value {
Ok((Value::Object(Rc::new(map)), pos))
}
TYPE_LIST => {
if bytes.len() < pos + 2 {
return Err("Insufficient bytes for List length".to_string());
}
let len = u16::from_be_bytes([bytes[pos], bytes[pos + 1]]) as usize;
pos += 2;
let mut list = Vec::with_capacity(len);
for _ in 0..len {
let (val, val_bytes) = Value::deserialize(&bytes[pos..])?;
pos += val_bytes;
list.push(val);
}
Ok((Value::List(Rc::new(list)), pos))
}
_ => Err(format!("Unknown type tag: {}", type_tag)),
}
}
@@ -426,7 +471,11 @@ impl Value {
.collect();
Ok(Value::StringList(Rc::new(strings)))
} else {
Err("Arrays must contain all numbers or all strings".to_string())
let mut items = Vec::with_capacity(arr.len());
for item in arr {
items.push(Self::from_json_value(item)?);
}
Ok(Value::List(Rc::new(items)))
}
}
serde_json::Value::Object(obj) => {

View File

@@ -121,6 +121,19 @@ impl LanguageInfo {
type_name: v.type_name().to_string(),
}).collect())
}
Value::List(list) => {
// For List of Objects, derive fields from the first Object element
list.iter().find_map(|item| {
if let Value::Object(map) = item {
Some(map.iter().map(|(k, v)| FieldInfo {
name: k.to_string(),
type_name: v.type_name().to_string(),
}).collect())
} else {
None
}
})
}
_ => None,
};
self.variables.push(VariableInfo {
@@ -276,6 +289,23 @@ fn builtin_methods() -> Vec<(&'static str, Vec<MethodInfo>)> {
MethodInfo { name: "contains", signature: "(key: String) -> Boolean", doc: Some("Check if key exists") },
MethodInfo { name: "get", signature: "(key: String) -> any", doc: Some("Get value by key") },
]),
("List", vec![
MethodInfo { name: "length", signature: "() -> Number", doc: Some("Number of elements") },
MethodInfo { name: "len", signature: "() -> Number", doc: None },
MethodInfo { name: "isEmpty", signature: "() -> Boolean", doc: None },
MethodInfo { name: "first", signature: "() -> any", doc: Some("First element") },
MethodInfo { name: "last", signature: "() -> any", doc: Some("Last element") },
MethodInfo { name: "get", signature: "(index: Number) -> any", doc: None },
MethodInfo { name: "contains", signature: "(value: any) -> Boolean", doc: None },
MethodInfo { name: "indexOf", signature: "(value: any) -> Number", doc: None },
MethodInfo { name: "slice", signature: "(start: Number, end?: Number) -> List", doc: None },
MethodInfo { name: "reverse", signature: "() -> List", doc: None },
MethodInfo { name: "join", signature: "(delim?: String) -> String", doc: None },
MethodInfo { name: "map", signature: "(field: String) -> NumberList | StringList | List", doc: Some("Extract field from each Object element") },
MethodInfo { name: "filter", signature: "(field: String, value?: any) -> List", doc: Some("Filter by field value or truthy field") },
MethodInfo { name: "find", signature: "(field: String, value?: any) -> any", doc: Some("Find first element matching field condition") },
MethodInfo { name: "sort", signature: "(field: String) -> List", doc: Some("Sort by field value") },
]),
("StringList", vec![
MethodInfo { name: "length", signature: "() -> Number", doc: None },
MethodInfo { name: "len", signature: "() -> Number", doc: None },

View File

@@ -160,6 +160,7 @@ impl<'a> VM<'a> {
Value::StringList(l) => Decimal::from(l.len()),
Value::NumberList(l) => Decimal::from(l.len()),
Value::Object(m) => Decimal::from(m.len()),
Value::List(l) => Decimal::from(l.len()),
other => {
return Err(VMError::RuntimeError(format!(
"len() not supported for type {}",

View File

@@ -26,6 +26,7 @@ impl<'a> VM<'a> {
Value::StringList(_) => self.dispatch_string_list_method_inner(&obj_val, method, args),
Value::NumberList(_) => self.dispatch_number_list_method_inner(&obj_val, method, args),
Value::Object(_) => self.dispatch_object_method_inner(&obj_val, method, args),
Value::List(_) => self.dispatch_list_method_inner(&obj_val, method, args),
_ => {
// Try external methods for any type
let type_name: SmolStr = obj_val.type_name().into();
@@ -452,9 +453,7 @@ impl<'a> VM<'a> {
.collect();
Ok(Value::NumberList(Rc::new(numbers)))
} else {
Err(VMError::RuntimeError(
"values() only works when all values are the same type (String or Number)".to_string(),
))
Ok(Value::List(Rc::new(vals)))
}
}
"length" | "len" => Ok(Value::Number(Decimal::from(map.len()))),
@@ -497,4 +496,254 @@ impl<'a> VM<'a> {
}
}
}
fn dispatch_list_method_inner(
&self,
obj_val: &Value,
method: &str,
args: &[Value],
) -> Result<Value, VMError> {
let list = match obj_val {
Value::List(l) => l,
_ => unreachable!(),
};
match method {
"length" | "len" => Ok(Value::Number(Decimal::from(list.len()))),
"isEmpty" => Ok(Value::Boolean(list.is_empty())),
"first" => Ok(list.first().cloned().unwrap_or(Value::Null)),
"last" => Ok(list.last().cloned().unwrap_or(Value::Null)),
"get" => {
if args.is_empty() {
return Err(VMError::RuntimeError("get() requires an index".to_string()));
}
match &args[0] {
Value::Number(idx) => {
let index = idx.to_usize().unwrap_or(usize::MAX);
Ok(list.get(index).cloned().unwrap_or(Value::Null))
}
_ => Err(VMError::RuntimeError("get() requires a number index".to_string())),
}
}
"contains" => {
if args.is_empty() {
return Err(VMError::RuntimeError("contains() requires an argument".to_string()));
}
Ok(Value::Boolean(list.contains(&args[0])))
}
"indexOf" => {
if args.is_empty() {
return Err(VMError::RuntimeError("indexOf() requires an argument".to_string()));
}
let idx = list.iter().position(|item| item == &args[0]);
Ok(idx.map(|i| Value::Number(Decimal::from(i))).unwrap_or(Value::Number(Decimal::from(-1))))
}
"slice" => {
if args.is_empty() {
return Err(VMError::RuntimeError("slice() requires at least a start index".to_string()));
}
match &args[0] {
Value::Number(start_idx) => {
let start = start_idx.to_usize().unwrap_or(0).min(list.len());
let end = if args.len() > 1 {
match &args[1] {
Value::Number(end_idx) => end_idx.to_usize().unwrap_or(list.len()).min(list.len()),
_ => list.len(),
}
} else {
list.len()
};
Ok(Value::List(Rc::new(list[start..end].to_vec())))
}
_ => Err(VMError::RuntimeError("slice() requires a number index".to_string())),
}
}
"reverse" => {
let mut reversed = list.to_vec();
reversed.reverse();
Ok(Value::List(Rc::new(reversed)))
}
"join" => {
let delim = if args.is_empty() {
""
} else {
match &args[0] {
Value::String(s) => s.as_str(),
_ => return Err(VMError::RuntimeError("join() requires a string delimiter".to_string())),
}
};
let strings: Vec<String> = list.iter().map(|v| format!("{}", v)).collect();
Ok(Value::String(SmolStr::new(strings.join(delim))))
}
"map" => {
// Property shorthand: list.map("fieldName") extracts a field from each Object element
if args.is_empty() {
return Err(VMError::RuntimeError("map() requires a field name argument".to_string()));
}
match &args[0] {
Value::String(field) => {
let mut values: Vec<Value> = Vec::with_capacity(list.len());
for item in list.iter() {
match item {
Value::Object(map) => {
values.push(map.get(field.as_str()).cloned().unwrap_or(Value::Null));
}
_ => {
return Err(VMError::RuntimeError(format!(
"map() requires all elements to be Objects, got {}",
item.type_name()
)));
}
}
}
// Smart return type: if all values are the same primitive type, return typed list
if values.is_empty() {
return Ok(Value::List(Rc::new(values)));
}
if values.iter().all(|v| matches!(v, Value::Number(_))) {
let nums: Vec<Decimal> = values
.into_iter()
.map(|v| match v {
Value::Number(n) => n,
_ => unreachable!(),
})
.collect();
Ok(Value::NumberList(Rc::new(nums)))
} else if values.iter().all(|v| matches!(v, Value::String(_))) {
let strings: Vec<SmolStr> = values
.into_iter()
.map(|v| match v {
Value::String(s) => s,
_ => unreachable!(),
})
.collect();
Ok(Value::StringList(Rc::new(strings)))
} else {
Ok(Value::List(Rc::new(values)))
}
}
_ => Err(VMError::RuntimeError("map() requires a string field name".to_string())),
}
}
"filter" => {
// Property shorthand:
// list.filter("active") — filter by truthy boolean field
// list.filter("field", value) — filter where field == value
if args.is_empty() {
return Err(VMError::RuntimeError("filter() requires a field name argument".to_string()));
}
match &args[0] {
Value::String(field) => {
let filtered: Result<Vec<Value>, VMError> = list
.iter()
.filter_map(|item| {
match item {
Value::Object(map) => {
let field_val = map.get(field.as_str()).cloned().unwrap_or(Value::Null);
if args.len() > 1 {
// filter("field", value) — equality check
if field_val == args[1] {
Some(Ok(item.clone()))
} else {
None
}
} else {
// filter("field") — truthy check
match &field_val {
Value::Boolean(b) => if *b { Some(Ok(item.clone())) } else { None },
Value::Null => None,
_ => Some(Ok(item.clone())),
}
}
}
_ => Some(Err(VMError::RuntimeError(format!(
"filter() requires all elements to be Objects, got {}",
item.type_name()
)))),
}
})
.collect();
Ok(Value::List(Rc::new(filtered?)))
}
_ => Err(VMError::RuntimeError("filter() requires a string field name".to_string())),
}
}
"find" => {
// Property shorthand: list.find("field", value) — first element where field == value
if args.is_empty() {
return Err(VMError::RuntimeError("find() requires a field name argument".to_string()));
}
match &args[0] {
Value::String(field) => {
for item in list.iter() {
match item {
Value::Object(map) => {
let field_val = map.get(field.as_str()).cloned().unwrap_or(Value::Null);
if args.len() > 1 {
if field_val == args[1] {
return Ok(item.clone());
}
} else {
// find("field") — first with truthy field
match &field_val {
Value::Boolean(b) => if *b { return Ok(item.clone()); },
Value::Null => {},
_ => return Ok(item.clone()),
}
}
}
_ => {
return Err(VMError::RuntimeError(format!(
"find() requires all elements to be Objects, got {}",
item.type_name()
)));
}
}
}
Ok(Value::Null)
}
_ => Err(VMError::RuntimeError("find() requires a string field name".to_string())),
}
}
"sort" => {
// Property shorthand: list.sort("field") — sort by field value
if args.is_empty() {
return Err(VMError::RuntimeError("sort() on List requires a field name argument".to_string()));
}
match &args[0] {
Value::String(field) => {
let mut sorted = list.to_vec();
sorted.sort_by(|a, b| {
let a_val = match a {
Value::Object(map) => map.get(field.as_str()).cloned().unwrap_or(Value::Null),
_ => Value::Null,
};
let b_val = match b {
Value::Object(map) => map.get(field.as_str()).cloned().unwrap_or(Value::Null),
_ => Value::Null,
};
match (&a_val, &b_val) {
(Value::Number(a), Value::Number(b)) => a.cmp(b),
(Value::String(a), Value::String(b)) => a.cmp(b),
_ => std::cmp::Ordering::Equal,
}
});
Ok(Value::List(Rc::new(sorted)))
}
_ => Err(VMError::RuntimeError("sort() requires a string field name".to_string())),
}
}
_ => {
let key = (SmolStr::new_static("List"), SmolStr::from(method));
if let Some(ext_method) = self.external_methods.as_ref().and_then(|m| m.get(&key)) {
ext_method(obj_val, args).map_err(VMError::RuntimeError)
} else {
Err(VMError::MethodNotFound {
type_name: "List",
method: SmolStr::from(method),
})
}
}
}
}
}

View File

@@ -597,6 +597,8 @@ impl<'a> VM<'a> {
}
// String in Object (key check)
(Value::String(key), Value::Object(map)) => map.contains_key(key),
// Value in List
(needle_val, Value::List(list)) => list.contains(needle_val),
(needle_val, haystack_val) => {
return Err(VMError::InvalidOperation {
operation: "in",
@@ -684,6 +686,42 @@ impl<'a> VM<'a> {
let value = map.get(prop).cloned().unwrap_or(Value::Null);
self.registers[dest] = value;
}
Value::List(list) => {
// Property projection: list.field → extract field from each Object element
let mut values: Vec<Value> = Vec::with_capacity(list.len());
for item in list.iter() {
match item {
Value::Object(map) => {
values.push(map.get(prop).cloned().unwrap_or(Value::Null));
}
_ => {
return Err(VMError::RuntimeError(format!(
"Cannot access property '{}' on non-Object element in List (got {})",
prop,
item.type_name()
)));
}
}
}
// Smart return type: NumberList/StringList when homogeneous
if values.is_empty() {
self.registers[dest] = Value::List(Rc::new(values));
} else if values.iter().all(|v| matches!(v, Value::Number(_))) {
let nums: Vec<Decimal> = values.into_iter().map(|v| match v {
Value::Number(n) => n,
_ => unreachable!(),
}).collect();
self.registers[dest] = Value::NumberList(Rc::new(nums));
} else if values.iter().all(|v| matches!(v, Value::String(_))) {
let strings: Vec<SmolStr> = values.into_iter().map(|v| match v {
Value::String(s) => s,
_ => unreachable!(),
}).collect();
self.registers[dest] = Value::StringList(Rc::new(strings));
} else {
self.registers[dest] = Value::List(Rc::new(values));
}
}
other => {
return Err(VMError::RuntimeError(format!(
"Cannot access property '{}' on type {}",

View File

@@ -1334,4 +1334,609 @@ fn test_from_json_full_workflow() {
"#;
let r = run_and_get_result_with_globals(code, vec![("customer", customer)]);
assert_eq!(r, Value::String("premium".into()));
}
// ==================== LIST (ARRAY OF OBJECTS) TESTS ====================
/// Build the invoice items test data
fn kalemler() -> Value {
let items: Vec<Value> = vec![
make_kalem(1, "Web Uygulama Gelistirme", 1, "Adet", dec!(45000), dec!(45000)),
make_kalem(2, "Mobil Uygulama Gelistirme", 1, "Adet", dec!(35000), dec!(35000)),
make_kalem(3, "UI/UX Tasarim Hizmeti", 40, "Saat", dec!(750), dec!(30000)),
make_kalem(4, "Sunucu Bakim Sozlesmesi (Yillik)", 1, "Adet", dec!(12000), dec!(12000)),
make_kalem(5, "SSL Sertifikasi", 3, "Adet", dec!(500), dec!(1500)),
make_kalem(6, "Veritabani Yonetimi", 12, "Ay", dec!(2000), dec!(24000)),
make_kalem(7, "API Entegrasyon Hizmeti", 1, "Adet", dec!(18000), dec!(18000)),
make_kalem(8, "Bulut Altyapi Kurulumu", 1, "Adet", dec!(8000), dec!(8000)),
make_kalem(9, "Siber Guvenlik Danismanligi", 20, "Saat", dec!(900), dec!(18000)),
make_kalem(10, "E-posta Sunucu Yapilandirmasi", 1, "Adet", dec!(3500), dec!(3500)),
make_kalem(11, "Yedekleme Sistemi Kurulumu", 1, "Adet", dec!(5000), dec!(5000)),
make_kalem(12, "SEO Optimizasyonu", 1, "Adet", dec!(7500), dec!(7500)),
make_kalem(13, "Egitim ve Dokumantasyon", 8, "Saat", dec!(600), dec!(4800)),
make_kalem(14, "Performans Testi ve Raporlama", 1, "Adet", dec!(6000), dec!(6000)),
make_kalem(15, "Teknik Destek Paketi (6 Ay)", 1, "Adet", dec!(9000), dec!(9000)),
];
Value::List(Rc::new(items))
}
fn make_kalem(sira_no: i64, adi: &str, miktar: i64, birim: &str, birim_fiyat: rust_decimal::Decimal, tutar: rust_decimal::Decimal) -> Value {
let mut map = IndexMap::new();
map.insert(SmolStr::new("siraNo"), Value::Number(rust_decimal::Decimal::from(sira_no)));
map.insert(SmolStr::new("adi"), Value::String(SmolStr::new(adi)));
map.insert(SmolStr::new("miktar"), Value::Number(rust_decimal::Decimal::from(miktar)));
map.insert(SmolStr::new("birim"), Value::String(SmolStr::new(birim)));
map.insert(SmolStr::new("birimFiyat"), Value::Number(birim_fiyat));
map.insert(SmolStr::new("tutar"), Value::Number(tutar));
Value::Object(Rc::new(map))
}
// --- List: length / len ---
#[test]
fn test_list_length() {
let result = run_expr_with_globals("kalemler.length()", vec![("kalemler", kalemler())]);
assert_eq!(result, Value::Number(dec!(15)));
}
#[test]
fn test_list_len() {
let result = run_expr_with_globals("kalemler.len()", vec![("kalemler", kalemler())]);
assert_eq!(result, Value::Number(dec!(15)));
}
// --- List: isEmpty ---
#[test]
fn test_list_is_empty_false() {
let result = run_expr_with_globals("kalemler.isEmpty()", vec![("kalemler", kalemler())]);
assert_eq!(result, Value::Boolean(false));
}
#[test]
fn test_list_is_empty_true() {
let empty = Value::List(Rc::new(vec![]));
let result = run_expr_with_globals("items.isEmpty()", vec![("items", empty)]);
assert_eq!(result, Value::Boolean(true));
}
// --- List: first / last ---
#[test]
fn test_list_first() {
let result = run_expr_with_globals("kalemler.first().adi", vec![("kalemler", kalemler())]);
assert_eq!(result, Value::String("Web Uygulama Gelistirme".into()));
}
#[test]
fn test_list_last() {
let result = run_expr_with_globals("kalemler.last().adi", vec![("kalemler", kalemler())]);
assert_eq!(result, Value::String("Teknik Destek Paketi (6 Ay)".into()));
}
#[test]
fn test_list_first_empty() {
let empty = Value::List(Rc::new(vec![]));
let result = run_expr_with_globals("items.first()", vec![("items", empty)]);
assert_eq!(result, Value::Null);
}
#[test]
fn test_list_last_empty() {
let empty = Value::List(Rc::new(vec![]));
let result = run_expr_with_globals("items.last()", vec![("items", empty)]);
assert_eq!(result, Value::Null);
}
// --- List: get ---
#[test]
fn test_list_get() {
let result = run_expr_with_globals("kalemler.get(2).adi", vec![("kalemler", kalemler())]);
assert_eq!(result, Value::String("UI/UX Tasarim Hizmeti".into()));
}
#[test]
fn test_list_get_out_of_bounds() {
let result = run_expr_with_globals("kalemler.get(100)", vec![("kalemler", kalemler())]);
assert_eq!(result, Value::Null);
}
// --- List: contains ---
#[test]
fn test_list_contains() {
let items = Value::List(Rc::new(vec![
Value::Number(dec!(1)),
Value::Number(dec!(2)),
Value::Number(dec!(3)),
]));
let result = run_expr_with_globals("items.contains(2)", vec![("items", items)]);
assert_eq!(result, Value::Boolean(true));
}
#[test]
fn test_list_contains_false() {
let items = Value::List(Rc::new(vec![
Value::Number(dec!(1)),
Value::Number(dec!(2)),
]));
let result = run_expr_with_globals("items.contains(5)", vec![("items", items)]);
assert_eq!(result, Value::Boolean(false));
}
// --- List: indexOf ---
#[test]
fn test_list_index_of() {
let items = Value::List(Rc::new(vec![
Value::String("a".into()),
Value::String("b".into()),
Value::String("c".into()),
]));
let result = run_expr_with_globals(r#"items.indexOf("b")"#, vec![("items", items)]);
assert_eq!(result, Value::Number(dec!(1)));
}
#[test]
fn test_list_index_of_not_found() {
let items = Value::List(Rc::new(vec![
Value::String("a".into()),
]));
let result = run_expr_with_globals(r#"items.indexOf("z")"#, vec![("items", items)]);
assert_eq!(result, Value::Number(dec!(-1)));
}
// --- List: slice ---
#[test]
fn test_list_slice() {
let result = run_expr_with_globals("kalemler.slice(0, 3).length()", vec![("kalemler", kalemler())]);
assert_eq!(result, Value::Number(dec!(3)));
}
#[test]
fn test_list_slice_to_end() {
let result = run_expr_with_globals("kalemler.slice(13).length()", vec![("kalemler", kalemler())]);
assert_eq!(result, Value::Number(dec!(2)));
}
// --- List: reverse ---
#[test]
fn test_list_reverse() {
let result = run_expr_with_globals("kalemler.reverse().first().adi", vec![("kalemler", kalemler())]);
assert_eq!(result, Value::String("Teknik Destek Paketi (6 Ay)".into()));
}
// --- List: join ---
#[test]
fn test_list_join() {
let items = Value::List(Rc::new(vec![
Value::Number(dec!(1)),
Value::Number(dec!(2)),
Value::Number(dec!(3)),
]));
let result = run_expr_with_globals(r#"items.join(", ")"#, vec![("items", items)]);
assert_eq!(result, Value::String("1, 2, 3".into()));
}
// --- List: map (property shorthand) ---
#[test]
fn test_list_map_number_field() {
// map("tutar") should return NumberList when all values are Number
let result = run_expr_with_globals(r#"kalemler.map("tutar").sum()"#, vec![("kalemler", kalemler())]);
// 45000+35000+30000+12000+1500+24000+18000+8000+18000+3500+5000+7500+4800+6000+9000 = 227300
assert_eq!(result, Value::Number(dec!(227300)));
}
#[test]
fn test_list_map_string_field() {
// map("birim") should return StringList when all values are String
let result = run_expr_with_globals(r#"kalemler.map("birim").contains("Saat")"#, vec![("kalemler", kalemler())]);
assert_eq!(result, Value::Boolean(true));
}
#[test]
fn test_list_map_string_field_join() {
let items = Value::List(Rc::new(vec![
make_kalem(1, "A", 1, "X", dec!(10), dec!(10)),
make_kalem(2, "B", 1, "Y", dec!(20), dec!(20)),
]));
let result = run_expr_with_globals(r#"items.map("adi").join(", ")"#, vec![("items", items)]);
assert_eq!(result, Value::String("A, B".into()));
}
// --- The main use case: kalemler.map("tutar").sum() * kdvOrani ---
#[test]
fn test_list_map_sum_multiply() {
let code = r#"kalemler.map("tutar").sum() * kdvOrani"#;
let result = run_expr_with_globals(code, vec![
("kalemler", kalemler()),
("kdvOrani", Value::Number(dec!(0.20))),
]);
// 227300 * 0.20 = 45460
assert_eq!(result, Value::Number(dec!(45460.00)));
}
#[test]
fn test_list_map_avg() {
let result = run_expr_with_globals(r#"kalemler.map("tutar").avg()"#, vec![("kalemler", kalemler())]);
// 227300 / 15 = 15153.333...
let avg = result.clone();
match avg {
Value::Number(n) => {
let rounded = n.round_dp(2);
assert_eq!(rounded, dec!(15153.33));
}
_ => panic!("Expected Number, got {:?}", avg),
}
}
#[test]
fn test_list_map_min() {
let result = run_expr_with_globals(r#"kalemler.map("tutar").min()"#, vec![("kalemler", kalemler())]);
assert_eq!(result, Value::Number(dec!(1500)));
}
#[test]
fn test_list_map_max() {
let result = run_expr_with_globals(r#"kalemler.map("tutar").max()"#, vec![("kalemler", kalemler())]);
assert_eq!(result, Value::Number(dec!(45000)));
}
// --- List: filter ---
#[test]
fn test_list_filter_by_value() {
let result = run_expr_with_globals(
r#"kalemler.filter("birim", "Saat").length()"#,
vec![("kalemler", kalemler())],
);
// Items 3, 9, 13 have birim="Saat"
assert_eq!(result, Value::Number(dec!(3)));
}
#[test]
fn test_list_filter_by_value_sum() {
let result = run_expr_with_globals(
r#"kalemler.filter("birim", "Saat").map("tutar").sum()"#,
vec![("kalemler", kalemler())],
);
// 30000 + 18000 + 4800 = 52800
assert_eq!(result, Value::Number(dec!(52800)));
}
#[test]
fn test_list_filter_by_boolean_field() {
let items = Value::List(Rc::new(vec![
{
let mut m = IndexMap::new();
m.insert(SmolStr::new("name"), Value::String("A".into()));
m.insert(SmolStr::new("active"), Value::Boolean(true));
Value::Object(Rc::new(m))
},
{
let mut m = IndexMap::new();
m.insert(SmolStr::new("name"), Value::String("B".into()));
m.insert(SmolStr::new("active"), Value::Boolean(false));
Value::Object(Rc::new(m))
},
{
let mut m = IndexMap::new();
m.insert(SmolStr::new("name"), Value::String("C".into()));
m.insert(SmolStr::new("active"), Value::Boolean(true));
Value::Object(Rc::new(m))
},
]));
let result = run_expr_with_globals(r#"items.filter("active").length()"#, vec![("items", items)]);
assert_eq!(result, Value::Number(dec!(2)));
}
// --- List: find ---
#[test]
fn test_list_find_by_value() {
let result = run_expr_with_globals(
r#"kalemler.find("siraNo", 5).adi"#,
vec![("kalemler", kalemler())],
);
assert_eq!(result, Value::String("SSL Sertifikasi".into()));
}
#[test]
fn test_list_find_not_found() {
let result = run_expr_with_globals(
r#"kalemler.find("siraNo", 99)"#,
vec![("kalemler", kalemler())],
);
assert_eq!(result, Value::Null);
}
// --- List: sort ---
#[test]
fn test_list_sort_by_field() {
let result = run_expr_with_globals(
r#"kalemler.sort("tutar").first().adi"#,
vec![("kalemler", kalemler())],
);
// Smallest tutar is 1500 (SSL Sertifikasi)
assert_eq!(result, Value::String("SSL Sertifikasi".into()));
}
#[test]
fn test_list_sort_by_field_last() {
let result = run_expr_with_globals(
r#"kalemler.sort("tutar").last().adi"#,
vec![("kalemler", kalemler())],
);
// Largest tutar is 45000 (Web Uygulama Gelistirme)
assert_eq!(result, Value::String("Web Uygulama Gelistirme".into()));
}
// --- List: in operator ---
#[test]
fn test_list_in_operator() {
let items = Value::List(Rc::new(vec![
Value::String("a".into()),
Value::String("b".into()),
Value::String("c".into()),
]));
let result = run_expr_with_globals(r#""b" in items"#, vec![("items", items)]);
assert_eq!(result, Value::Boolean(true));
}
#[test]
fn test_list_in_operator_false() {
let items = Value::List(Rc::new(vec![
Value::Number(dec!(1)),
Value::Number(dec!(2)),
]));
let result = run_expr_with_globals("5 in items", vec![("items", items)]);
assert_eq!(result, Value::Boolean(false));
}
// --- List: len() builtin function ---
#[test]
fn test_list_builtin_len() {
let result = run_expr_with_globals("len(kalemler)", vec![("kalemler", kalemler())]);
assert_eq!(result, Value::Number(dec!(15)));
}
// --- List: from_json ---
#[test]
fn test_list_from_json() {
let json = r#"[{"a": 1}, {"a": 2}]"#;
let val = Value::from_json(json).unwrap();
match &val {
Value::List(list) => {
assert_eq!(list.len(), 2);
match &list[0] {
Value::Object(map) => assert_eq!(map.get("a"), Some(&Value::Number(dec!(1)))),
other => panic!("Expected Object, got {:?}", other),
}
}
other => panic!("Expected List, got {:?}", other),
}
}
#[test]
fn test_list_from_json_mixed() {
// Mixed types in array → List
let json = r#"[1, "hello", true]"#;
let val = Value::from_json(json).unwrap();
match &val {
Value::List(list) => {
assert_eq!(list.len(), 3);
assert_eq!(list[0], Value::Number(dec!(1)));
assert_eq!(list[1], Value::String("hello".into()));
assert_eq!(list[2], Value::Boolean(true));
}
other => panic!("Expected List, got {:?}", other),
}
}
// --- List: serialization round-trip ---
#[test]
fn test_list_serialize_deserialize() {
let original = Value::List(Rc::new(vec![
Value::Number(dec!(42)),
Value::String("hello".into()),
Value::Boolean(true),
]));
let bytes = original.serialize();
let (deserialized, _) = Value::deserialize(&bytes).unwrap();
assert_eq!(original, deserialized);
}
#[test]
fn test_list_serialize_deserialize_objects() {
let original = kalemler();
let bytes = original.serialize();
let (deserialized, _) = Value::deserialize(&bytes).unwrap();
assert_eq!(original, deserialized);
}
// --- List: chained operations ---
#[test]
fn test_list_chain_filter_map_sum() {
// Filter only "Adet" items, then sum their tutars
let code = r#"kalemler.filter("birim", "Adet").map("tutar").sum()"#;
let result = run_expr_with_globals(code, vec![("kalemler", kalemler())]);
// Adet items: 45000+35000+12000+1500+18000+8000+3500+5000+7500+6000+9000 = 150500
assert_eq!(result, Value::Number(dec!(150500)));
}
#[test]
fn test_list_chain_filter_length() {
let code = r#"kalemler.filter("birim", "Ay").length()"#;
let result = run_expr_with_globals(code, vec![("kalemler", kalemler())]);
// Only item 6 (Veritabani Yonetimi) has birim="Ay"
assert_eq!(result, Value::Number(dec!(1)));
}
#[test]
fn test_list_chain_sort_slice_map() {
// Sort by tutar, take top 3, get names
let code = r#"kalemler.sort("tutar").reverse().slice(0, 3).map("adi").join(", ")"#;
let result = run_expr_with_globals(code, vec![("kalemler", kalemler())]);
assert_eq!(result, Value::String("Web Uygulama Gelistirme, Mobil Uygulama Gelistirme, UI/UX Tasarim Hizmeti".into()));
}
// --- Object values() now returns List for mixed types ---
#[test]
fn test_object_values_mixed_returns_list() {
let mut map = IndexMap::new();
map.insert(SmolStr::new("name"), Value::String("Alice".into()));
map.insert(SmolStr::new("age"), Value::Number(dec!(30)));
let obj = Value::Object(Rc::new(map));
let result = run_expr_with_globals("item.values().length()", vec![("item", obj)]);
assert_eq!(result, Value::Number(dec!(2)));
}
// --- List: Display ---
#[test]
fn test_list_display() {
let list = Value::List(Rc::new(vec![
Value::Number(dec!(1)),
Value::String("hello".into()),
Value::Boolean(true),
]));
assert_eq!(format!("{}", list), r#"[1, "hello", true]"#);
}
// --- List: complex invoice scenario ---
#[test]
fn test_invoice_total_with_kdv() {
let code = r#"
araToplam = kalemler.map("tutar").sum()
kdv = araToplam * kdvOrani
result = araToplam + kdv
"#;
let result = run_and_get_result_with_globals(code, vec![
("kalemler", kalemler()),
("kdvOrani", Value::Number(dec!(0.20))),
]);
// 227300 + 45460 = 272760
assert_eq!(result, Value::Number(dec!(272760.00)));
}
#[test]
fn test_invoice_item_count_by_birim() {
let code = r#"
adetSayisi = kalemler.filter("birim", "Adet").length()
saatSayisi = kalemler.filter("birim", "Saat").length()
result = adetSayisi + saatSayisi
"#;
let result = run_and_get_result_with_globals(code, vec![("kalemler", kalemler())]);
// 11 Adet + 3 Saat = 14
assert_eq!(result, Value::Number(dec!(14)));
}
#[test]
fn test_invoice_find_and_access_property() {
let code = r#"kalemler.find("adi", "SSL Sertifikasi").birimFiyat"#;
let result = run_expr_with_globals(code, vec![("kalemler", kalemler())]);
assert_eq!(result, Value::Number(dec!(500)));
}
// ==================== LIST PROPERTY PROJECTION TESTS ====================
#[test]
fn test_list_property_projection_number() {
// kalemler.tutar → NumberList
let result = run_expr_with_globals("kalemler.tutar.sum()", vec![("kalemler", kalemler())]);
assert_eq!(result, Value::Number(dec!(227300)));
}
#[test]
fn test_list_property_projection_string() {
// kalemler.adi → StringList
let result = run_expr_with_globals(r#"kalemler.birim.contains("Saat")"#, vec![("kalemler", kalemler())]);
assert_eq!(result, Value::Boolean(true));
}
#[test]
fn test_list_property_projection_sum_multiply() {
// The main use case: kalemler.tutar.sum() * kdvOrani
let code = "kalemler.tutar.sum() * kdvOrani";
let result = run_expr_with_globals(code, vec![
("kalemler", kalemler()),
("kdvOrani", Value::Number(dec!(0.20))),
]);
assert_eq!(result, Value::Number(dec!(45460.00)));
}
#[test]
fn test_list_property_projection_min_max() {
let result = run_expr_with_globals("kalemler.tutar.min()", vec![("kalemler", kalemler())]);
assert_eq!(result, Value::Number(dec!(1500)));
let result = run_expr_with_globals("kalemler.tutar.max()", vec![("kalemler", kalemler())]);
assert_eq!(result, Value::Number(dec!(45000)));
}
#[test]
fn test_list_property_projection_join() {
let items = Value::List(Rc::new(vec![
make_kalem(1, "A", 1, "X", dec!(10), dec!(10)),
make_kalem(2, "B", 1, "Y", dec!(20), dec!(20)),
]));
let result = run_expr_with_globals(r#"items.adi.join(", ")"#, vec![("items", items)]);
assert_eq!(result, Value::String("A, B".into()));
}
#[test]
fn test_list_property_projection_chained_with_filter() {
// filter → projection: kalemler.filter("birim", "Saat").tutar.sum()
let code = r#"kalemler.filter("birim", "Saat").tutar.sum()"#;
let result = run_expr_with_globals(code, vec![("kalemler", kalemler())]);
// Saat items: 30000 + 18000 + 4800 = 52800
assert_eq!(result, Value::Number(dec!(52800)));
}
#[test]
fn test_list_property_projection_empty() {
let empty = Value::List(Rc::new(vec![]));
let result = run_expr_with_globals("items.tutar", vec![("items", empty)]);
match result {
Value::List(l) => assert!(l.is_empty()),
_ => panic!("Expected empty List, got {:?}", result),
}
}
#[test]
fn test_list_property_projection_with_sort() {
// sort → projection
let code = r#"kalemler.sort("tutar").adi.first()"#;
let result = run_expr_with_globals(code, vec![("kalemler", kalemler())]);
assert_eq!(result, Value::String("SSL Sertifikasi".into()));
}
#[test]
fn test_invoice_full_scenario() {
let code = r#"
araToplam = kalemler.tutar.sum()
kdv = araToplam * kdvOrani
genelToplam = araToplam + kdv
saatlikHizmetler = kalemler.filter("birim", "Saat").tutar.sum()
result = genelToplam
"#;
let result = run_and_get_result_with_globals(code, vec![
("kalemler", kalemler()),
("kdvOrani", Value::Number(dec!(0.20))),
]);
assert_eq!(result, Value::Number(dec!(272760.00)));
}

View File

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

View File

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