Compare commits
40
Commits
v0.1.2
..
e0a9a8efb7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e0a9a8efb7 | ||
|
|
848c7f169f | ||
|
|
8c50dd202d | ||
|
|
cc6ab40807 | ||
|
|
8198b615a8 | ||
|
|
62aa81b91c | ||
|
|
9c46635ee0 | ||
|
|
515adc2533 | ||
|
|
d9cf24d0eb | ||
|
|
d28edcf041 | ||
|
|
e862e53f3e | ||
|
|
610358110f | ||
|
|
bdaccd9803 | ||
|
|
08603dc4b5 | ||
|
|
165daada23 | ||
|
|
46b2b3b1b6 | ||
|
|
5a05da6fe4 | ||
|
|
aa0f464320 | ||
|
|
fc1f3e0b25 | ||
|
|
8eac5c5cb4 | ||
|
|
59cb8f31e4 | ||
|
|
4eee2cd2c9 | ||
|
|
44b6041e60 | ||
|
|
a95afe0463 | ||
|
|
43ab6244ef | ||
|
|
1d4503b015 | ||
|
|
250e29e00c | ||
|
|
1080081cec | ||
|
|
43f692e75c | ||
|
|
5b26d071ca | ||
|
|
a653f0b7c6 | ||
|
|
384d17b539 | ||
|
|
09a84264ab | ||
|
|
0a8c261545 | ||
|
|
a8a46998a8 | ||
|
|
e4a9f80617 | ||
|
|
0e2a7b35d2 | ||
|
|
ac99c4cb4e | ||
|
|
22f6bc1ab9 | ||
|
|
16006d000a |
Generated
+182
-8
@@ -11,6 +11,12 @@ dependencies = [
|
|||||||
"memchr",
|
"memchr",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bitflags"
|
||||||
|
version = "1.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "bumpalo"
|
name = "bumpalo"
|
||||||
version = "3.20.3"
|
version = "3.20.3"
|
||||||
@@ -23,9 +29,24 @@ version = "1.0.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-channel"
|
||||||
|
version = "0.5.16"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-utils",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "crossbeam-utils"
|
||||||
|
version = "0.8.22"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "decodal"
|
name = "decodal"
|
||||||
version = "0.1.2"
|
version = "0.4.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"decodal-derive",
|
"decodal-derive",
|
||||||
"regex",
|
"regex",
|
||||||
@@ -33,36 +54,143 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "decodal-cli"
|
name = "decodal-cli"
|
||||||
version = "0.1.2"
|
version = "0.4.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"decodal",
|
"decodal",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "decodal-derive"
|
name = "decodal-derive"
|
||||||
version = "0.1.2"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"decodal",
|
"decodal",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "decodal-wasm"
|
name = "decodal-language-service"
|
||||||
version = "0.1.2"
|
version = "0.4.0"
|
||||||
|
dependencies = [
|
||||||
|
"decodal",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "decodal-language-tools"
|
||||||
|
version = "0.4.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"decodal",
|
"decodal",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "decodal-lsp"
|
||||||
|
version = "0.4.0"
|
||||||
|
dependencies = [
|
||||||
|
"decodal",
|
||||||
|
"decodal-language-service",
|
||||||
|
"decodal-language-tools",
|
||||||
|
"lsp-server",
|
||||||
|
"lsp-types",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "decodal-wasm"
|
||||||
|
version = "0.4.0"
|
||||||
|
dependencies = [
|
||||||
|
"decodal",
|
||||||
|
"decodal-language-service",
|
||||||
|
"js-sys",
|
||||||
|
"serde-wasm-bindgen",
|
||||||
|
"serde_json",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fluent-uri"
|
||||||
|
version = "0.1.4"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-core"
|
||||||
|
version = "0.3.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-task"
|
||||||
|
version = "0.3.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-util"
|
||||||
|
version = "0.3.34"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"futures-task",
|
||||||
|
"pin-project-lite",
|
||||||
|
"slab",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "itoa"
|
name = "itoa"
|
||||||
version = "1.0.18"
|
version = "1.0.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "js-sys"
|
||||||
|
version = "0.3.102"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"futures-util",
|
||||||
|
"wasm-bindgen",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "log"
|
||||||
|
version = "0.4.33"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lsp-server"
|
||||||
|
version = "0.10.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3ee25a31f2e571e426eef2896179450cafc7e2f5be00d8a93b1c2d21c0ff7656"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-channel",
|
||||||
|
"log",
|
||||||
|
"serde",
|
||||||
|
"serde_derive",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "lsp-types"
|
||||||
|
version = "0.97.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "53353550a17c04ac46c585feb189c2db82154fc84b79c7a66c96c2c644f66071"
|
||||||
|
dependencies = [
|
||||||
|
"bitflags",
|
||||||
|
"fluent-uri",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"serde_repr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.8.2"
|
version = "2.8.2"
|
||||||
@@ -75,6 +203,12 @@ version = "1.21.4"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pin-project-lite"
|
||||||
|
version = "0.2.17"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.106"
|
version = "1.0.106"
|
||||||
@@ -135,6 +269,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"serde_core",
|
"serde_core",
|
||||||
|
"serde_derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde-wasm-bindgen"
|
||||||
|
version = "0.6.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b"
|
||||||
|
dependencies = [
|
||||||
|
"js-sys",
|
||||||
|
"serde",
|
||||||
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -154,7 +300,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -170,6 +316,23 @@ dependencies = [
|
|||||||
"zmij",
|
"zmij",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_repr"
|
||||||
|
version = "0.1.21"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 3.0.3",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "slab"
|
||||||
|
version = "0.4.12"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "syn"
|
name = "syn"
|
||||||
version = "2.0.117"
|
version = "2.0.117"
|
||||||
@@ -181,6 +344,17 @@ dependencies = [
|
|||||||
"unicode-ident",
|
"unicode-ident",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "syn"
|
||||||
|
version = "3.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"unicode-ident",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.24"
|
version = "1.0.24"
|
||||||
@@ -219,7 +393,7 @@ dependencies = [
|
|||||||
"bumpalo",
|
"bumpalo",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.117",
|
||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
+5
-1
@@ -4,11 +4,15 @@ members = [
|
|||||||
"crates/decodal-cli",
|
"crates/decodal-cli",
|
||||||
"crates/decodal-wasm",
|
"crates/decodal-wasm",
|
||||||
"crates/decodal-derive",
|
"crates/decodal-derive",
|
||||||
|
"crates/decodal-language-tools",
|
||||||
|
"crates/decodal-language-service",
|
||||||
|
"crates/decodal-lsp",
|
||||||
]
|
]
|
||||||
|
exclude = ["editors/tree-sitter-decodal"]
|
||||||
resolver = "2"
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.1.2"
|
version = "0.4.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.85"
|
rust-version = "1.85"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
|
|||||||
@@ -4,29 +4,34 @@ Decodal is a small deterministic DSL for describing, composing, validating, and
|
|||||||
|
|
||||||
It is designed around a lightweight Rust library:
|
It is designed around a lightweight Rust library:
|
||||||
|
|
||||||
- host-supplied imports through `SourceLoader`
|
- host-supplied source and structured-value imports through `ImportLoader`
|
||||||
|
- shared host environments for production and semantic editor tooling
|
||||||
- no filesystem access in the library core
|
- no filesystem access in the library core
|
||||||
- concrete and abstract values with constraints and defaults
|
- concrete and abstract values with constraints and defaults
|
||||||
|
- asymmetric range refinement with `narrower as wider`
|
||||||
|
- homogeneous associative-array schemas with `{...valueSchema}` and object rest constraints
|
||||||
|
- the safe top range `Unknown`, which remains abstract until refined, defaulted, or supplied concretely
|
||||||
- deterministic expression evaluation
|
- deterministic expression evaluation
|
||||||
- optional regex support behind a Cargo feature
|
- optional regex support behind a Cargo feature
|
||||||
- browser playground support through WebAssembly
|
- browser playground support through WebAssembly
|
||||||
|
|
||||||
## Library crate
|
## Library crate
|
||||||
|
|
||||||
Embedded hosts should depend on `decodal` and provide imports with a `SourceLoader`.
|
Embedded hosts should depend on `decodal` and provide imports with an `ImportLoader`.
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[dependencies]
|
[dependencies]
|
||||||
decodal = "0.1"
|
decodal = "0.4.0"
|
||||||
```
|
```
|
||||||
|
|
||||||
## Derive support
|
## Derive support
|
||||||
|
|
||||||
For embedded Rust applications, Decodal can generate a schema and typed decoder from a Rust struct with the `derive` feature.
|
For embedded Rust applications, Decodal can generate a schema and typed decoder from a Rust struct with the `derive` feature.
|
||||||
|
Map fields marked with `#[decodal(rest)]` receive additional object fields; `BTreeMap<String, Data>` corresponds to `...Unknown`.
|
||||||
|
|
||||||
```toml
|
```toml
|
||||||
[dependencies]
|
[dependencies]
|
||||||
decodal = { version = "0.1", features = ["derive"] }
|
decodal = { version = "0.4.0", features = ["derive"] }
|
||||||
```
|
```
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
@@ -44,7 +49,7 @@ struct Service {
|
|||||||
|
|
||||||
The derive implements:
|
The derive implements:
|
||||||
|
|
||||||
- `DecodalSchema`, which produces a host schema for `Engine::bind_global`
|
- `DecodalSchema`, which produces a `Value` range for `Engine::bind_global`
|
||||||
- `DecodalDecode`, which converts materialized `Data` into the Rust struct
|
- `DecodalDecode`, which converts materialized `Data` into the Rust struct
|
||||||
|
|
||||||
## CLI
|
## CLI
|
||||||
@@ -64,6 +69,26 @@ Enable optional regex support when needed:
|
|||||||
cargo run -q -p decodal-cli --features regex -- examples/regex/main.dcdl
|
cargo run -q -p decodal-cli --features regex -- examples/regex/main.dcdl
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Language server
|
||||||
|
|
||||||
|
The `decodal-language-service` crate provides transport-independent evaluation and completion, while `decodal-lsp` exposes it through the Language Server Protocol with live semantic diagnostics and document formatting.
|
||||||
|
Its library API accepts the same host environment used for production evaluation, so application-defined globals and structured import routing remain available in the editor.
|
||||||
|
|
||||||
|
```toml
|
||||||
|
[dependencies]
|
||||||
|
decodal = "0.4.0"
|
||||||
|
decodal-language-service = "0.4.0"
|
||||||
|
decodal-lsp = "0.4.0"
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the default filesystem-backed server from the repository:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo run -q -p decodal-lsp
|
||||||
|
```
|
||||||
|
|
||||||
|
Embedded applications can implement `decodal_lsp::LspEnvironment` for their existing `HostEnvironment` and construct it from the `InitializeParams` passed to `decodal_lsp::run_stdio`.
|
||||||
|
|
||||||
## Web playground
|
## Web playground
|
||||||
|
|
||||||
The static documentation site and browser playground live under:
|
The static documentation site and browser playground live under:
|
||||||
@@ -72,6 +97,8 @@ The static documentation site and browser playground live under:
|
|||||||
site/decodal-site/
|
site/decodal-site/
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Browser hosts can use `decodal-wasm`. Its `DecodalLanguageService` accepts JavaScript-owned `globals`, `loadImport`, and `completeImport` callbacks; filesystem and virtual-project policy remain outside the package.
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Licensed under either of:
|
Licensed under either of:
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
# Releasing Decodal
|
||||||
|
|
||||||
|
Run all commands from the repository root. Publishing requires Cargo, npm, and JSR credentials; validation does not.
|
||||||
|
|
||||||
|
## Versions
|
||||||
|
|
||||||
|
- `decodal-derive`: `0.2.0`
|
||||||
|
- Other published Rust crates: `0.4.0`
|
||||||
|
- `decodal-wasm` npm/JSR package: `0.3.0`
|
||||||
|
- `decodal-codemirror`: `0.3.0`
|
||||||
|
|
||||||
|
The npm packages advertise the repository's dual `MIT OR Apache-2.0` license.
|
||||||
|
JSR metadata uses the single `MIT` identifier required by its publisher; both license files remain included in every package.
|
||||||
|
|
||||||
|
## Validate
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo fmt --check
|
||||||
|
cargo test --workspace
|
||||||
|
cargo test -p decodal --no-default-features
|
||||||
|
cargo clippy --workspace --all-targets -- -D warnings
|
||||||
|
cargo clippy -p decodal-wasm --target wasm32-unknown-unknown -- -D warnings
|
||||||
|
cargo clippy -p decodal-language-tools --target wasm32-unknown-unknown -- -D warnings
|
||||||
|
npm --prefix site/decodal-site run build:wasm
|
||||||
|
npm --prefix packages/decodal-codemirror test
|
||||||
|
npm --prefix site/decodal-site test
|
||||||
|
npm --prefix site/decodal-site run build
|
||||||
|
deno check packages/decodal-wasm/mod.ts
|
||||||
|
npm pack --dry-run ./packages/decodal-wasm
|
||||||
|
deno publish --dry-run --config packages/decodal-wasm/jsr.json
|
||||||
|
npm pack --dry-run ./packages/decodal-codemirror
|
||||||
|
deno publish --dry-run --config packages/decodal-codemirror/jsr.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Publish Rust crates
|
||||||
|
|
||||||
|
Publish in dependency order. Wait for each crate to become available in the crates.io index before continuing.
|
||||||
|
Run the corresponding command with `--dry-run` immediately before each real publish.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo publish -p decodal-derive
|
||||||
|
cargo publish -p decodal
|
||||||
|
cargo publish -p decodal-language-service
|
||||||
|
cargo publish -p decodal-language-tools
|
||||||
|
cargo publish -p decodal-lsp
|
||||||
|
```
|
||||||
|
|
||||||
|
`decodal-cli` and the Rust `decodal-wasm` wrapper remain repository-only packages.
|
||||||
|
|
||||||
|
## Publish JavaScript packages
|
||||||
|
|
||||||
|
The WASM build regenerates and normalizes both committed npm/JSR packages before publishing.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm --prefix site/decodal-site run build:wasm
|
||||||
|
npm publish ./packages/decodal-wasm
|
||||||
|
deno publish --config packages/decodal-wasm/jsr.json
|
||||||
|
npm publish ./packages/decodal-codemirror
|
||||||
|
deno publish --config packages/decodal-codemirror/jsr.json
|
||||||
|
```
|
||||||
|
|
||||||
|
After both registries accept the release, tag the release commit and update the deployed site.
|
||||||
@@ -20,4 +20,4 @@ default = []
|
|||||||
regex = ["decodal/regex"]
|
regex = ["decodal/regex"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
decodal = { version = "0.1.2", path = "../decodal-core" }
|
decodal = { version = "0.4.0", path = "../decodal-core" }
|
||||||
|
|||||||
@@ -5,10 +5,16 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use decodal::{
|
use decodal::{
|
||||||
Data, Diagnostic, DiagnosticKind, Engine, LoadedSource, SourceId, SourceLoader, Span,
|
Data, Diagnostic, DiagnosticKind, Engine, ImportLoader, LoadedImport, SourceId, Span,
|
||||||
format_diagnostic_with,
|
format_diagnostic_with,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct SourceInput {
|
||||||
|
key: String,
|
||||||
|
name: String,
|
||||||
|
source: String,
|
||||||
|
}
|
||||||
|
|
||||||
fn main() -> ExitCode {
|
fn main() -> ExitCode {
|
||||||
match run() {
|
match run() {
|
||||||
Ok(()) => ExitCode::SUCCESS,
|
Ok(()) => ExitCode::SUCCESS,
|
||||||
@@ -69,7 +75,7 @@ fn materialize_path(path: &str) -> Result<Data, String> {
|
|||||||
.map_err(|error| engine.format_diagnostic(&error))
|
.map_err(|error| engine.format_diagnostic(&error))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn read_root_source(path: &str) -> Result<LoadedSource, Diagnostic> {
|
fn read_root_source(path: &str) -> Result<SourceInput, Diagnostic> {
|
||||||
if path == "-" {
|
if path == "-" {
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
let mut source = String::new();
|
let mut source = String::new();
|
||||||
@@ -82,7 +88,7 @@ fn read_root_source(path: &str) -> Result<LoadedSource, Diagnostic> {
|
|||||||
format!("failed to read stdin: {error}"),
|
format!("failed to read stdin: {error}"),
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
Ok(LoadedSource {
|
Ok(SourceInput {
|
||||||
key: String::from("<stdin>"),
|
key: String::from("<stdin>"),
|
||||||
name: String::from("<stdin>"),
|
name: String::from("<stdin>"),
|
||||||
source,
|
source,
|
||||||
@@ -95,12 +101,12 @@ fn read_root_source(path: &str) -> Result<LoadedSource, Diagnostic> {
|
|||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
struct FsLoader;
|
struct FsLoader;
|
||||||
|
|
||||||
impl SourceLoader for FsLoader {
|
impl ImportLoader for FsLoader {
|
||||||
fn load(
|
fn load(
|
||||||
&mut self,
|
&mut self,
|
||||||
current_key: Option<&str>,
|
current_key: Option<&str>,
|
||||||
specifier: &str,
|
specifier: &str,
|
||||||
) -> Result<LoadedSource, Diagnostic> {
|
) -> Result<LoadedImport, Diagnostic> {
|
||||||
let path = Path::new(specifier);
|
let path = Path::new(specifier);
|
||||||
let path = if path.is_absolute() {
|
let path = if path.is_absolute() {
|
||||||
PathBuf::from(path)
|
PathBuf::from(path)
|
||||||
@@ -112,11 +118,11 @@ impl SourceLoader for FsLoader {
|
|||||||
} else {
|
} else {
|
||||||
PathBuf::from(path)
|
PathBuf::from(path)
|
||||||
};
|
};
|
||||||
load_path(&path)
|
load_path(&path).map(|source| LoadedImport::source(source.key, source.name, source.source))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_path(path: &Path) -> Result<LoadedSource, Diagnostic> {
|
fn load_path(path: &Path) -> Result<SourceInput, Diagnostic> {
|
||||||
let canonical = path.canonicalize().map_err(|error| {
|
let canonical = path.canonicalize().map_err(|error| {
|
||||||
Diagnostic::new(
|
Diagnostic::new(
|
||||||
DiagnosticKind::Import,
|
DiagnosticKind::Import,
|
||||||
@@ -132,7 +138,7 @@ fn load_path(path: &Path) -> Result<LoadedSource, Diagnostic> {
|
|||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
let key = canonical.to_string_lossy().into_owned();
|
let key = canonical.to_string_lossy().into_owned();
|
||||||
Ok(LoadedSource {
|
Ok(SourceInput {
|
||||||
name: key.clone(),
|
name: key.clone(),
|
||||||
key,
|
key,
|
||||||
source,
|
source,
|
||||||
|
|||||||
@@ -17,5 +17,5 @@ derive = ["dep:decodal-derive"]
|
|||||||
regex = ["std", "dep:regex"]
|
regex = ["std", "dep:regex"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
decodal-derive = { version = "0.1.1", path = "../decodal-derive", optional = true }
|
decodal-derive = { version = "0.2.0", path = "../decodal-derive", optional = true }
|
||||||
regex = { version = "1.10", default-features = false, features = ["std", "unicode-perl"], optional = true }
|
regex = { version = "1.10", default-features = false, features = ["std", "unicode-perl"], optional = true }
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
use decodal::{Data, EmptyLoader, Engine, HostValue};
|
use decodal::{Data, EmptyLoader, Engine, Value};
|
||||||
|
|
||||||
fn main() -> decodal::Result<()> {
|
fn main() -> decodal::Result<()> {
|
||||||
let mut engine = Engine::new(EmptyLoader);
|
let mut engine = Engine::new(EmptyLoader);
|
||||||
|
|
||||||
engine.bind_global(
|
engine.bind_global(
|
||||||
"Service",
|
"Service",
|
||||||
HostValue::object([
|
Value::object([
|
||||||
("name", HostValue::string_type()),
|
("name", Value::string_type()),
|
||||||
("port", HostValue::int_type().gt(443).default_int(8443)?),
|
("port", Value::int_type().gt(443).default_int(8443)?),
|
||||||
("enabled", HostValue::bool_type().default_bool(true)?),
|
("enabled", Value::bool_type().default_bool(true)?),
|
||||||
]),
|
]),
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,85 @@
|
|||||||
|
use decodal::{Data, Diagnostic, DiagnosticKind, Engine, ImportLoader, LoadedImport, Span, Value};
|
||||||
|
|
||||||
|
const POST: &str = r#"---
|
||||||
|
title: Hello
|
||||||
|
draft: false
|
||||||
|
---
|
||||||
|
# Hello
|
||||||
|
|
||||||
|
This body stays as Markdown.
|
||||||
|
"#;
|
||||||
|
|
||||||
|
struct ContentLoader;
|
||||||
|
|
||||||
|
impl ImportLoader for ContentLoader {
|
||||||
|
fn load(
|
||||||
|
&mut self,
|
||||||
|
_current_key: Option<&str>,
|
||||||
|
specifier: &str,
|
||||||
|
) -> decodal::Result<LoadedImport> {
|
||||||
|
if specifier != "./post.md" {
|
||||||
|
return Err(Diagnostic::new(
|
||||||
|
DiagnosticKind::Import,
|
||||||
|
Span::default(),
|
||||||
|
format!("unknown content import `{specifier}`"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(LoadedImport::value(
|
||||||
|
"content/post.md",
|
||||||
|
parse_markdown(POST)?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_markdown(source: &str) -> decodal::Result<Value> {
|
||||||
|
let source = source.strip_prefix("---\n").ok_or_else(frontmatter_error)?;
|
||||||
|
let (frontmatter, body) = source.split_once("\n---\n").ok_or_else(frontmatter_error)?;
|
||||||
|
let mut fields = Vec::new();
|
||||||
|
for line in frontmatter.lines() {
|
||||||
|
let (name, value) = line.split_once(':').ok_or_else(frontmatter_error)?;
|
||||||
|
let value = match value.trim() {
|
||||||
|
"true" => Value::bool(true),
|
||||||
|
"false" => Value::bool(false),
|
||||||
|
value => Value::string(value),
|
||||||
|
};
|
||||||
|
fields.push((name.trim(), value));
|
||||||
|
}
|
||||||
|
Ok(Value::object([
|
||||||
|
("frontmatter", Value::object(fields)),
|
||||||
|
("body", Value::string(body)),
|
||||||
|
]))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn frontmatter_error() -> Diagnostic {
|
||||||
|
Diagnostic::new(
|
||||||
|
DiagnosticKind::Import,
|
||||||
|
Span::default(),
|
||||||
|
"invalid Markdown frontmatter",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> decodal::Result<()> {
|
||||||
|
let mut engine = Engine::new(ContentLoader);
|
||||||
|
let module = engine.add_root_source(
|
||||||
|
"main.dcdl",
|
||||||
|
"main.dcdl",
|
||||||
|
r#"
|
||||||
|
let post = import "./post.md";
|
||||||
|
in {
|
||||||
|
title = post.frontmatter.title;
|
||||||
|
draft = post.frontmatter.draft;
|
||||||
|
body = post.body;
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
let value = engine.eval_module(module)?;
|
||||||
|
let data = engine.materialize(&value)?;
|
||||||
|
|
||||||
|
let Data::Object(fields) = data else {
|
||||||
|
panic!("expected imported Markdown to produce an object")
|
||||||
|
};
|
||||||
|
assert_eq!(fields[0].value, Data::String(String::from("Hello")));
|
||||||
|
assert_eq!(fields[1].value, Data::Bool(false));
|
||||||
|
assert!(matches!(fields[2].value, Data::String(_)));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -48,8 +48,17 @@ pub struct SpannedExpr {
|
|||||||
pub enum Expr {
|
pub enum Expr {
|
||||||
Literal(Literal),
|
Literal(Literal),
|
||||||
Ident(String),
|
Ident(String),
|
||||||
Object(Vec<Field>),
|
Object {
|
||||||
|
fields: Vec<Field>,
|
||||||
|
rest: Option<ObjectRest>,
|
||||||
|
},
|
||||||
Array(Vec<ExprId>),
|
Array(Vec<ExprId>),
|
||||||
|
ArrayConstraint {
|
||||||
|
item: ExprId,
|
||||||
|
},
|
||||||
|
MapConstraint {
|
||||||
|
value: ExprId,
|
||||||
|
},
|
||||||
Let {
|
Let {
|
||||||
bindings: Vec<Field>,
|
bindings: Vec<Field>,
|
||||||
body: ExprId,
|
body: ExprId,
|
||||||
@@ -67,6 +76,13 @@ pub enum Expr {
|
|||||||
params: Vec<Param>,
|
params: Vec<Param>,
|
||||||
body: ExprId,
|
body: ExprId,
|
||||||
},
|
},
|
||||||
|
/// An explicitly parenthesized expression.
|
||||||
|
///
|
||||||
|
/// Keeping this node preserves the delimiters and their trivia for source
|
||||||
|
/// tools while evaluation remains equivalent to evaluating `expr`.
|
||||||
|
Parenthesized {
|
||||||
|
expr: ExprId,
|
||||||
|
},
|
||||||
Match {
|
Match {
|
||||||
scrutinee: ExprId,
|
scrutinee: ExprId,
|
||||||
arms: Vec<MatchArm>,
|
arms: Vec<MatchArm>,
|
||||||
@@ -84,6 +100,10 @@ pub enum Expr {
|
|||||||
base: ExprId,
|
base: ExprId,
|
||||||
fallback: ExprId,
|
fallback: ExprId,
|
||||||
},
|
},
|
||||||
|
As {
|
||||||
|
narrower: ExprId,
|
||||||
|
wider: ExprId,
|
||||||
|
},
|
||||||
CompareConstraint {
|
CompareConstraint {
|
||||||
op: CompareOp,
|
op: CompareOp,
|
||||||
value: ExprId,
|
value: ExprId,
|
||||||
@@ -99,6 +119,12 @@ pub struct Field {
|
|||||||
pub span: Span,
|
pub span: Span,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct ObjectRest {
|
||||||
|
pub value: ExprId,
|
||||||
|
pub span: Span,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct Param {
|
pub struct Param {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|||||||
@@ -11,12 +11,18 @@ pub fn normalize_constraints(
|
|||||||
span: Span,
|
span: Span,
|
||||||
) -> crate::Result<Vec<ConstraintEntry>> {
|
) -> crate::Result<Vec<ConstraintEntry>> {
|
||||||
let mut primitive: Option<(PrimitiveType, Span)> = None;
|
let mut primitive: Option<(PrimitiveType, Span)> = None;
|
||||||
|
let mut array_span: Option<Span> = None;
|
||||||
|
let mut map_span: Option<Span> = None;
|
||||||
let mut lower: Option<(Bound, Span)> = None;
|
let mut lower: Option<(Bound, Span)> = None;
|
||||||
let mut upper: Option<(Bound, Span)> = None;
|
let mut upper: Option<(Bound, Span)> = None;
|
||||||
let mut rest = Vec::new();
|
let mut rest = Vec::new();
|
||||||
|
|
||||||
for entry in constraints {
|
for entry in constraints {
|
||||||
match entry.constraint {
|
match entry.constraint {
|
||||||
|
Constraint::Unknown => rest.push(ConstraintEntry {
|
||||||
|
constraint: Constraint::Unknown,
|
||||||
|
span: entry.span,
|
||||||
|
}),
|
||||||
Constraint::Type(next) => match primitive {
|
Constraint::Type(next) => match primitive {
|
||||||
Some((current, current_span)) if current != next => {
|
Some((current, current_span)) if current != next => {
|
||||||
return Err(Diagnostic::new(
|
return Err(Diagnostic::new(
|
||||||
@@ -61,6 +67,20 @@ pub fn normalize_constraints(
|
|||||||
constraint: Constraint::Regex(pattern),
|
constraint: Constraint::Regex(pattern),
|
||||||
span: entry.span,
|
span: entry.span,
|
||||||
}),
|
}),
|
||||||
|
Constraint::ArrayItems(item) => {
|
||||||
|
array_span.get_or_insert(entry.span);
|
||||||
|
rest.push(ConstraintEntry {
|
||||||
|
constraint: Constraint::ArrayItems(item),
|
||||||
|
span: entry.span,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
Constraint::MapValues(value) => {
|
||||||
|
map_span.get_or_insert(entry.span);
|
||||||
|
rest.push(ConstraintEntry {
|
||||||
|
constraint: Constraint::MapValues(value),
|
||||||
|
span: entry.span,
|
||||||
|
});
|
||||||
|
}
|
||||||
Constraint::BuiltinPredicate(name) => rest.push(ConstraintEntry {
|
Constraint::BuiltinPredicate(name) => rest.push(ConstraintEntry {
|
||||||
constraint: Constraint::BuiltinPredicate(name),
|
constraint: Constraint::BuiltinPredicate(name),
|
||||||
span: entry.span,
|
span: entry.span,
|
||||||
@@ -68,12 +88,75 @@ pub fn normalize_constraints(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let (Some((_, primitive_span)), Some(array_span)) = (primitive, array_span) {
|
||||||
|
return Err(Diagnostic::new(
|
||||||
|
DiagnosticKind::Conflict,
|
||||||
|
span,
|
||||||
|
"array element constraints conflict with primitive type constraints",
|
||||||
|
)
|
||||||
|
.with_label(primitive_span, "primitive type constraint")
|
||||||
|
.with_label(array_span, "array element constraint"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let (Some((_, primitive_span)), Some(map_span)) = (primitive, map_span) {
|
||||||
|
return Err(Diagnostic::new(
|
||||||
|
DiagnosticKind::Conflict,
|
||||||
|
span,
|
||||||
|
"map value constraints conflict with primitive type constraints",
|
||||||
|
)
|
||||||
|
.with_label(primitive_span, "primitive constraint")
|
||||||
|
.with_label(map_span, "map value constraint"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let (Some(array_span), Some(map_span)) = (array_span, map_span) {
|
||||||
|
return Err(Diagnostic::new(
|
||||||
|
DiagnosticKind::Conflict,
|
||||||
|
span,
|
||||||
|
"array and map constraints conflict",
|
||||||
|
)
|
||||||
|
.with_label(array_span, "array element constraint")
|
||||||
|
.with_label(map_span, "map value constraint"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(array_span) = array_span
|
||||||
|
&& (lower.is_some() || upper.is_some())
|
||||||
|
{
|
||||||
|
let mut diagnostic = Diagnostic::new(
|
||||||
|
DiagnosticKind::Conflict,
|
||||||
|
span,
|
||||||
|
"numeric comparison constraints conflict with array element constraints",
|
||||||
|
)
|
||||||
|
.with_label(array_span, "array element constraint");
|
||||||
|
if let Some((_, lower_span)) = lower {
|
||||||
|
diagnostic = diagnostic.with_label(lower_span, "numeric comparison constraint");
|
||||||
|
}
|
||||||
|
if let Some((_, upper_span)) = upper {
|
||||||
|
diagnostic = diagnostic.with_label(upper_span, "numeric comparison constraint");
|
||||||
|
}
|
||||||
|
return Err(diagnostic);
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(map_span) = map_span
|
||||||
|
&& (lower.is_some() || upper.is_some())
|
||||||
|
{
|
||||||
|
let mut diagnostic = Diagnostic::new(
|
||||||
|
DiagnosticKind::Conflict,
|
||||||
|
span,
|
||||||
|
"numeric comparison constraints conflict with map value constraints",
|
||||||
|
)
|
||||||
|
.with_label(map_span, "map value constraint");
|
||||||
|
if let Some((_, lower_span)) = lower {
|
||||||
|
diagnostic = diagnostic.with_label(lower_span, "numeric comparison constraint");
|
||||||
|
}
|
||||||
|
if let Some((_, upper_span)) = upper {
|
||||||
|
diagnostic = diagnostic.with_label(upper_span, "numeric comparison constraint");
|
||||||
|
}
|
||||||
|
return Err(diagnostic);
|
||||||
|
}
|
||||||
|
|
||||||
if matches!(
|
if matches!(
|
||||||
primitive,
|
primitive,
|
||||||
Some((
|
Some((PrimitiveType::String | PrimitiveType::Bool, _))
|
||||||
PrimitiveType::String | PrimitiveType::Bool | PrimitiveType::Array,
|
|
||||||
_
|
|
||||||
))
|
|
||||||
) && (lower.is_some() || upper.is_some())
|
) && (lower.is_some() || upper.is_some())
|
||||||
{
|
{
|
||||||
let mut diagnostic = Diagnostic::new(
|
let mut diagnostic = Diagnostic::new(
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ pub struct Diagnostic {
|
|||||||
pub span: Span,
|
pub span: Span,
|
||||||
pub message: String,
|
pub message: String,
|
||||||
pub labels: Vec<DiagnosticLabel>,
|
pub labels: Vec<DiagnosticLabel>,
|
||||||
|
pub notes: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
@@ -25,6 +26,7 @@ impl Diagnostic {
|
|||||||
span,
|
span,
|
||||||
message: message.into(),
|
message: message.into(),
|
||||||
labels: Vec::new(),
|
labels: Vec::new(),
|
||||||
|
notes: Vec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,6 +41,11 @@ impl Diagnostic {
|
|||||||
});
|
});
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn with_note(mut self, message: impl Into<String>) -> Self {
|
||||||
|
self.notes.push(message.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
|||||||
@@ -1,215 +0,0 @@
|
|||||||
use alloc::{boxed::Box, string::String, vec::Vec};
|
|
||||||
|
|
||||||
use crate::runtime::{Constraint, LiteralValue, PrimitiveType};
|
|
||||||
use crate::{CompareOp, Diagnostic, DiagnosticKind, Result, Span};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub enum HostValue {
|
|
||||||
String(String),
|
|
||||||
Int(i64),
|
|
||||||
Float(f64),
|
|
||||||
Bool(bool),
|
|
||||||
Array(Vec<HostValue>),
|
|
||||||
Object(Vec<HostField>),
|
|
||||||
Abstract {
|
|
||||||
constraints: Vec<Constraint>,
|
|
||||||
default: Option<Box<HostValue>>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
|
||||||
pub struct HostField {
|
|
||||||
pub name: String,
|
|
||||||
pub value: HostValue,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HostValue {
|
|
||||||
pub fn string(value: impl Into<String>) -> Self {
|
|
||||||
Self::String(value.into())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn int(value: i64) -> Self {
|
|
||||||
Self::Int(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn float(value: f64) -> Self {
|
|
||||||
Self::Float(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn bool(value: bool) -> Self {
|
|
||||||
Self::Bool(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn array<I>(items: I) -> Self
|
|
||||||
where
|
|
||||||
I: IntoIterator<Item = HostValue>,
|
|
||||||
{
|
|
||||||
Self::Array(items.into_iter().collect())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn object<I, N>(fields: I) -> Self
|
|
||||||
where
|
|
||||||
I: IntoIterator<Item = (N, HostValue)>,
|
|
||||||
N: Into<String>,
|
|
||||||
{
|
|
||||||
Self::Object(
|
|
||||||
fields
|
|
||||||
.into_iter()
|
|
||||||
.map(|(name, value)| HostField {
|
|
||||||
name: name.into(),
|
|
||||||
value,
|
|
||||||
})
|
|
||||||
.collect(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn object_from_paths<I, N>(fields: I) -> Self
|
|
||||||
where
|
|
||||||
I: IntoIterator<Item = (N, HostValue)>,
|
|
||||||
N: Into<String>,
|
|
||||||
{
|
|
||||||
let mut root = Vec::new();
|
|
||||||
for (path, value) in fields {
|
|
||||||
insert_path(&mut root, &path.into(), value);
|
|
||||||
}
|
|
||||||
Self::Object(root)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn string_type() -> Self {
|
|
||||||
Self::abstract_with_constraint(Constraint::Type(PrimitiveType::String))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn int_type() -> Self {
|
|
||||||
Self::abstract_with_constraint(Constraint::Type(PrimitiveType::Int))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn float_type() -> Self {
|
|
||||||
Self::abstract_with_constraint(Constraint::Type(PrimitiveType::Float))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn bool_type() -> Self {
|
|
||||||
Self::abstract_with_constraint(Constraint::Type(PrimitiveType::Bool))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn array_type() -> Self {
|
|
||||||
Self::abstract_with_constraint(Constraint::Type(PrimitiveType::Array))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn builtin_predicate(name: impl Into<String>) -> Self {
|
|
||||||
Self::abstract_with_constraint(Constraint::BuiltinPredicate(name.into()))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn abstract_with_constraint(constraint: Constraint) -> Self {
|
|
||||||
Self::Abstract {
|
|
||||||
constraints: alloc::vec![constraint],
|
|
||||||
default: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_constraint(mut self, constraint: Constraint) -> Self {
|
|
||||||
match &mut self {
|
|
||||||
Self::Abstract { constraints, .. } => constraints.push(constraint),
|
|
||||||
_ => {
|
|
||||||
self = Self::Abstract {
|
|
||||||
constraints: alloc::vec![constraint],
|
|
||||||
default: Some(Box::new(self)),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn gt(self, value: i64) -> Self {
|
|
||||||
self.with_constraint(Constraint::Compare(CompareOp::Gt, LiteralValue::Int(value)))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn gte(self, value: i64) -> Self {
|
|
||||||
self.with_constraint(Constraint::Compare(
|
|
||||||
CompareOp::Gte,
|
|
||||||
LiteralValue::Int(value),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn lt(self, value: i64) -> Self {
|
|
||||||
self.with_constraint(Constraint::Compare(CompareOp::Lt, LiteralValue::Int(value)))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn lte(self, value: i64) -> Self {
|
|
||||||
self.with_constraint(Constraint::Compare(
|
|
||||||
CompareOp::Lte,
|
|
||||||
LiteralValue::Int(value),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn default(self, value: HostValue) -> Result<Self> {
|
|
||||||
match self {
|
|
||||||
Self::Abstract {
|
|
||||||
constraints,
|
|
||||||
default: None,
|
|
||||||
} => Ok(Self::Abstract {
|
|
||||||
constraints,
|
|
||||||
default: Some(Box::new(value)),
|
|
||||||
}),
|
|
||||||
Self::Abstract { .. } => Err(Diagnostic::new(
|
|
||||||
DiagnosticKind::DefaultConflict,
|
|
||||||
Span::default(),
|
|
||||||
"host value already has a default",
|
|
||||||
)),
|
|
||||||
concrete => Ok(Self::Abstract {
|
|
||||||
constraints: Vec::new(),
|
|
||||||
default: Some(Box::new(concrete)),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn default_string(self, value: impl Into<String>) -> Result<Self> {
|
|
||||||
self.default(Self::string(value))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn default_int(self, value: i64) -> Result<Self> {
|
|
||||||
self.default(Self::int(value))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn default_float(self, value: f64) -> Result<Self> {
|
|
||||||
self.default(Self::float(value))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn default_bool(self, value: bool) -> Result<Self> {
|
|
||||||
self.default(Self::bool(value))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl HostField {
|
|
||||||
pub fn new(name: impl Into<String>, value: HostValue) -> Self {
|
|
||||||
Self {
|
|
||||||
name: name.into(),
|
|
||||||
value,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn insert_path(fields: &mut Vec<HostField>, path: &str, value: HostValue) {
|
|
||||||
let mut parts = path.splitn(2, '.');
|
|
||||||
let Some(head) = parts.next().filter(|part| !part.is_empty()) else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
if let Some(tail) = parts.next() {
|
|
||||||
if let Some(field) = fields.iter_mut().find(|field| field.name == head) {
|
|
||||||
if let HostValue::Object(children) = &mut field.value {
|
|
||||||
insert_path(children, tail, value);
|
|
||||||
} else {
|
|
||||||
let mut children = Vec::new();
|
|
||||||
insert_path(&mut children, tail, value);
|
|
||||||
field.value = HostValue::Object(children);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let mut children = Vec::new();
|
|
||||||
insert_path(&mut children, tail, value);
|
|
||||||
fields.push(HostField::new(head, HostValue::Object(children)));
|
|
||||||
}
|
|
||||||
} else if let Some(field) = fields.iter_mut().find(|field| field.name == head) {
|
|
||||||
field.value = value;
|
|
||||||
} else {
|
|
||||||
fields.push(HostField::new(head, value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
use crate::{Engine, ImportLoader, Result};
|
||||||
|
|
||||||
|
/// Host-owned configuration shared by runtime and language tooling.
|
||||||
|
///
|
||||||
|
/// Implementations create the import loader and install global bindings on a
|
||||||
|
/// fresh engine. Calling [`HostEnvironment::create_engine`] therefore gives
|
||||||
|
/// every consumer the same Decodal execution environment.
|
||||||
|
pub trait HostEnvironment {
|
||||||
|
type Loader: ImportLoader;
|
||||||
|
|
||||||
|
fn create_loader(&self) -> Self::Loader;
|
||||||
|
|
||||||
|
fn configure_engine(&self, _engine: &mut Engine<Self::Loader>) -> Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn create_engine(&self) -> Result<Engine<Self::Loader>> {
|
||||||
|
let mut engine = Engine::new(self.create_loader());
|
||||||
|
self.configure_engine(&mut engine)?;
|
||||||
|
Ok(engine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: HostEnvironment + ?Sized> HostEnvironment for &T {
|
||||||
|
type Loader = T::Loader;
|
||||||
|
|
||||||
|
fn create_loader(&self) -> Self::Loader {
|
||||||
|
T::create_loader(*self)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configure_engine(&self, engine: &mut Engine<Self::Loader>) -> Result<()> {
|
||||||
|
T::configure_engine(*self, engine)
|
||||||
|
}
|
||||||
|
}
|
||||||
+2083
-101
File diff suppressed because it is too large
Load Diff
@@ -22,6 +22,7 @@ pub enum TokenKind {
|
|||||||
Match,
|
Match,
|
||||||
Import,
|
Import,
|
||||||
Default,
|
Default,
|
||||||
|
As,
|
||||||
Underscore,
|
Underscore,
|
||||||
LBrace,
|
LBrace,
|
||||||
RBrace,
|
RBrace,
|
||||||
@@ -32,6 +33,7 @@ pub enum TokenKind {
|
|||||||
Semicolon,
|
Semicolon,
|
||||||
Comma,
|
Comma,
|
||||||
Dot,
|
Dot,
|
||||||
|
Ellipsis,
|
||||||
Colon,
|
Colon,
|
||||||
Equal,
|
Equal,
|
||||||
EqualEqual,
|
EqualEqual,
|
||||||
@@ -51,9 +53,25 @@ pub enum TokenKind {
|
|||||||
Gte,
|
Gte,
|
||||||
Lt,
|
Lt,
|
||||||
Lte,
|
Lte,
|
||||||
|
/// A line comment, including its leading `#` and excluding its newline.
|
||||||
|
Comment,
|
||||||
Eof,
|
Eof,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Tokenizes source while retaining comments and source spans for tooling.
|
||||||
|
///
|
||||||
|
/// Whitespace remains available through the gaps between adjacent token spans,
|
||||||
|
/// making this a lossless syntax view when paired with the original source.
|
||||||
|
pub fn tokenize_source(source: &str) -> Result<Vec<Token>> {
|
||||||
|
tokenize_source_with_source_id(SourceId(0), source)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tokenizes source with a caller-provided source identifier while retaining
|
||||||
|
/// comments and source spans for tooling.
|
||||||
|
pub fn tokenize_source_with_source_id(source_id: SourceId, source: &str) -> Result<Vec<Token>> {
|
||||||
|
Lexer::with_source_id(source_id, source).tokenize_with_comments()
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Lexer<'a> {
|
pub struct Lexer<'a> {
|
||||||
source_id: SourceId,
|
source_id: SourceId,
|
||||||
source: &'a str,
|
source: &'a str,
|
||||||
@@ -77,10 +95,26 @@ impl<'a> Lexer<'a> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn tokenize(mut self) -> Result<Vec<Token>> {
|
pub fn tokenize(mut self) -> Result<Vec<Token>> {
|
||||||
|
self.tokenize_impl(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tokenize_with_comments(mut self) -> Result<Vec<Token>> {
|
||||||
|
self.tokenize_impl(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tokenize_impl(&mut self, include_comments: bool) -> Result<Vec<Token>> {
|
||||||
let mut tokens = Vec::new();
|
let mut tokens = Vec::new();
|
||||||
let mut previous = None;
|
let mut previous = None;
|
||||||
loop {
|
loop {
|
||||||
let token = self.next_token(previous.as_ref())?;
|
self.skip_whitespace();
|
||||||
|
if self.peek() == Some(b'#') {
|
||||||
|
let comment = self.lex_comment();
|
||||||
|
if include_comments {
|
||||||
|
tokens.push(comment);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let token = self.next_non_ws_token(previous.as_ref())?;
|
||||||
let is_eof = token.kind == TokenKind::Eof;
|
let is_eof = token.kind == TokenKind::Eof;
|
||||||
if !is_eof {
|
if !is_eof {
|
||||||
previous = Some(token.kind.clone());
|
previous = Some(token.kind.clone());
|
||||||
@@ -92,11 +126,6 @@ impl<'a> Lexer<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn next_token(&mut self, previous: Option<&TokenKind>) -> Result<Token> {
|
|
||||||
self.skip_ws_and_comments();
|
|
||||||
self.next_non_ws_token(previous)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn next_non_ws_token(&mut self, previous: Option<&TokenKind>) -> Result<Token> {
|
fn next_non_ws_token(&mut self, previous: Option<&TokenKind>) -> Result<Token> {
|
||||||
let start = self.pos;
|
let start = self.pos;
|
||||||
let Some(ch) = self.peek() else {
|
let Some(ch) = self.peek() else {
|
||||||
@@ -141,8 +170,19 @@ impl<'a> Lexer<'a> {
|
|||||||
}
|
}
|
||||||
b'.' => {
|
b'.' => {
|
||||||
self.pos += 1;
|
self.pos += 1;
|
||||||
|
if self.consume(b'.') {
|
||||||
|
if self.consume(b'.') {
|
||||||
|
TokenKind::Ellipsis
|
||||||
|
} else {
|
||||||
|
return Err(Diagnostic::syntax(
|
||||||
|
self.span(start, self.pos),
|
||||||
|
"expected third '.' in ellipsis",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
TokenKind::Dot
|
TokenKind::Dot
|
||||||
}
|
}
|
||||||
|
}
|
||||||
b':' => {
|
b':' => {
|
||||||
self.pos += 1;
|
self.pos += 1;
|
||||||
TokenKind::Colon
|
TokenKind::Colon
|
||||||
@@ -247,21 +287,23 @@ impl<'a> Lexer<'a> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn skip_ws_and_comments(&mut self) {
|
fn skip_whitespace(&mut self) {
|
||||||
loop {
|
|
||||||
while matches!(self.peek(), Some(b' ' | b'\t' | b'\r' | b'\n')) {
|
while matches!(self.peek(), Some(b' ' | b'\t' | b'\r' | b'\n')) {
|
||||||
self.pos += 1;
|
self.pos += 1;
|
||||||
}
|
}
|
||||||
if self.peek() == Some(b'#') {
|
}
|
||||||
|
|
||||||
|
fn lex_comment(&mut self) -> Token {
|
||||||
|
let start = self.pos;
|
||||||
while let Some(c) = self.peek() {
|
while let Some(c) = self.peek() {
|
||||||
self.pos += 1;
|
|
||||||
if c == b'\n' {
|
if c == b'\n' {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
self.pos += 1;
|
||||||
}
|
}
|
||||||
continue;
|
Token {
|
||||||
}
|
kind: TokenKind::Comment,
|
||||||
break;
|
span: self.span(start, self.pos),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -365,6 +407,7 @@ impl<'a> Lexer<'a> {
|
|||||||
"match" => TokenKind::Match,
|
"match" => TokenKind::Match,
|
||||||
"import" => TokenKind::Import,
|
"import" => TokenKind::Import,
|
||||||
"default" => TokenKind::Default,
|
"default" => TokenKind::Default,
|
||||||
|
"as" => TokenKind::As,
|
||||||
_ => TokenKind::Ident(String::from(text)),
|
_ => TokenKind::Ident(String::from(text)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -432,4 +475,39 @@ mod tests {
|
|||||||
assert_eq!(tokens[3].kind, TokenKind::Amp);
|
assert_eq!(tokens[3].kind, TokenKind::Amp);
|
||||||
assert_eq!(tokens[4].kind, TokenKind::Gte);
|
assert_eq!(tokens[4].kind, TokenKind::Gte);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tokenizes_array_constraint_ellipsis() {
|
||||||
|
let tokens = Lexer::new("[...String]").tokenize().unwrap();
|
||||||
|
assert_eq!(tokens[0].kind, TokenKind::LBracket);
|
||||||
|
assert_eq!(tokens[1].kind, TokenKind::Ellipsis);
|
||||||
|
assert!(matches!(tokens[2].kind, TokenKind::Ident(_)));
|
||||||
|
assert_eq!(tokens[3].kind, TokenKind::RBracket);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tooling_tokens_retain_comments_and_whitespace_gaps() {
|
||||||
|
let source = "value = 1; # trailing\n\n# leading\nnext = 2;";
|
||||||
|
let tokens = tokenize_source(source).unwrap();
|
||||||
|
let comments = tokens
|
||||||
|
.iter()
|
||||||
|
.filter(|token| token.kind == TokenKind::Comment)
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
assert_eq!(comments.len(), 2);
|
||||||
|
assert_eq!(
|
||||||
|
&source[comments[0].span.start as usize..comments[0].span.end as usize],
|
||||||
|
"# trailing"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
source[comments[0].span.end as usize..comments[1].span.start as usize].contains("\n\n")
|
||||||
|
);
|
||||||
|
|
||||||
|
let source_id = SourceId(7);
|
||||||
|
let identified = tokenize_source_with_source_id(source_id, "# comment").unwrap();
|
||||||
|
assert!(
|
||||||
|
identified
|
||||||
|
.iter()
|
||||||
|
.all(|token| token.span.source == source_id)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ extern crate alloc;
|
|||||||
pub mod ast;
|
pub mod ast;
|
||||||
pub mod constraints;
|
pub mod constraints;
|
||||||
pub mod diagnostic;
|
pub mod diagnostic;
|
||||||
pub mod embedding;
|
pub mod environment;
|
||||||
pub mod eval;
|
pub mod eval;
|
||||||
mod lexer;
|
mod lexer;
|
||||||
pub mod module;
|
pub mod module;
|
||||||
@@ -13,22 +13,28 @@ pub mod parser;
|
|||||||
pub mod runtime;
|
pub mod runtime;
|
||||||
pub mod span;
|
pub mod span;
|
||||||
pub mod typed;
|
pub mod typed;
|
||||||
|
pub mod value;
|
||||||
|
|
||||||
pub use ast::{Ast, BinaryOp, CompareOp, Expr, ExprId, Field, Literal, Param};
|
pub use ast::{Ast, BinaryOp, CompareOp, Expr, ExprId, Field, Literal, ObjectRest, Param};
|
||||||
pub use constraints::normalize_constraints;
|
pub use constraints::normalize_constraints;
|
||||||
#[cfg(feature = "derive")]
|
#[cfg(feature = "derive")]
|
||||||
pub use decodal_derive::Decodal;
|
pub use decodal_derive::Decodal;
|
||||||
pub use diagnostic::{Diagnostic, DiagnosticKind, Result};
|
pub use diagnostic::{Diagnostic, DiagnosticKind, Result};
|
||||||
pub use embedding::{HostField, HostValue};
|
pub use environment::HostEnvironment;
|
||||||
pub use eval::{Engine, format_diagnostic_with};
|
pub use eval::{Engine, format_diagnostic_with};
|
||||||
pub use module::{EmptyLoader, LoadedSource, Module, SourceLoader};
|
pub use lexer::{
|
||||||
|
Token as SyntaxToken, TokenKind as SyntaxTokenKind, tokenize_source,
|
||||||
|
tokenize_source_with_source_id,
|
||||||
|
};
|
||||||
|
pub use module::{EmptyLoader, ImportCandidate, ImportLoader, LoadedImport, Module};
|
||||||
pub use parser::{ParseOutput, Parser, SourceForm, parse_source, parse_source_with_source_id};
|
pub use parser::{ParseOutput, Parser, SourceForm, parse_source, parse_source_with_source_id};
|
||||||
pub use runtime::{Constraint, Data, ExprRef, LiteralValue, ModuleId, PrimitiveType, RuntimeValue};
|
pub use runtime::{Constraint, Data, ExprRef, LiteralValue, ModuleId, PrimitiveType};
|
||||||
pub use span::{SourceId, Span};
|
pub use span::{SourceId, Span};
|
||||||
pub use typed::{
|
pub use typed::{
|
||||||
DecodalDecode, DecodalSchema, DecodeError, DecodeResult, IntoHostValue, data_at_path,
|
DecodalDecode, DecodalRest, DecodalSchema, DecodeError, DecodeResult, IntoValue, data_at_path,
|
||||||
decode_path, prefix_decode_error,
|
decode_path, prefix_decode_error,
|
||||||
};
|
};
|
||||||
|
pub use value::{Value, ValueField};
|
||||||
|
|
||||||
pub fn version() -> &'static str {
|
pub fn version() -> &'static str {
|
||||||
env!("CARGO_PKG_VERSION")
|
env!("CARGO_PKG_VERSION")
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use alloc::string::String;
|
use alloc::{string::String, vec::Vec};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Ast, ExprId, SourceForm, SourceId,
|
Ast, ExprId, SourceForm, SourceId, Value,
|
||||||
runtime::{EnvId, ThunkId},
|
runtime::{EnvId, ThunkId},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -17,22 +17,84 @@ pub struct Module {
|
|||||||
pub root_thunk: ThunkId,
|
pub root_thunk: ThunkId,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Content resolved by an [`ImportLoader`].
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct LoadedSource {
|
pub enum LoadedImport {
|
||||||
pub key: String,
|
Source {
|
||||||
pub name: String,
|
key: String,
|
||||||
pub source: String,
|
name: String,
|
||||||
|
source: String,
|
||||||
|
},
|
||||||
|
Value {
|
||||||
|
key: String,
|
||||||
|
value: Value,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait SourceLoader {
|
/// An import specifier offered by host-owned language tooling.
|
||||||
fn load(&mut self, current_key: Option<&str>, specifier: &str) -> crate::Result<LoadedSource>;
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct ImportCandidate {
|
||||||
|
pub specifier: String,
|
||||||
|
pub detail: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImportCandidate {
|
||||||
|
pub fn new(specifier: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
specifier: specifier.into(),
|
||||||
|
detail: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_detail(mut self, detail: impl Into<String>) -> Self {
|
||||||
|
self.detail = Some(detail.into());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl LoadedImport {
|
||||||
|
pub fn source(
|
||||||
|
key: impl Into<String>,
|
||||||
|
name: impl Into<String>,
|
||||||
|
source: impl Into<String>,
|
||||||
|
) -> Self {
|
||||||
|
Self::Source {
|
||||||
|
key: key.into(),
|
||||||
|
name: name.into(),
|
||||||
|
source: source.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn value(key: impl Into<String>, value: Value) -> Self {
|
||||||
|
Self::Value {
|
||||||
|
key: key.into(),
|
||||||
|
value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ImportLoader {
|
||||||
|
fn load(&mut self, current_key: Option<&str>, specifier: &str) -> crate::Result<LoadedImport>;
|
||||||
|
|
||||||
|
/// Returns host-resolvable import specifiers matching an unfinished prefix.
|
||||||
|
///
|
||||||
|
/// Runtime-only loaders may keep the default implementation. Hosts that
|
||||||
|
/// provide an editor should implement this from the same namespace used by
|
||||||
|
/// [`ImportLoader::load`].
|
||||||
|
fn complete_import(
|
||||||
|
&mut self,
|
||||||
|
_current_key: Option<&str>,
|
||||||
|
_prefix: &str,
|
||||||
|
) -> crate::Result<Vec<ImportCandidate>> {
|
||||||
|
Ok(Vec::new())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Default)]
|
#[derive(Debug, Clone, Copy, Default)]
|
||||||
pub struct EmptyLoader;
|
pub struct EmptyLoader;
|
||||||
|
|
||||||
impl SourceLoader for EmptyLoader {
|
impl ImportLoader for EmptyLoader {
|
||||||
fn load(&mut self, _current_key: Option<&str>, specifier: &str) -> crate::Result<LoadedSource> {
|
fn load(&mut self, _current_key: Option<&str>, specifier: &str) -> crate::Result<LoadedImport> {
|
||||||
Err(crate::Diagnostic::new(
|
Err(crate::Diagnostic::new(
|
||||||
crate::DiagnosticKind::Import,
|
crate::DiagnosticKind::Import,
|
||||||
crate::Span::default(),
|
crate::Span::default(),
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ use alloc::{string::String, vec::Vec};
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
SourceId, Span,
|
SourceId, Span,
|
||||||
ast::{Ast, BinaryOp, CompareOp, Expr, ExprId, Field, Literal, MatchArm, Param, UnaryOp},
|
ast::{
|
||||||
|
Ast, BinaryOp, CompareOp, Expr, ExprId, Field, Literal, MatchArm, ObjectRest, Param,
|
||||||
|
UnaryOp,
|
||||||
|
},
|
||||||
diagnostic::{Diagnostic, Result},
|
diagnostic::{Diagnostic, Result},
|
||||||
lexer::{Lexer, Token, TokenKind},
|
lexer::{Lexer, Token, TokenKind},
|
||||||
};
|
};
|
||||||
@@ -38,7 +41,10 @@ pub struct Parser {
|
|||||||
impl Parser {
|
impl Parser {
|
||||||
pub fn new(tokens: Vec<Token>) -> Self {
|
pub fn new(tokens: Vec<Token>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
tokens,
|
tokens: tokens
|
||||||
|
.into_iter()
|
||||||
|
.filter(|token| token.kind != TokenKind::Comment)
|
||||||
|
.collect(),
|
||||||
pos: 0,
|
pos: 0,
|
||||||
ast: Ast::new(),
|
ast: Ast::new(),
|
||||||
}
|
}
|
||||||
@@ -56,7 +62,7 @@ impl Parser {
|
|||||||
})
|
})
|
||||||
.unwrap_or_else(|| self.peek().span);
|
.unwrap_or_else(|| self.peek().span);
|
||||||
(
|
(
|
||||||
self.ast.push(Expr::Object(fields), span),
|
self.ast.push(Expr::Object { fields, rest: None }, span),
|
||||||
SourceForm::Fields,
|
SourceForm::Fields,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
@@ -242,6 +248,13 @@ impl Parser {
|
|||||||
},
|
},
|
||||||
span,
|
span,
|
||||||
),
|
),
|
||||||
|
InfixKind::As => self.ast.push(
|
||||||
|
Expr::As {
|
||||||
|
narrower: lhs,
|
||||||
|
wider: rhs,
|
||||||
|
},
|
||||||
|
span,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -316,28 +329,64 @@ impl Parser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn parse_object_after_lbrace(&mut self, start_span: Span) -> Result<ExprId> {
|
fn parse_object_after_lbrace(&mut self, start_span: Span) -> Result<ExprId> {
|
||||||
|
if self.consume_kind(&TokenKind::Ellipsis).is_some() {
|
||||||
|
let value = self.parse_expr(0)?;
|
||||||
|
self.expect_kind(
|
||||||
|
&TokenKind::RBrace,
|
||||||
|
"expected '}' after map value constraint",
|
||||||
|
)?;
|
||||||
|
let span = start_span.join(self.previous_span());
|
||||||
|
return Ok(self.ast.push(Expr::MapConstraint { value }, span));
|
||||||
|
}
|
||||||
|
|
||||||
let mut fields = Vec::new();
|
let mut fields = Vec::new();
|
||||||
if self.consume_kind(&TokenKind::RBrace).is_some() {
|
if self.consume_kind(&TokenKind::RBrace).is_some() {
|
||||||
return Ok(self
|
return Ok(self.ast.push(
|
||||||
.ast
|
Expr::Object { fields, rest: None },
|
||||||
.push(Expr::Object(fields), start_span.join(self.previous_span())));
|
start_span.join(self.previous_span()),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
let mut rest = None;
|
||||||
loop {
|
loop {
|
||||||
fields.push(self.parse_field()?);
|
fields.push(self.parse_field()?);
|
||||||
if self.consume_kind(&TokenKind::Semicolon).is_some() {
|
if self.consume_kind(&TokenKind::Semicolon).is_some() {
|
||||||
if self.consume_kind(&TokenKind::RBrace).is_some() {
|
if self.consume_kind(&TokenKind::RBrace).is_some() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
if let Some(ellipsis) = self.consume_kind(&TokenKind::Ellipsis) {
|
||||||
|
let value = self.parse_expr(0)?;
|
||||||
|
rest = Some(ObjectRest {
|
||||||
|
value,
|
||||||
|
span: ellipsis.join(self.ast.span(value)),
|
||||||
|
});
|
||||||
|
let _ = self.consume_kind(&TokenKind::Semicolon);
|
||||||
|
self.expect_kind(
|
||||||
|
&TokenKind::RBrace,
|
||||||
|
"expected '}' after object rest constraint",
|
||||||
|
)?;
|
||||||
|
break;
|
||||||
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
self.expect_kind(&TokenKind::RBrace, "expected ';' or '}' after object field")?;
|
self.expect_kind(&TokenKind::RBrace, "expected ';' or '}' after object field")?;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
let span = start_span.join(self.previous_span());
|
let span = start_span.join(self.previous_span());
|
||||||
Ok(self.ast.push(Expr::Object(fields), span))
|
Ok(self.ast.push(Expr::Object { fields, rest }, span))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_array_after_lbracket(&mut self, start_span: Span) -> Result<ExprId> {
|
fn parse_array_after_lbracket(&mut self, start_span: Span) -> Result<ExprId> {
|
||||||
|
if self.consume_kind(&TokenKind::Ellipsis).is_some() {
|
||||||
|
let item = self.parse_expr(0)?;
|
||||||
|
let _ = self.consume_kind(&TokenKind::Comma);
|
||||||
|
self.expect_kind(
|
||||||
|
&TokenKind::RBracket,
|
||||||
|
"expected ']' after array element constraint",
|
||||||
|
)?;
|
||||||
|
let span = start_span.join(self.previous_span());
|
||||||
|
return Ok(self.ast.push(Expr::ArrayConstraint { item }, span));
|
||||||
|
}
|
||||||
|
|
||||||
let mut items = Vec::new();
|
let mut items = Vec::new();
|
||||||
if self.consume_kind(&TokenKind::RBracket).is_some() {
|
if self.consume_kind(&TokenKind::RBracket).is_some() {
|
||||||
return Ok(self
|
return Ok(self
|
||||||
@@ -369,8 +418,9 @@ impl Parser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let expr = self.parse_expr(0)?;
|
let expr = self.parse_expr(0)?;
|
||||||
self.expect_kind(&TokenKind::RParen, "expected ')' after expression")?;
|
let end_span = self.expect_kind(&TokenKind::RParen, "expected ')' after expression")?;
|
||||||
Ok(expr)
|
let span = start_span.join(end_span);
|
||||||
|
Ok(self.ast.push(Expr::Parenthesized { expr }, span))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_params_after_lparen(&mut self) -> Result<Vec<Param>> {
|
fn parse_params_after_lparen(&mut self) -> Result<Vec<Param>> {
|
||||||
@@ -529,6 +579,7 @@ impl Parser {
|
|||||||
|
|
||||||
fn peek_infix(&self) -> Option<(InfixKind, u8, u8)> {
|
fn peek_infix(&self) -> Option<(InfixKind, u8, u8)> {
|
||||||
match self.peek_kind() {
|
match self.peek_kind() {
|
||||||
|
TokenKind::As => Some((InfixKind::As, 0, 1)),
|
||||||
TokenKind::Default => Some((InfixKind::Default, 1, 2)),
|
TokenKind::Default => Some((InfixKind::Default, 1, 2)),
|
||||||
TokenKind::SlashSlash => Some((InfixKind::Patch, 3, 4)),
|
TokenKind::SlashSlash => Some((InfixKind::Patch, 3, 4)),
|
||||||
TokenKind::Amp => Some((InfixKind::And, 5, 6)),
|
TokenKind::Amp => Some((InfixKind::And, 5, 6)),
|
||||||
@@ -637,6 +688,7 @@ impl Parser {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
enum InfixKind {
|
enum InfixKind {
|
||||||
|
As,
|
||||||
Add,
|
Add,
|
||||||
Sub,
|
Sub,
|
||||||
Mul,
|
Mul,
|
||||||
@@ -663,15 +715,79 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn parses_top_level_fields_as_object() {
|
fn parses_top_level_fields_as_object() {
|
||||||
let parsed = parse_source("port = Int & >= 1 default 8080;").unwrap();
|
let parsed = parse_source("port = Int & >= 1 default 8080;").unwrap();
|
||||||
assert!(matches!(parsed.ast.get(parsed.root).expr, Expr::Object(_)));
|
assert!(matches!(
|
||||||
|
parsed.ast.get(parsed.root).expr,
|
||||||
|
Expr::Object { .. }
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parses_object_dot_field() {
|
fn parses_object_dot_field() {
|
||||||
let parsed = parse_source("{ feature.enable = false; }").unwrap();
|
let parsed = parse_source("{ feature.enable = false; }").unwrap();
|
||||||
let Expr::Object(fields) = &parsed.ast.get(parsed.root).expr else {
|
let Expr::Object { fields, .. } = &parsed.ast.get(parsed.root).expr else {
|
||||||
panic!()
|
panic!()
|
||||||
};
|
};
|
||||||
assert_eq!(fields[0].path, ["feature", "enable"]);
|
assert_eq!(fields[0].path, ["feature", "enable"]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_array_constraint() {
|
||||||
|
let parsed = parse_source("[...(String & /^api-/)]").unwrap();
|
||||||
|
let Expr::ArrayConstraint { item } = parsed.ast.get(parsed.root).expr else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
let Expr::Parenthesized { expr } = parsed.ast.get(item).expr else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
assert!(matches!(parsed.ast.get(expr).expr, Expr::Binary { .. }));
|
||||||
|
assert_eq!(parsed.ast.span(item), Span::new(SourceId(0), 4, 22));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parser_accepts_the_public_lossless_token_stream() {
|
||||||
|
let tokens = crate::tokenize_source("value = (# note\n 1);").unwrap();
|
||||||
|
let parsed = Parser::new(tokens).parse().unwrap();
|
||||||
|
let Expr::Object { fields, .. } = &parsed.ast.get(parsed.root).expr else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
assert!(matches!(
|
||||||
|
parsed.ast.get(fields[0].value).expr,
|
||||||
|
Expr::Parenthesized { .. }
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_array_constraint_without_element_constraint() {
|
||||||
|
assert!(parse_source("[...]").is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_map_constraint() {
|
||||||
|
let parsed = parse_source("{...String}").unwrap();
|
||||||
|
let Expr::MapConstraint { value } = parsed.ast.get(parsed.root).expr else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
assert!(matches!(parsed.ast.get(value).expr, Expr::Ident(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_object_rest_constraint_after_named_fields() {
|
||||||
|
let parsed = parse_source("{ hoge = Int; fuga = String; ...Unknown }").unwrap();
|
||||||
|
let Expr::Object { fields, rest } = &parsed.ast.get(parsed.root).expr else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
assert_eq!(fields.len(), 2);
|
||||||
|
let rest = rest.as_ref().expect("object has a rest constraint");
|
||||||
|
assert!(matches!(parsed.ast.get(rest.value).expr, Expr::Ident(_)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parses_as_below_composition() {
|
||||||
|
let parsed = parse_source("value & override as Schema").unwrap();
|
||||||
|
let Expr::As { narrower, wider } = parsed.ast.get(parsed.root).expr else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
assert!(matches!(parsed.ast.get(narrower).expr, Expr::Binary { .. }));
|
||||||
|
assert!(matches!(parsed.ast.get(wider).expr, Expr::Ident(_)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ pub struct ExprRef {
|
|||||||
pub expr: ExprId,
|
pub expr: ExprId,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Evaluator-owned representation of a lazy concrete value or unresolved range.
|
||||||
|
///
|
||||||
|
/// Embedders normally construct [`crate::Value`] and consume [`Data`] instead.
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum RuntimeValue {
|
pub enum RuntimeValue {
|
||||||
Concrete(ConcreteValue),
|
Concrete(ConcreteValue),
|
||||||
@@ -31,6 +34,13 @@ pub enum ConcreteValue {
|
|||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct ObjectValue {
|
pub struct ObjectValue {
|
||||||
pub fields: Vec<ObjectField>,
|
pub fields: Vec<ObjectField>,
|
||||||
|
pub rest: Option<ObjectRest>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct ObjectRest {
|
||||||
|
pub value: ThunkId,
|
||||||
|
pub span: Span,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
@@ -67,7 +77,10 @@ pub struct ConstraintEntry {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum Constraint {
|
pub enum Constraint {
|
||||||
|
Unknown,
|
||||||
Type(PrimitiveType),
|
Type(PrimitiveType),
|
||||||
|
ArrayItems(ThunkId),
|
||||||
|
MapValues(ThunkId),
|
||||||
Compare(CompareOp, LiteralValue),
|
Compare(CompareOp, LiteralValue),
|
||||||
Regex(String),
|
Regex(String),
|
||||||
BuiltinPredicate(String),
|
BuiltinPredicate(String),
|
||||||
@@ -79,7 +92,6 @@ pub enum PrimitiveType {
|
|||||||
Int,
|
Int,
|
||||||
Float,
|
Float,
|
||||||
Bool,
|
Bool,
|
||||||
Array,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
@@ -90,6 +102,7 @@ pub enum LiteralValue {
|
|||||||
Bool(bool),
|
Bool(bool),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Fully materialized Decodal data.
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum Data {
|
pub enum Data {
|
||||||
String(String),
|
String(String),
|
||||||
|
|||||||
@@ -1,17 +1,24 @@
|
|||||||
use alloc::{format, string::String, vec::Vec};
|
use alloc::{collections::BTreeMap, format, string::String, vec::Vec};
|
||||||
|
|
||||||
use crate::{Data, HostValue};
|
use crate::{Data, Value, runtime::DataField};
|
||||||
|
|
||||||
pub trait DecodalSchema {
|
pub trait DecodalSchema {
|
||||||
fn decodal_schema() -> HostValue;
|
fn decodal_schema() -> Value;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait DecodalDecode: Sized {
|
pub trait DecodalDecode: Sized {
|
||||||
fn decodal_decode(data: &Data) -> DecodeResult<Self>;
|
fn decodal_decode(data: &Data) -> DecodeResult<Self>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait IntoHostValue {
|
pub trait IntoValue {
|
||||||
fn into_host_value(self) -> HostValue;
|
fn into_value(self) -> Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A map-like receiver for fields not named by a derived struct schema.
|
||||||
|
pub trait DecodalRest: Sized {
|
||||||
|
fn decodal_rest_schema() -> Value;
|
||||||
|
|
||||||
|
fn decodal_decode_rest(data: &Data, known_fields: &[&str]) -> DecodeResult<Self>;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type DecodeResult<T> = core::result::Result<T, DecodeError>;
|
pub type DecodeResult<T> = core::result::Result<T, DecodeError>;
|
||||||
@@ -80,9 +87,38 @@ fn prefix_error(path: &str, mut error: DecodeError) -> DecodeError {
|
|||||||
error
|
error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl DecodalSchema for Data {
|
||||||
|
fn decodal_schema() -> Value {
|
||||||
|
Value::unknown()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DecodalDecode for Data {
|
||||||
|
fn decodal_decode(data: &Data) -> DecodeResult<Self> {
|
||||||
|
Ok(data.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoValue for Data {
|
||||||
|
fn into_value(self) -> Value {
|
||||||
|
match self {
|
||||||
|
Data::String(value) => Value::string(value),
|
||||||
|
Data::Int(value) => Value::int(value),
|
||||||
|
Data::Float(value) => Value::float(value),
|
||||||
|
Data::Bool(value) => Value::bool(value),
|
||||||
|
Data::Array(items) => Value::array(items.into_iter().map(IntoValue::into_value)),
|
||||||
|
Data::Object(fields) => Value::object(
|
||||||
|
fields
|
||||||
|
.into_iter()
|
||||||
|
.map(|field| (field.name, field.value.into_value())),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl DecodalSchema for String {
|
impl DecodalSchema for String {
|
||||||
fn decodal_schema() -> HostValue {
|
fn decodal_schema() -> Value {
|
||||||
HostValue::string_type()
|
Value::string_type()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,15 +131,15 @@ impl DecodalDecode for String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoHostValue for String {
|
impl IntoValue for String {
|
||||||
fn into_host_value(self) -> HostValue {
|
fn into_value(self) -> Value {
|
||||||
HostValue::string(self)
|
Value::string(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoHostValue for &str {
|
impl IntoValue for &str {
|
||||||
fn into_host_value(self) -> HostValue {
|
fn into_value(self) -> Value {
|
||||||
HostValue::string(self)
|
Value::string(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,8 +147,8 @@ macro_rules! impl_int_decode {
|
|||||||
($($ty:ty),* $(,)?) => {
|
($($ty:ty),* $(,)?) => {
|
||||||
$(
|
$(
|
||||||
impl DecodalSchema for $ty {
|
impl DecodalSchema for $ty {
|
||||||
fn decodal_schema() -> HostValue {
|
fn decodal_schema() -> Value {
|
||||||
HostValue::int_type()
|
Value::int_type()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,9 +162,9 @@ macro_rules! impl_int_decode {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoHostValue for $ty {
|
impl IntoValue for $ty {
|
||||||
fn into_host_value(self) -> HostValue {
|
fn into_value(self) -> Value {
|
||||||
HostValue::int(self as i64)
|
Value::int(self as i64)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)*
|
)*
|
||||||
@@ -138,8 +174,8 @@ macro_rules! impl_int_decode {
|
|||||||
impl_int_decode!(i8, i16, i32, i64, u8, u16, u32);
|
impl_int_decode!(i8, i16, i32, i64, u8, u16, u32);
|
||||||
|
|
||||||
impl DecodalSchema for f64 {
|
impl DecodalSchema for f64 {
|
||||||
fn decodal_schema() -> HostValue {
|
fn decodal_schema() -> Value {
|
||||||
HostValue::float_type()
|
Value::float_type()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,15 +189,15 @@ impl DecodalDecode for f64 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoHostValue for f64 {
|
impl IntoValue for f64 {
|
||||||
fn into_host_value(self) -> HostValue {
|
fn into_value(self) -> Value {
|
||||||
HostValue::float(self)
|
Value::float(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DecodalSchema for f32 {
|
impl DecodalSchema for f32 {
|
||||||
fn decodal_schema() -> HostValue {
|
fn decodal_schema() -> Value {
|
||||||
HostValue::float_type()
|
Value::float_type()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,15 +207,15 @@ impl DecodalDecode for f32 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoHostValue for f32 {
|
impl IntoValue for f32 {
|
||||||
fn into_host_value(self) -> HostValue {
|
fn into_value(self) -> Value {
|
||||||
HostValue::float(f64::from(self))
|
Value::float(f64::from(self))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl DecodalSchema for bool {
|
impl DecodalSchema for bool {
|
||||||
fn decodal_schema() -> HostValue {
|
fn decodal_schema() -> Value {
|
||||||
HostValue::bool_type()
|
Value::bool_type()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,15 +228,15 @@ impl DecodalDecode for bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl IntoHostValue for bool {
|
impl IntoValue for bool {
|
||||||
fn into_host_value(self) -> HostValue {
|
fn into_value(self) -> Value {
|
||||||
HostValue::bool(self)
|
Value::bool(self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> DecodalSchema for Vec<T> {
|
impl<T: DecodalSchema> DecodalSchema for Vec<T> {
|
||||||
fn decodal_schema() -> HostValue {
|
fn decodal_schema() -> Value {
|
||||||
HostValue::array_type()
|
Value::array_of(T::decodal_schema())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,8 +256,190 @@ impl<T: DecodalDecode> DecodalDecode for Vec<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: IntoHostValue> IntoHostValue for Vec<T> {
|
impl<T: IntoValue> IntoValue for Vec<T> {
|
||||||
fn into_host_value(self) -> HostValue {
|
fn into_value(self) -> Value {
|
||||||
HostValue::array(self.into_iter().map(IntoHostValue::into_host_value))
|
Value::array(self.into_iter().map(IntoValue::into_value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: DecodalSchema> DecodalSchema for BTreeMap<String, T> {
|
||||||
|
fn decodal_schema() -> Value {
|
||||||
|
Value::map_of(T::decodal_schema())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: DecodalDecode> DecodalDecode for BTreeMap<String, T> {
|
||||||
|
fn decodal_decode(data: &Data) -> DecodeResult<Self> {
|
||||||
|
match data {
|
||||||
|
Data::Object(fields) => fields
|
||||||
|
.iter()
|
||||||
|
.map(|field| {
|
||||||
|
T::decodal_decode(&field.value)
|
||||||
|
.map(|value| (field.name.clone(), value))
|
||||||
|
.map_err(|error| prefix_error(&field.name, error))
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
_ => Err(DecodeError::at_type("", "Object")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: IntoValue> IntoValue for BTreeMap<String, T> {
|
||||||
|
fn into_value(self) -> Value {
|
||||||
|
Value::object(
|
||||||
|
self.into_iter()
|
||||||
|
.map(|(name, value)| (name, value.into_value())),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: DecodalSchema + DecodalDecode> DecodalRest for BTreeMap<String, T> {
|
||||||
|
fn decodal_rest_schema() -> Value {
|
||||||
|
T::decodal_schema()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decodal_decode_rest(data: &Data, known_fields: &[&str]) -> DecodeResult<Self> {
|
||||||
|
let Data::Object(fields) = data else {
|
||||||
|
return Err(DecodeError::at_type("", "Object"));
|
||||||
|
};
|
||||||
|
fields
|
||||||
|
.iter()
|
||||||
|
.filter(|field| !known_fields.contains(&field.name.as_str()))
|
||||||
|
.map(decode_rest_field::<T>)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
impl<T: DecodalSchema> DecodalSchema for std::collections::HashMap<String, T> {
|
||||||
|
fn decodal_schema() -> Value {
|
||||||
|
Value::map_of(T::decodal_schema())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
impl<T: DecodalDecode> DecodalDecode for std::collections::HashMap<String, T> {
|
||||||
|
fn decodal_decode(data: &Data) -> DecodeResult<Self> {
|
||||||
|
match data {
|
||||||
|
Data::Object(fields) => fields
|
||||||
|
.iter()
|
||||||
|
.map(|field| {
|
||||||
|
T::decodal_decode(&field.value)
|
||||||
|
.map(|value| (field.name.clone(), value))
|
||||||
|
.map_err(|error| prefix_error(&field.name, error))
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
_ => Err(DecodeError::at_type("", "Object")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
impl<T: IntoValue> IntoValue for std::collections::HashMap<String, T> {
|
||||||
|
fn into_value(self) -> Value {
|
||||||
|
let mut fields: Vec<_> = self
|
||||||
|
.into_iter()
|
||||||
|
.map(|(name, value)| (name, value.into_value()))
|
||||||
|
.collect();
|
||||||
|
fields.sort_by(|left, right| left.0.cmp(&right.0));
|
||||||
|
Value::object(fields)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
impl<T: DecodalSchema + DecodalDecode> DecodalRest for std::collections::HashMap<String, T> {
|
||||||
|
fn decodal_rest_schema() -> Value {
|
||||||
|
T::decodal_schema()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decodal_decode_rest(data: &Data, known_fields: &[&str]) -> DecodeResult<Self> {
|
||||||
|
let Data::Object(fields) = data else {
|
||||||
|
return Err(DecodeError::at_type("", "Object"));
|
||||||
|
};
|
||||||
|
fields
|
||||||
|
.iter()
|
||||||
|
.filter(|field| !known_fields.contains(&field.name.as_str()))
|
||||||
|
.map(decode_rest_field::<T>)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn decode_rest_field<T: DecodalDecode>(field: &DataField) -> DecodeResult<(String, T)> {
|
||||||
|
T::decodal_decode(&field.value)
|
||||||
|
.map(|value| (field.name.clone(), value))
|
||||||
|
.map_err(|error| prefix_error(&field.name, error))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::runtime::{Constraint, DataField, PrimitiveType};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn string_maps_expose_map_schemas_and_decode_objects() {
|
||||||
|
assert!(matches!(
|
||||||
|
BTreeMap::<String, i64>::decodal_schema(),
|
||||||
|
Value::MapRange { value, .. }
|
||||||
|
if matches!(
|
||||||
|
*value,
|
||||||
|
Value::Range { ref constraints, .. }
|
||||||
|
if constraints == &[Constraint::Type(PrimitiveType::Int)]
|
||||||
|
)
|
||||||
|
));
|
||||||
|
|
||||||
|
let data = Data::Object(alloc::vec![DataField {
|
||||||
|
name: String::from("api"),
|
||||||
|
value: Data::Int(8080),
|
||||||
|
}]);
|
||||||
|
let decoded = BTreeMap::<String, i64>::decodal_decode(&data).unwrap();
|
||||||
|
assert_eq!(decoded.get("api"), Some(&8080));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn data_is_the_unknown_schema_and_round_trips_values() {
|
||||||
|
assert!(matches!(
|
||||||
|
Data::decodal_schema(),
|
||||||
|
Value::Range { ref constraints, .. }
|
||||||
|
if constraints == &[Constraint::Unknown]
|
||||||
|
));
|
||||||
|
|
||||||
|
let data = Data::Object(alloc::vec![DataField {
|
||||||
|
name: String::from("nested"),
|
||||||
|
value: Data::Array(alloc::vec![Data::Bool(true), Data::Int(2)]),
|
||||||
|
}]);
|
||||||
|
assert_eq!(Data::decodal_decode(&data).unwrap(), data);
|
||||||
|
assert!(matches!(data.into_value(), Value::Object { .. }));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rest_maps_decode_only_fields_outside_the_known_domain() {
|
||||||
|
let data = Data::Object(alloc::vec![
|
||||||
|
DataField {
|
||||||
|
name: String::from("name"),
|
||||||
|
value: Data::String(String::from("api")),
|
||||||
|
},
|
||||||
|
DataField {
|
||||||
|
name: String::from("priority"),
|
||||||
|
value: Data::Int(3),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
let decoded = BTreeMap::<String, Data>::decodal_decode_rest(&data, &["name"]).unwrap();
|
||||||
|
assert_eq!(decoded.len(), 1);
|
||||||
|
assert_eq!(decoded.get("priority"), Some(&Data::Int(3)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "std")]
|
||||||
|
#[test]
|
||||||
|
fn hash_maps_can_receive_rest_fields() {
|
||||||
|
let data = Data::Object(alloc::vec![DataField {
|
||||||
|
name: String::from("region"),
|
||||||
|
value: Data::String(String::from("ap-northeast-1")),
|
||||||
|
}]);
|
||||||
|
let decoded =
|
||||||
|
std::collections::HashMap::<String, String>::decodal_decode_rest(&data, &[]).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
decoded.get("region").map(String::as_str),
|
||||||
|
Some("ap-northeast-1")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,333 @@
|
|||||||
|
use alloc::{boxed::Box, string::String, vec::Vec};
|
||||||
|
|
||||||
|
use crate::runtime::{Constraint, LiteralValue, PrimitiveType};
|
||||||
|
use crate::{CompareOp, Diagnostic, DiagnosticKind, Result, Span};
|
||||||
|
|
||||||
|
/// A Decodal value supplied through the embedding API.
|
||||||
|
///
|
||||||
|
/// Unlike [`crate::Data`], a `Value` may contain unresolved ranges and
|
||||||
|
/// defaults. The evaluator converts it into its lazy runtime representation
|
||||||
|
/// when it is bound as a global or returned from an import loader.
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub enum Value {
|
||||||
|
String(String),
|
||||||
|
Int(i64),
|
||||||
|
Float(f64),
|
||||||
|
Bool(bool),
|
||||||
|
Array(Vec<Value>),
|
||||||
|
ArrayRange {
|
||||||
|
item: Box<Value>,
|
||||||
|
constraints: Vec<Constraint>,
|
||||||
|
default: Option<Box<Value>>,
|
||||||
|
},
|
||||||
|
MapRange {
|
||||||
|
value: Box<Value>,
|
||||||
|
constraints: Vec<Constraint>,
|
||||||
|
default: Option<Box<Value>>,
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
fields: Vec<ValueField>,
|
||||||
|
rest: Option<Box<Value>>,
|
||||||
|
},
|
||||||
|
Range {
|
||||||
|
constraints: Vec<Constraint>,
|
||||||
|
default: Option<Box<Value>>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A named field in a [`Value::Object`].
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct ValueField {
|
||||||
|
pub name: String,
|
||||||
|
pub value: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Value {
|
||||||
|
pub fn string(value: impl Into<String>) -> Self {
|
||||||
|
Self::String(value.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn int(value: i64) -> Self {
|
||||||
|
Self::Int(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn float(value: f64) -> Self {
|
||||||
|
Self::Float(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bool(value: bool) -> Self {
|
||||||
|
Self::Bool(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn array<I>(items: I) -> Self
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = Value>,
|
||||||
|
{
|
||||||
|
Self::Array(items.into_iter().collect())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn object<I, N>(fields: I) -> Self
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = (N, Value)>,
|
||||||
|
N: Into<String>,
|
||||||
|
{
|
||||||
|
Self::Object {
|
||||||
|
fields: fields
|
||||||
|
.into_iter()
|
||||||
|
.map(|(name, value)| ValueField {
|
||||||
|
name: name.into(),
|
||||||
|
value,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
rest: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn object_from_paths<I, N>(fields: I) -> Self
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = (N, Value)>,
|
||||||
|
N: Into<String>,
|
||||||
|
{
|
||||||
|
let mut root = Vec::new();
|
||||||
|
for (path, value) in fields {
|
||||||
|
insert_path(&mut root, &path.into(), value);
|
||||||
|
}
|
||||||
|
Self::Object {
|
||||||
|
fields: root,
|
||||||
|
rest: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn object_from_paths_with_rest<I, N>(fields: I, rest: Value) -> Self
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = (N, Value)>,
|
||||||
|
N: Into<String>,
|
||||||
|
{
|
||||||
|
let mut root = Vec::new();
|
||||||
|
for (path, value) in fields {
|
||||||
|
insert_path(&mut root, &path.into(), value);
|
||||||
|
}
|
||||||
|
Self::Object {
|
||||||
|
fields: root,
|
||||||
|
rest: Some(Box::new(rest)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn object_with_rest<I, N>(fields: I, rest: Value) -> Self
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = (N, Value)>,
|
||||||
|
N: Into<String>,
|
||||||
|
{
|
||||||
|
Self::Object {
|
||||||
|
fields: fields
|
||||||
|
.into_iter()
|
||||||
|
.map(|(name, value)| ValueField {
|
||||||
|
name: name.into(),
|
||||||
|
value,
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
rest: Some(Box::new(rest)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn unknown() -> Self {
|
||||||
|
Self::range_with_constraint(Constraint::Unknown)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn string_type() -> Self {
|
||||||
|
Self::range_with_constraint(Constraint::Type(PrimitiveType::String))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn int_type() -> Self {
|
||||||
|
Self::range_with_constraint(Constraint::Type(PrimitiveType::Int))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn float_type() -> Self {
|
||||||
|
Self::range_with_constraint(Constraint::Type(PrimitiveType::Float))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn bool_type() -> Self {
|
||||||
|
Self::range_with_constraint(Constraint::Type(PrimitiveType::Bool))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn array_of(item: Value) -> Self {
|
||||||
|
Self::ArrayRange {
|
||||||
|
item: Box::new(item),
|
||||||
|
constraints: Vec::new(),
|
||||||
|
default: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn map_of(value: Value) -> Self {
|
||||||
|
Self::MapRange {
|
||||||
|
value: Box::new(value),
|
||||||
|
constraints: Vec::new(),
|
||||||
|
default: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn builtin_predicate(name: impl Into<String>) -> Self {
|
||||||
|
Self::range_with_constraint(Constraint::BuiltinPredicate(name.into()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn range_with_constraint(constraint: Constraint) -> Self {
|
||||||
|
Self::Range {
|
||||||
|
constraints: alloc::vec![constraint],
|
||||||
|
default: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn with_constraint(mut self, constraint: Constraint) -> Self {
|
||||||
|
match &mut self {
|
||||||
|
Self::Range { constraints, .. } => constraints.push(constraint),
|
||||||
|
Self::ArrayRange { constraints, .. } | Self::MapRange { constraints, .. } => {
|
||||||
|
constraints.push(constraint)
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
self = Self::Range {
|
||||||
|
constraints: alloc::vec![constraint],
|
||||||
|
default: Some(Box::new(self)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn gt(self, value: i64) -> Self {
|
||||||
|
self.with_constraint(Constraint::Compare(CompareOp::Gt, LiteralValue::Int(value)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn gte(self, value: i64) -> Self {
|
||||||
|
self.with_constraint(Constraint::Compare(
|
||||||
|
CompareOp::Gte,
|
||||||
|
LiteralValue::Int(value),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn lt(self, value: i64) -> Self {
|
||||||
|
self.with_constraint(Constraint::Compare(CompareOp::Lt, LiteralValue::Int(value)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn lte(self, value: i64) -> Self {
|
||||||
|
self.with_constraint(Constraint::Compare(
|
||||||
|
CompareOp::Lte,
|
||||||
|
LiteralValue::Int(value),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default(self, value: Value) -> Result<Self> {
|
||||||
|
match self {
|
||||||
|
Self::ArrayRange {
|
||||||
|
item,
|
||||||
|
constraints,
|
||||||
|
default: None,
|
||||||
|
} => Ok(Self::ArrayRange {
|
||||||
|
item,
|
||||||
|
constraints,
|
||||||
|
default: Some(Box::new(value)),
|
||||||
|
}),
|
||||||
|
Self::ArrayRange {
|
||||||
|
default: Some(_), ..
|
||||||
|
} => Err(Diagnostic::new(
|
||||||
|
DiagnosticKind::DefaultConflict,
|
||||||
|
Span::default(),
|
||||||
|
"value already has a default",
|
||||||
|
)),
|
||||||
|
Self::MapRange {
|
||||||
|
value: map_value,
|
||||||
|
constraints,
|
||||||
|
default: None,
|
||||||
|
} => Ok(Self::MapRange {
|
||||||
|
value: map_value,
|
||||||
|
constraints,
|
||||||
|
default: Some(Box::new(value)),
|
||||||
|
}),
|
||||||
|
Self::MapRange {
|
||||||
|
default: Some(_), ..
|
||||||
|
} => Err(Diagnostic::new(
|
||||||
|
DiagnosticKind::DefaultConflict,
|
||||||
|
Span::default(),
|
||||||
|
"value already has a default",
|
||||||
|
)),
|
||||||
|
Self::Range {
|
||||||
|
constraints,
|
||||||
|
default: None,
|
||||||
|
} => Ok(Self::Range {
|
||||||
|
constraints,
|
||||||
|
default: Some(Box::new(value)),
|
||||||
|
}),
|
||||||
|
Self::Range { .. } => Err(Diagnostic::new(
|
||||||
|
DiagnosticKind::DefaultConflict,
|
||||||
|
Span::default(),
|
||||||
|
"value already has a default",
|
||||||
|
)),
|
||||||
|
concrete => Ok(Self::Range {
|
||||||
|
constraints: Vec::new(),
|
||||||
|
default: Some(Box::new(concrete)),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_string(self, value: impl Into<String>) -> Result<Self> {
|
||||||
|
self.default(Self::string(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_int(self, value: i64) -> Result<Self> {
|
||||||
|
self.default(Self::int(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_float(self, value: f64) -> Result<Self> {
|
||||||
|
self.default(Self::float(value))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn default_bool(self, value: bool) -> Result<Self> {
|
||||||
|
self.default(Self::bool(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ValueField {
|
||||||
|
pub fn new(name: impl Into<String>, value: Value) -> Self {
|
||||||
|
Self {
|
||||||
|
name: name.into(),
|
||||||
|
value,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert_path(fields: &mut Vec<ValueField>, path: &str, value: Value) {
|
||||||
|
let mut parts = path.splitn(2, '.');
|
||||||
|
let Some(head) = parts.next().filter(|part| !part.is_empty()) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
if let Some(tail) = parts.next() {
|
||||||
|
if let Some(field) = fields.iter_mut().find(|field| field.name == head) {
|
||||||
|
if let Value::Object {
|
||||||
|
fields: children, ..
|
||||||
|
} = &mut field.value
|
||||||
|
{
|
||||||
|
insert_path(children, tail, value);
|
||||||
|
} else {
|
||||||
|
let mut children = Vec::new();
|
||||||
|
insert_path(&mut children, tail, value);
|
||||||
|
field.value = Value::Object {
|
||||||
|
fields: children,
|
||||||
|
rest: None,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let mut children = Vec::new();
|
||||||
|
insert_path(&mut children, tail, value);
|
||||||
|
fields.push(ValueField::new(
|
||||||
|
head,
|
||||||
|
Value::Object {
|
||||||
|
fields: children,
|
||||||
|
rest: None,
|
||||||
|
},
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else if let Some(field) = fields.iter_mut().find(|field| field.name == head) {
|
||||||
|
field.value = value;
|
||||||
|
} else {
|
||||||
|
fields.push(ValueField::new(head, value));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "decodal-derive"
|
name = "decodal-derive"
|
||||||
version.workspace = true
|
version = "0.2.0"
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
rust-version.workspace = true
|
rust-version.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
@@ -19,4 +19,4 @@ quote = "1"
|
|||||||
syn = { version = "2", features = ["full"] }
|
syn = { version = "2", features = ["full"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
decodal = { version = "0.1", path = "../decodal-core" }
|
decodal = { path = "../decodal-core" }
|
||||||
|
|||||||
@@ -35,12 +35,38 @@ fn expand_decodal(input: DeriveInput) -> Result<TokenStream2> {
|
|||||||
|
|
||||||
let mut schema_fields = Vec::new();
|
let mut schema_fields = Vec::new();
|
||||||
let mut decode_fields = Vec::new();
|
let mut decode_fields = Vec::new();
|
||||||
|
let mut known_field_roots = Vec::new();
|
||||||
|
let mut rest_field = None;
|
||||||
|
|
||||||
for field in fields {
|
for field in fields {
|
||||||
let ident = field.ident.expect("named field");
|
let ident = field.ident.expect("named field");
|
||||||
let ty = field.ty;
|
let ty = field.ty;
|
||||||
let attrs = FieldAttrs::from_attrs(&field.attrs, &ident.to_string())?;
|
let attrs = FieldAttrs::from_attrs(&field.attrs, &ident.to_string())?;
|
||||||
|
|
||||||
|
if attrs.rest {
|
||||||
|
if attrs.rename_explicit || attrs.default.is_some() || !attrs.constraints.is_empty() {
|
||||||
|
return Err(syn::Error::new(
|
||||||
|
ident.span(),
|
||||||
|
"`rest` cannot be combined with rename, default, or field constraints",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if rest_field.is_some() {
|
||||||
|
return Err(syn::Error::new(
|
||||||
|
ident.span(),
|
||||||
|
"Decodal derive supports only one `rest` field",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
rest_field = Some((ident, ty));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
let path = attrs.rename.clone();
|
let path = attrs.rename.clone();
|
||||||
|
known_field_roots.push(
|
||||||
|
path.split('.')
|
||||||
|
.next()
|
||||||
|
.expect("field paths are non-empty")
|
||||||
|
.to_owned(),
|
||||||
|
);
|
||||||
let schema_value = schema_expr(&ty, &attrs)?;
|
let schema_value = schema_expr(&ty, &attrs)?;
|
||||||
let decode_value = decode_expr(&ty, &path, &attrs);
|
let decode_value = decode_expr(&ty, &path, &attrs);
|
||||||
|
|
||||||
@@ -52,14 +78,40 @@ fn expand_decodal(input: DeriveInput) -> Result<TokenStream2> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some((ident, ty)) = &rest_field {
|
||||||
|
decode_fields.push(quote! {
|
||||||
|
#ident: <#ty as ::decodal::DecodalRest>::decodal_decode_rest(
|
||||||
|
data,
|
||||||
|
&[#(#known_field_roots),*],
|
||||||
|
)?
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let schema_entries = if schema_fields.is_empty() {
|
||||||
|
quote! { ::core::iter::empty::<(&'static str, ::decodal::Value)>() }
|
||||||
|
} else {
|
||||||
|
quote! { [#(#schema_fields),*] }
|
||||||
|
};
|
||||||
|
|
||||||
|
let build_schema = if let Some((_, ty)) = &rest_field {
|
||||||
|
quote! {
|
||||||
|
::decodal::Value::object_from_paths_with_rest(
|
||||||
|
#schema_entries,
|
||||||
|
<#ty as ::decodal::DecodalRest>::decodal_rest_schema(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
quote! {
|
||||||
|
::decodal::Value::object_from_paths(#schema_entries)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
|
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
|
||||||
|
|
||||||
Ok(quote! {
|
Ok(quote! {
|
||||||
impl #impl_generics ::decodal::DecodalSchema for #name #ty_generics #where_clause {
|
impl #impl_generics ::decodal::DecodalSchema for #name #ty_generics #where_clause {
|
||||||
fn decodal_schema() -> ::decodal::HostValue {
|
fn decodal_schema() -> ::decodal::Value {
|
||||||
::decodal::HostValue::object_from_paths([
|
#build_schema
|
||||||
#(#schema_fields),*
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,6 +128,8 @@ fn expand_decodal(input: DeriveInput) -> Result<TokenStream2> {
|
|||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct FieldAttrs {
|
struct FieldAttrs {
|
||||||
rename: String,
|
rename: String,
|
||||||
|
rename_explicit: bool,
|
||||||
|
rest: bool,
|
||||||
default: Option<DefaultAttr>,
|
default: Option<DefaultAttr>,
|
||||||
constraints: Vec<ConstraintAttr>,
|
constraints: Vec<ConstraintAttr>,
|
||||||
}
|
}
|
||||||
@@ -95,6 +149,14 @@ impl FieldAttrs {
|
|||||||
if meta.path.is_ident("rename") {
|
if meta.path.is_ident("rename") {
|
||||||
let value: LitStr = meta.value()?.parse()?;
|
let value: LitStr = meta.value()?.parse()?;
|
||||||
output.rename = value.value();
|
output.rename = value.value();
|
||||||
|
output.rename_explicit = true;
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
if meta.path.is_ident("rest") {
|
||||||
|
if meta.input.peek(syn::Token![=]) || meta.input.peek(syn::token::Paren) {
|
||||||
|
return Err(meta.error("`rest` does not accept a value"));
|
||||||
|
}
|
||||||
|
output.rest = true;
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
if meta.path.is_ident("default") {
|
if meta.path.is_ident("default") {
|
||||||
@@ -182,7 +244,7 @@ fn schema_expr(ty: &Type, attrs: &FieldAttrs) -> Result<TokenStream2> {
|
|||||||
};
|
};
|
||||||
tokens.extend(quote! {
|
tokens.extend(quote! {
|
||||||
schema = schema
|
schema = schema
|
||||||
.default(::decodal::IntoHostValue::into_host_value(#default_expr))
|
.default(::decodal::IntoValue::into_value(#default_expr))
|
||||||
.expect("Decodal derive generated a valid default");
|
.expect("Decodal derive generated a valid default");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -210,3 +272,42 @@ fn decode_expr(ty: &Type, path: &str, attrs: &FieldAttrs) -> TokenStream2 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_multiple_rest_fields() {
|
||||||
|
let input: DeriveInput = syn::parse_quote! {
|
||||||
|
struct Invalid {
|
||||||
|
#[decodal(rest)]
|
||||||
|
first: std::collections::BTreeMap<String, String>,
|
||||||
|
#[decodal(rest)]
|
||||||
|
second: std::collections::BTreeMap<String, String>,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
assert!(
|
||||||
|
expand_decodal(input)
|
||||||
|
.unwrap_err()
|
||||||
|
.to_string()
|
||||||
|
.contains("only one `rest` field")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_rest_field_modifiers() {
|
||||||
|
let input: DeriveInput = syn::parse_quote! {
|
||||||
|
struct Invalid {
|
||||||
|
#[decodal(rest, default)]
|
||||||
|
extra: std::collections::BTreeMap<String, String>,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
assert!(
|
||||||
|
expand_decodal(input)
|
||||||
|
.unwrap_err()
|
||||||
|
.to_string()
|
||||||
|
.contains("cannot be combined")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
use decodal::{Data, DecodalDecode, DecodalSchema, EmptyLoader, Engine};
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
use decodal::{Constraint, Data, DecodalDecode, DecodalSchema, EmptyLoader, Engine, Value};
|
||||||
use decodal_derive::Decodal;
|
use decodal_derive::Decodal;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Decodal)]
|
#[derive(Debug, PartialEq, Decodal)]
|
||||||
@@ -12,6 +14,40 @@ struct Service {
|
|||||||
tags: Vec<String>,
|
tags: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Decodal)]
|
||||||
|
struct Worker {
|
||||||
|
name: String,
|
||||||
|
#[decodal(default = true)]
|
||||||
|
enabled: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Decodal)]
|
||||||
|
struct Fleet {
|
||||||
|
workers: Vec<Worker>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Decodal)]
|
||||||
|
struct OpenConfig {
|
||||||
|
name: String,
|
||||||
|
#[decodal(default = true, rename = "feature.enabled")]
|
||||||
|
enabled: bool,
|
||||||
|
#[decodal(rest)]
|
||||||
|
extra: BTreeMap<String, Data>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Decodal)]
|
||||||
|
struct StringLabels {
|
||||||
|
name: String,
|
||||||
|
#[decodal(rest)]
|
||||||
|
labels: BTreeMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Decodal)]
|
||||||
|
struct OpenBag {
|
||||||
|
#[decodal(rest)]
|
||||||
|
values: BTreeMap<String, Data>,
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn derives_schema_and_decode() {
|
fn derives_schema_and_decode() {
|
||||||
let mut engine = Engine::new(EmptyLoader);
|
let mut engine = Engine::new(EmptyLoader);
|
||||||
@@ -77,3 +113,156 @@ fn decode_reports_field_path() {
|
|||||||
let error = Service::decodal_decode(&data).unwrap_err();
|
let error = Service::decodal_decode(&data).unwrap_err();
|
||||||
assert_eq!(error.path, "name");
|
assert_eq!(error.path, "name");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn vec_schema_rejects_invalid_element_before_decode() {
|
||||||
|
let mut engine = Engine::new(EmptyLoader);
|
||||||
|
engine
|
||||||
|
.bind_global("Service", Service::decodal_schema())
|
||||||
|
.unwrap();
|
||||||
|
let module = engine
|
||||||
|
.add_root_source(
|
||||||
|
"test",
|
||||||
|
"test",
|
||||||
|
r#"
|
||||||
|
Service & {
|
||||||
|
name = "api";
|
||||||
|
port = 9443;
|
||||||
|
tags = ["web", 1];
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let error = engine.eval_module(module).unwrap_err();
|
||||||
|
assert!(
|
||||||
|
error
|
||||||
|
.labels
|
||||||
|
.iter()
|
||||||
|
.any(|label| label.message.contains("array element [1]"))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn vec_schema_applies_nested_struct_defaults() {
|
||||||
|
let mut engine = Engine::new(EmptyLoader);
|
||||||
|
engine
|
||||||
|
.bind_global("Fleet", Fleet::decodal_schema())
|
||||||
|
.unwrap();
|
||||||
|
let module = engine
|
||||||
|
.add_root_source(
|
||||||
|
"test",
|
||||||
|
"test",
|
||||||
|
r#"
|
||||||
|
Fleet & {
|
||||||
|
workers = [{ name = "api"; }];
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let value = engine.eval_module(module).unwrap();
|
||||||
|
let data = engine.materialize(&value).unwrap();
|
||||||
|
let fleet = Fleet::decodal_decode(&data).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
fleet,
|
||||||
|
Fleet {
|
||||||
|
workers: vec![Worker {
|
||||||
|
name: "api".into(),
|
||||||
|
enabled: true,
|
||||||
|
}],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rest_map_opens_the_derived_object_and_collects_additional_fields() {
|
||||||
|
let schema = OpenConfig::decodal_schema();
|
||||||
|
let Value::Object {
|
||||||
|
fields,
|
||||||
|
rest: Some(rest),
|
||||||
|
} = schema
|
||||||
|
else {
|
||||||
|
panic!("derived schema should have an object rest range")
|
||||||
|
};
|
||||||
|
assert_eq!(fields.len(), 2);
|
||||||
|
assert!(matches!(
|
||||||
|
*rest,
|
||||||
|
Value::Range { ref constraints, .. }
|
||||||
|
if constraints == &[Constraint::Unknown]
|
||||||
|
));
|
||||||
|
|
||||||
|
let mut engine = Engine::new(EmptyLoader);
|
||||||
|
engine
|
||||||
|
.bind_global("OpenConfig", OpenConfig::decodal_schema())
|
||||||
|
.unwrap();
|
||||||
|
let module = engine
|
||||||
|
.add_root_source(
|
||||||
|
"test",
|
||||||
|
"test",
|
||||||
|
r#"
|
||||||
|
{
|
||||||
|
name = "api";
|
||||||
|
plugin = { name = "cache"; };
|
||||||
|
retries = 3;
|
||||||
|
} as OpenConfig
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let value = engine.eval_module(module).unwrap();
|
||||||
|
let data = engine.materialize(&value).unwrap();
|
||||||
|
let config = OpenConfig::decodal_decode(&data).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(config.name, "api");
|
||||||
|
assert!(config.enabled);
|
||||||
|
assert_eq!(config.extra.get("retries"), Some(&Data::Int(3)));
|
||||||
|
assert!(matches!(config.extra.get("plugin"), Some(Data::Object(_))));
|
||||||
|
assert!(!config.extra.contains_key("name"));
|
||||||
|
assert!(!config.extra.contains_key("feature"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn typed_rest_map_constrains_each_additional_field() {
|
||||||
|
let mut engine = Engine::new(EmptyLoader);
|
||||||
|
engine
|
||||||
|
.bind_global("StringLabels", StringLabels::decodal_schema())
|
||||||
|
.unwrap();
|
||||||
|
let module = engine
|
||||||
|
.add_root_source(
|
||||||
|
"test",
|
||||||
|
"test",
|
||||||
|
r#"{ name = "service"; region = "ap-northeast-1"; tier = "edge"; } as StringLabels"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let value = engine.eval_module(module).unwrap();
|
||||||
|
let data = engine.materialize(&value).unwrap();
|
||||||
|
let labels = StringLabels::decodal_decode(&data).unwrap();
|
||||||
|
assert_eq!(labels.labels.get("region"), Some(&"ap-northeast-1".into()));
|
||||||
|
assert_eq!(labels.labels.get("tier"), Some(&"edge".into()));
|
||||||
|
|
||||||
|
let module = engine
|
||||||
|
.add_root_source(
|
||||||
|
"invalid",
|
||||||
|
"invalid",
|
||||||
|
r#"{ name = "service"; priority = 1; } as StringLabels"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(engine.eval_module(module).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rest_only_struct_collects_every_field() {
|
||||||
|
let schema = OpenBag::decodal_schema();
|
||||||
|
assert!(matches!(
|
||||||
|
schema,
|
||||||
|
Value::Object {
|
||||||
|
ref fields,
|
||||||
|
rest: Some(_),
|
||||||
|
} if fields.is_empty()
|
||||||
|
));
|
||||||
|
|
||||||
|
let data = Data::Object(vec![decodal::runtime::DataField {
|
||||||
|
name: "answer".into(),
|
||||||
|
value: Data::Int(42),
|
||||||
|
}]);
|
||||||
|
let bag = OpenBag::decodal_decode(&data).unwrap();
|
||||||
|
assert_eq!(bag.values.get("answer"), Some(&Data::Int(42)));
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
[package]
|
||||||
|
name = "decodal-language-service"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
readme.workspace = true
|
||||||
|
description = "Host-configurable semantic language service for Decodal."
|
||||||
|
keywords = ["decodal", "lsp", "language-server", "editor"]
|
||||||
|
categories = ["development-tools", "text-editors"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
decodal = { version = "0.4.0", path = "../decodal-core" }
|
||||||
@@ -0,0 +1,913 @@
|
|||||||
|
use std::collections::{BTreeMap, HashMap};
|
||||||
|
|
||||||
|
use decodal::{
|
||||||
|
Engine, HostEnvironment, ImportLoader, LoadedImport, Result, Value, runtime::RuntimeValue,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum CompletionKind {
|
||||||
|
Keyword,
|
||||||
|
Constant,
|
||||||
|
Type,
|
||||||
|
Variable,
|
||||||
|
Namespace,
|
||||||
|
Property,
|
||||||
|
File,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct CompletionItem {
|
||||||
|
pub label: String,
|
||||||
|
pub kind: CompletionKind,
|
||||||
|
pub detail: Option<String>,
|
||||||
|
pub priority: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct CompletionResult {
|
||||||
|
/// UTF-8 byte offset at which the unfinished token starts.
|
||||||
|
pub from: usize,
|
||||||
|
pub items: Vec<CompletionItem>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||||
|
struct FieldTree(BTreeMap<String, FieldTree>);
|
||||||
|
|
||||||
|
impl FieldTree {
|
||||||
|
fn new() -> Self {
|
||||||
|
Self::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_empty(&self) -> bool {
|
||||||
|
self.0.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn insert(&mut self, name: String, fields: FieldTree) {
|
||||||
|
self.0.insert(name, fields);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get(&self, name: &str) -> Option<&FieldTree> {
|
||||||
|
self.0.get(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove(&mut self, name: &str) -> Option<FieldTree> {
|
||||||
|
self.0.remove(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoIterator for FieldTree {
|
||||||
|
type Item = (String, FieldTree);
|
||||||
|
type IntoIter = std::collections::btree_map::IntoIter<String, FieldTree>;
|
||||||
|
|
||||||
|
fn into_iter(self) -> Self::IntoIter {
|
||||||
|
self.0.into_iter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FromIterator<(String, FieldTree)> for FieldTree {
|
||||||
|
fn from_iter<T: IntoIterator<Item = (String, FieldTree)>>(iter: T) -> Self {
|
||||||
|
Self(iter.into_iter().collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn complete<E: HostEnvironment>(
|
||||||
|
environment: &E,
|
||||||
|
key: &str,
|
||||||
|
source: &str,
|
||||||
|
position: usize,
|
||||||
|
explicit: bool,
|
||||||
|
) -> Result<Option<CompletionResult>> {
|
||||||
|
let position = floor_char_boundary(source, position.min(source.len()));
|
||||||
|
if let Some((from, prefix)) = unfinished_import(source, position) {
|
||||||
|
let mut loader = environment.create_loader();
|
||||||
|
let items = loader
|
||||||
|
.complete_import(Some(key), prefix)?
|
||||||
|
.into_iter()
|
||||||
|
.map(|candidate| CompletionItem {
|
||||||
|
label: candidate.specifier,
|
||||||
|
kind: CompletionKind::File,
|
||||||
|
detail: candidate.detail,
|
||||||
|
priority: 30,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
return Ok(Some(CompletionResult {
|
||||||
|
from,
|
||||||
|
items: unique_items(items),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
if in_string_or_comment(source, position) {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let tokens = tokenize(source);
|
||||||
|
let local_fields = collect_fields(&tokens);
|
||||||
|
let imports = collect_import_bindings(&tokens);
|
||||||
|
let globals = collect_globals(environment)?;
|
||||||
|
|
||||||
|
if let Some((from, path)) = member_path(source, position) {
|
||||||
|
let mut parts = path.split('.');
|
||||||
|
let Some(root) = parts.next() else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let mut fields;
|
||||||
|
let detail;
|
||||||
|
if let Some(specifier) = imports.get(root) {
|
||||||
|
let mut loader = environment.create_loader();
|
||||||
|
let loaded = loader.load(Some(key), specifier)?;
|
||||||
|
detail = match &loaded {
|
||||||
|
LoadedImport::Source { name, .. } => name.clone(),
|
||||||
|
LoadedImport::Value { key, .. } => key.clone(),
|
||||||
|
};
|
||||||
|
fields = match loaded {
|
||||||
|
LoadedImport::Source { source, .. } => collect_fields(&tokenize(&source)),
|
||||||
|
LoadedImport::Value { value, .. } => fields_from_value(&value),
|
||||||
|
};
|
||||||
|
} else if let Some(local) = local_fields.get(root) {
|
||||||
|
fields = local.clone();
|
||||||
|
detail = String::from("local value");
|
||||||
|
} else if let Some(global) = globals.get(root) {
|
||||||
|
fields = global.clone();
|
||||||
|
detail = String::from("host global");
|
||||||
|
} else {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
for part in parts {
|
||||||
|
let Some(nested) = fields.remove(part) else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
fields = nested;
|
||||||
|
}
|
||||||
|
let items = fields
|
||||||
|
.into_iter()
|
||||||
|
.map(|(label, children)| CompletionItem {
|
||||||
|
label,
|
||||||
|
kind: if children.is_empty() {
|
||||||
|
CompletionKind::Property
|
||||||
|
} else {
|
||||||
|
CompletionKind::Namespace
|
||||||
|
},
|
||||||
|
detail: Some(detail.clone()),
|
||||||
|
priority: 30,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
return Ok(Some(CompletionResult {
|
||||||
|
from,
|
||||||
|
items: unique_items(items),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
let word_from = word_start(source, position);
|
||||||
|
if word_from == position && !explicit {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut items = builtin_items();
|
||||||
|
for (label, children) in local_fields {
|
||||||
|
items.push(CompletionItem {
|
||||||
|
label,
|
||||||
|
kind: if children.is_empty() {
|
||||||
|
CompletionKind::Variable
|
||||||
|
} else {
|
||||||
|
CompletionKind::Namespace
|
||||||
|
},
|
||||||
|
detail: Some(if children.is_empty() {
|
||||||
|
String::from("local value")
|
||||||
|
} else {
|
||||||
|
String::from("local object")
|
||||||
|
}),
|
||||||
|
priority: 20,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for label in collect_parameters(&tokens) {
|
||||||
|
items.push(CompletionItem {
|
||||||
|
label,
|
||||||
|
kind: CompletionKind::Variable,
|
||||||
|
detail: Some(String::from("parameter")),
|
||||||
|
priority: 20,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (label, specifier) in imports {
|
||||||
|
items.push(CompletionItem {
|
||||||
|
label,
|
||||||
|
kind: CompletionKind::Namespace,
|
||||||
|
detail: Some(specifier),
|
||||||
|
priority: 30,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
for (label, children) in globals {
|
||||||
|
items.push(CompletionItem {
|
||||||
|
label,
|
||||||
|
kind: if children.is_empty() {
|
||||||
|
CompletionKind::Variable
|
||||||
|
} else {
|
||||||
|
CompletionKind::Namespace
|
||||||
|
},
|
||||||
|
detail: Some(String::from("host global")),
|
||||||
|
priority: 40,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(Some(CompletionResult {
|
||||||
|
from: word_from,
|
||||||
|
items: unique_items(items),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn builtin_items() -> Vec<CompletionItem> {
|
||||||
|
[
|
||||||
|
("let", CompletionKind::Keyword, "local bindings", 5),
|
||||||
|
("in", CompletionKind::Keyword, "let body", 0),
|
||||||
|
("match", CompletionKind::Keyword, "pattern matching", 5),
|
||||||
|
("import", CompletionKind::Keyword, "load a module", 5),
|
||||||
|
("default", CompletionKind::Keyword, "fallback value", 0),
|
||||||
|
("as", CompletionKind::Keyword, "refine narrower as wider", 0),
|
||||||
|
("true", CompletionKind::Constant, "Bool", 0),
|
||||||
|
("false", CompletionKind::Constant, "Bool", 0),
|
||||||
|
("String", CompletionKind::Type, "string constraint", 5),
|
||||||
|
("Int", CompletionKind::Type, "integer constraint", 5),
|
||||||
|
("Float", CompletionKind::Type, "float constraint", 5),
|
||||||
|
("Bool", CompletionKind::Type, "boolean constraint", 5),
|
||||||
|
("Unknown", CompletionKind::Type, "unknown top range", 5),
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.map(|(label, kind, detail, priority)| CompletionItem {
|
||||||
|
label: label.into(),
|
||||||
|
kind,
|
||||||
|
detail: Some(detail.into()),
|
||||||
|
priority,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_globals<E: HostEnvironment>(environment: &E) -> Result<FieldTree> {
|
||||||
|
let mut engine = environment.create_engine()?;
|
||||||
|
let mut fields = FieldTree::new();
|
||||||
|
for (name, value) in engine.global_values()? {
|
||||||
|
fields.insert(name, fields_from_runtime(&mut engine, &value, 0)?);
|
||||||
|
}
|
||||||
|
Ok(fields)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fields_from_runtime<L: ImportLoader>(
|
||||||
|
engine: &mut Engine<L>,
|
||||||
|
value: &RuntimeValue,
|
||||||
|
depth: usize,
|
||||||
|
) -> Result<FieldTree> {
|
||||||
|
if depth >= 32 {
|
||||||
|
return Ok(FieldTree::new());
|
||||||
|
}
|
||||||
|
let Some(fields) = engine.value_fields(value)? else {
|
||||||
|
return Ok(FieldTree::new());
|
||||||
|
};
|
||||||
|
let mut tree = FieldTree::new();
|
||||||
|
for (name, value) in fields {
|
||||||
|
tree.insert(name, fields_from_runtime(engine, &value, depth + 1)?);
|
||||||
|
}
|
||||||
|
Ok(tree)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn fields_from_value(value: &Value) -> FieldTree {
|
||||||
|
match value {
|
||||||
|
Value::Object { fields, .. } => fields
|
||||||
|
.iter()
|
||||||
|
.map(|field| (field.name.clone(), fields_from_value(&field.value)))
|
||||||
|
.collect(),
|
||||||
|
Value::ArrayRange {
|
||||||
|
default: Some(value),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| Value::MapRange {
|
||||||
|
default: Some(value),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| Value::Range {
|
||||||
|
default: Some(value),
|
||||||
|
..
|
||||||
|
} => fields_from_value(value),
|
||||||
|
_ => FieldTree::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unique_items(items: Vec<CompletionItem>) -> Vec<CompletionItem> {
|
||||||
|
let mut unique = HashMap::<String, CompletionItem>::new();
|
||||||
|
let mut order = Vec::new();
|
||||||
|
for item in items {
|
||||||
|
if !unique.contains_key(&item.label) {
|
||||||
|
order.push(item.label.clone());
|
||||||
|
}
|
||||||
|
let replace = unique
|
||||||
|
.get(&item.label)
|
||||||
|
.is_none_or(|previous| item.priority > previous.priority);
|
||||||
|
if replace {
|
||||||
|
unique.insert(item.label.clone(), item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
order
|
||||||
|
.into_iter()
|
||||||
|
.filter_map(|label| unique.remove(&label))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
enum TokenKind {
|
||||||
|
Identifier(String),
|
||||||
|
String(String),
|
||||||
|
Symbol(char),
|
||||||
|
Arrow,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
struct Token {
|
||||||
|
kind: TokenKind,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tokenize(source: &str) -> Vec<Token> {
|
||||||
|
let bytes = source.as_bytes();
|
||||||
|
let mut tokens = Vec::new();
|
||||||
|
let mut index = 0;
|
||||||
|
while index < bytes.len() {
|
||||||
|
match bytes[index] {
|
||||||
|
b' ' | b'\t' | b'\r' | b'\n' => index += 1,
|
||||||
|
b'#' => {
|
||||||
|
while index < bytes.len() && bytes[index] != b'\n' {
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b'"' => {
|
||||||
|
index += 1;
|
||||||
|
let start = index;
|
||||||
|
let mut escaped = false;
|
||||||
|
while index < bytes.len() {
|
||||||
|
let byte = bytes[index];
|
||||||
|
if escaped {
|
||||||
|
escaped = false;
|
||||||
|
} else if byte == b'\\' {
|
||||||
|
escaped = true;
|
||||||
|
} else if byte == b'"' || byte == b'\n' {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
let value = unescape_string(&source[start..index]);
|
||||||
|
index = index.saturating_add(1);
|
||||||
|
tokens.push(Token {
|
||||||
|
kind: TokenKind::String(value),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
byte if is_identifier_start(byte) => {
|
||||||
|
let start = index;
|
||||||
|
index += 1;
|
||||||
|
while index < bytes.len() && is_identifier_continue(bytes[index]) {
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
tokens.push(Token {
|
||||||
|
kind: TokenKind::Identifier(source[start..index].into()),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
b'=' if bytes.get(index + 1) == Some(&b'>') => {
|
||||||
|
tokens.push(Token {
|
||||||
|
kind: TokenKind::Arrow,
|
||||||
|
});
|
||||||
|
index += 2;
|
||||||
|
}
|
||||||
|
byte => {
|
||||||
|
tokens.push(Token {
|
||||||
|
kind: TokenKind::Symbol(byte as char),
|
||||||
|
});
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unescape_string(value: &str) -> String {
|
||||||
|
let mut chars = value.chars();
|
||||||
|
let mut unescaped = String::new();
|
||||||
|
while let Some(ch) = chars.next() {
|
||||||
|
if ch != '\\' {
|
||||||
|
unescaped.push(ch);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(escaped) = chars.next() else {
|
||||||
|
unescaped.push('\\');
|
||||||
|
break;
|
||||||
|
};
|
||||||
|
unescaped.push(match escaped {
|
||||||
|
'n' => '\n',
|
||||||
|
'r' => '\r',
|
||||||
|
't' => '\t',
|
||||||
|
escaped => escaped,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
unescaped
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_fields(tokens: &[Token]) -> FieldTree {
|
||||||
|
let mut fields = FieldTree::new();
|
||||||
|
collect_fields_in(tokens, 0, tokens.len(), &mut fields);
|
||||||
|
fields
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_fields_in(tokens: &[Token], start: usize, end: usize, fields: &mut FieldTree) {
|
||||||
|
let mut index = start;
|
||||||
|
while index < end {
|
||||||
|
let Some((path, equals)) = field_definition_at(tokens, index, end) else {
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let value_start = equals + 1;
|
||||||
|
let value_end = definition_end(tokens, value_start, end);
|
||||||
|
let mut nested = &mut *fields;
|
||||||
|
for part in path {
|
||||||
|
nested = nested.0.entry(part).or_default();
|
||||||
|
}
|
||||||
|
collect_fields_in(tokens, value_start, value_end, nested);
|
||||||
|
index = value_end.saturating_add(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn field_definition_at(tokens: &[Token], start: usize, end: usize) -> Option<(Vec<String>, usize)> {
|
||||||
|
let TokenKind::Identifier(first) = &tokens.get(start)?.kind else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
let mut path = vec![first.clone()];
|
||||||
|
let mut index = start + 1;
|
||||||
|
while index + 1 < end
|
||||||
|
&& matches!(tokens[index].kind, TokenKind::Symbol('.'))
|
||||||
|
&& matches!(tokens[index + 1].kind, TokenKind::Identifier(_))
|
||||||
|
{
|
||||||
|
let TokenKind::Identifier(part) = &tokens[index + 1].kind else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
path.push(part.clone());
|
||||||
|
index += 2;
|
||||||
|
}
|
||||||
|
matches!(
|
||||||
|
tokens.get(index).map(|token| &token.kind),
|
||||||
|
Some(TokenKind::Symbol('='))
|
||||||
|
)
|
||||||
|
.then_some((path, index))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn definition_end(tokens: &[Token], start: usize, end: usize) -> usize {
|
||||||
|
let mut delimiters = Vec::new();
|
||||||
|
for (index, token) in tokens.iter().enumerate().take(end).skip(start) {
|
||||||
|
match token.kind {
|
||||||
|
TokenKind::Symbol('(' | '[' | '{') => delimiters.push(token.kind.clone()),
|
||||||
|
TokenKind::Symbol(')') if matches!(delimiters.last(), Some(TokenKind::Symbol('('))) => {
|
||||||
|
delimiters.pop();
|
||||||
|
}
|
||||||
|
TokenKind::Symbol(']') if matches!(delimiters.last(), Some(TokenKind::Symbol('['))) => {
|
||||||
|
delimiters.pop();
|
||||||
|
}
|
||||||
|
TokenKind::Symbol('}') if matches!(delimiters.last(), Some(TokenKind::Symbol('{'))) => {
|
||||||
|
delimiters.pop();
|
||||||
|
}
|
||||||
|
TokenKind::Symbol(';') if delimiters.is_empty() => return index,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
end
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_import_bindings(tokens: &[Token]) -> BTreeMap<String, String> {
|
||||||
|
let mut imports = BTreeMap::new();
|
||||||
|
for window in tokens.windows(4) {
|
||||||
|
let (
|
||||||
|
TokenKind::Identifier(binding),
|
||||||
|
TokenKind::Symbol('='),
|
||||||
|
TokenKind::Identifier(keyword),
|
||||||
|
TokenKind::String(specifier),
|
||||||
|
) = (
|
||||||
|
&window[0].kind,
|
||||||
|
&window[1].kind,
|
||||||
|
&window[2].kind,
|
||||||
|
&window[3].kind,
|
||||||
|
)
|
||||||
|
else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if keyword == "import" {
|
||||||
|
imports.insert(binding.clone(), specifier.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
imports
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_parameters(tokens: &[Token]) -> Vec<String> {
|
||||||
|
let mut parameters = Vec::new();
|
||||||
|
for (close, token) in tokens.iter().enumerate() {
|
||||||
|
if !matches!(token.kind, TokenKind::Symbol(')'))
|
||||||
|
|| !matches!(
|
||||||
|
tokens.get(close + 1).map(|token| &token.kind),
|
||||||
|
Some(TokenKind::Arrow)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let Some(open) = matching_open_paren(tokens, close) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let mut depth = 0usize;
|
||||||
|
let mut segment_start = true;
|
||||||
|
for token in &tokens[open + 1..close] {
|
||||||
|
match &token.kind {
|
||||||
|
TokenKind::Symbol('(' | '[' | '{') => depth += 1,
|
||||||
|
TokenKind::Symbol(')' | ']' | '}') => depth = depth.saturating_sub(1),
|
||||||
|
TokenKind::Symbol(',') if depth == 0 => segment_start = true,
|
||||||
|
TokenKind::Identifier(name) if depth == 0 && segment_start => {
|
||||||
|
parameters.push(name.clone());
|
||||||
|
segment_start = false;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parameters
|
||||||
|
}
|
||||||
|
|
||||||
|
fn matching_open_paren(tokens: &[Token], close: usize) -> Option<usize> {
|
||||||
|
let mut depth = 0usize;
|
||||||
|
for index in (0..close).rev() {
|
||||||
|
match tokens[index].kind {
|
||||||
|
TokenKind::Symbol(')') => depth += 1,
|
||||||
|
TokenKind::Symbol('(') if depth == 0 => return Some(index),
|
||||||
|
TokenKind::Symbol('(') => depth -= 1,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unfinished_import(source: &str, position: usize) -> Option<(usize, &str)> {
|
||||||
|
let bytes = source.as_bytes();
|
||||||
|
let mut index = 0usize;
|
||||||
|
let mut previous_word = None;
|
||||||
|
while index < position {
|
||||||
|
match bytes[index] {
|
||||||
|
b' ' | b'\t' | b'\r' | b'\n' => index += 1,
|
||||||
|
b'#' => {
|
||||||
|
while index < position && bytes[index] != b'\n' {
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
previous_word = None;
|
||||||
|
}
|
||||||
|
b'"' => {
|
||||||
|
let from = index + 1;
|
||||||
|
index += 1;
|
||||||
|
let mut escaped = false;
|
||||||
|
while index < position {
|
||||||
|
let byte = bytes[index];
|
||||||
|
if escaped {
|
||||||
|
escaped = false;
|
||||||
|
} else if byte == b'\\' {
|
||||||
|
escaped = true;
|
||||||
|
} else if byte == b'"' || byte == b'\n' {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
if index == position && previous_word == Some("import") {
|
||||||
|
return Some((from, &source[from..position]));
|
||||||
|
}
|
||||||
|
index = index.saturating_add(1);
|
||||||
|
previous_word = None;
|
||||||
|
}
|
||||||
|
byte if is_identifier_start(byte) => {
|
||||||
|
let start = index;
|
||||||
|
index += 1;
|
||||||
|
while index < position && is_identifier_continue(bytes[index]) {
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
previous_word = Some(&source[start..index]);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
index += 1;
|
||||||
|
previous_word = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
fn in_string_or_comment(source: &str, position: usize) -> bool {
|
||||||
|
let bytes = source.as_bytes();
|
||||||
|
let mut index = 0usize;
|
||||||
|
while index < position {
|
||||||
|
match bytes[index] {
|
||||||
|
b'#' => {
|
||||||
|
while index < position && bytes[index] != b'\n' {
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
if index == position {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b'"' => {
|
||||||
|
index += 1;
|
||||||
|
let mut escaped = false;
|
||||||
|
while index < position {
|
||||||
|
let byte = bytes[index];
|
||||||
|
if escaped {
|
||||||
|
escaped = false;
|
||||||
|
} else if byte == b'\\' {
|
||||||
|
escaped = true;
|
||||||
|
} else if byte == b'"' || byte == b'\n' {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
|
if index == position {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
index = index.saturating_add(1);
|
||||||
|
}
|
||||||
|
_ => index += 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn member_path(source: &str, position: usize) -> Option<(usize, &str)> {
|
||||||
|
let bytes = source.as_bytes();
|
||||||
|
let from = word_start(source, position);
|
||||||
|
if from == 0 || bytes[from - 1] != b'.' {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut base_start = from - 1;
|
||||||
|
while base_start > 0 {
|
||||||
|
let byte = bytes[base_start - 1];
|
||||||
|
if is_identifier_continue(byte) || byte == b'.' {
|
||||||
|
base_start -= 1;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let base = &source[base_start..from - 1];
|
||||||
|
(!base.is_empty()
|
||||||
|
&& base
|
||||||
|
.split('.')
|
||||||
|
.all(|part| !part.is_empty() && is_identifier(part)))
|
||||||
|
.then_some((from, base))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn word_start(source: &str, position: usize) -> usize {
|
||||||
|
let bytes = source.as_bytes();
|
||||||
|
let mut from = position;
|
||||||
|
while from > 0 && is_identifier_continue(bytes[from - 1]) {
|
||||||
|
from -= 1;
|
||||||
|
}
|
||||||
|
from
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_identifier(value: &str) -> bool {
|
||||||
|
let bytes = value.as_bytes();
|
||||||
|
bytes.first().is_some_and(|byte| is_identifier_start(*byte))
|
||||||
|
&& bytes[1..].iter().all(|byte| is_identifier_continue(*byte))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_identifier_start(byte: u8) -> bool {
|
||||||
|
byte.is_ascii_alphabetic() || byte == b'_'
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_identifier_continue(byte: u8) -> bool {
|
||||||
|
is_identifier_start(byte) || byte.is_ascii_digit()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn floor_char_boundary(source: &str, mut position: usize) -> usize {
|
||||||
|
while !source.is_char_boundary(position) {
|
||||||
|
position -= 1;
|
||||||
|
}
|
||||||
|
position
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
use decodal::{
|
||||||
|
Diagnostic, DiagnosticKind, EmptyLoader, ImportCandidate, LoadedImport, Span, Value,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
struct TestEnvironment {
|
||||||
|
files: BTreeMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
struct TestLoader {
|
||||||
|
files: BTreeMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImportLoader for TestLoader {
|
||||||
|
fn load(&mut self, _current_key: Option<&str>, specifier: &str) -> Result<LoadedImport> {
|
||||||
|
if specifier == "./post.md" {
|
||||||
|
return Ok(LoadedImport::value(
|
||||||
|
"post.md",
|
||||||
|
Value::object([
|
||||||
|
(
|
||||||
|
"frontmatter",
|
||||||
|
Value::object([
|
||||||
|
("title", Value::string("Hello")),
|
||||||
|
("draft", Value::bool(false)),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
("body", Value::string("# Hello")),
|
||||||
|
]),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let key = resolve_virtual("main.dcdl", specifier);
|
||||||
|
let Some(source) = self.files.get(&key) else {
|
||||||
|
return Err(Diagnostic::new(
|
||||||
|
DiagnosticKind::Import,
|
||||||
|
Span::default(),
|
||||||
|
"unknown import",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
Ok(LoadedImport::source(key.clone(), key, source))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn complete_import(
|
||||||
|
&mut self,
|
||||||
|
_current_key: Option<&str>,
|
||||||
|
prefix: &str,
|
||||||
|
) -> Result<Vec<ImportCandidate>> {
|
||||||
|
Ok(self
|
||||||
|
.files
|
||||||
|
.keys()
|
||||||
|
.map(|path| format!("./{path}"))
|
||||||
|
.filter(|path| path.starts_with(prefix))
|
||||||
|
.map(ImportCandidate::new)
|
||||||
|
.collect())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HostEnvironment for TestEnvironment {
|
||||||
|
type Loader = TestLoader;
|
||||||
|
|
||||||
|
fn create_loader(&self) -> Self::Loader {
|
||||||
|
TestLoader {
|
||||||
|
files: self.files.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configure_engine(&self, engine: &mut Engine<Self::Loader>) -> Result<()> {
|
||||||
|
engine.bind_global(
|
||||||
|
"App",
|
||||||
|
Value::object([(
|
||||||
|
"config",
|
||||||
|
Value::object([("enabled", Value::bool_type()), ("port", Value::int_type())]),
|
||||||
|
)]),
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn environment() -> TestEnvironment {
|
||||||
|
TestEnvironment {
|
||||||
|
files: BTreeMap::from([
|
||||||
|
(
|
||||||
|
"schemas/service.dcdl".into(),
|
||||||
|
"Service = { name = String; resources = { cpu = Int; }; };".into(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"env/production.dcdl".into(),
|
||||||
|
"capacity = { cpu = 2000; };".into(),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"資料/service.dcdl".into(),
|
||||||
|
"Service = { label = String; };".into(),
|
||||||
|
),
|
||||||
|
]),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn labels(result: CompletionResult) -> Vec<String> {
|
||||||
|
result.items.into_iter().map(|item| item.label).collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn completes_import_paths_from_the_host_loader() {
|
||||||
|
let source = "schema = import \"./sch";
|
||||||
|
let result = complete(&environment(), "main.dcdl", source, source.len(), false)
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(result.from, source.len() - 5);
|
||||||
|
assert!(labels(result).contains(&"./schemas/service.dcdl".into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn completes_nested_fields_from_imported_source() {
|
||||||
|
let source = "let schema = import \"./schemas/service.dcdl\"; in schema.Service.";
|
||||||
|
let result = complete(&environment(), "main.dcdl", source, source.len(), false)
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(labels(result), ["name", "resources"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn completes_structured_host_imports() {
|
||||||
|
let source = "let post = import \"./post.md\"; in post.frontmatter.";
|
||||||
|
let result = complete(&environment(), "main.dcdl", source, source.len(), false)
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(labels(result), ["draft", "title"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn preserves_unicode_import_specifiers() {
|
||||||
|
let source = "let schema = import \"./資料/service.dcdl\"; in schema.Service.";
|
||||||
|
let result = complete(&environment(), "main.dcdl", source, source.len(), false)
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(labels(result), ["label"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn completes_host_globals_from_the_evaluation_environment() {
|
||||||
|
let source = "App.config.";
|
||||||
|
let result = complete(&environment(), "main.dcdl", source, source.len(), false)
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(labels(result), ["enabled", "port"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn completes_language_locals_parameters_and_partial_members() {
|
||||||
|
let source = "let service = { port = 8080; }; in (value: Int) => service.po";
|
||||||
|
let result = complete(&environment(), "main.dcdl", source, source.len(), false)
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(result.from, source.len() - 2);
|
||||||
|
assert_eq!(labels(result), ["port"]);
|
||||||
|
|
||||||
|
let source = "let service = { port = 8080; }; in (value: Int) => val";
|
||||||
|
let result = complete(&environment(), "main.dcdl", source, source.len(), false)
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
let labels = labels(result);
|
||||||
|
assert!(labels.contains(&"String".into()));
|
||||||
|
assert!(labels.contains(&"Unknown".into()));
|
||||||
|
assert!(labels.contains(&"service".into()));
|
||||||
|
assert!(labels.contains(&"value".into()));
|
||||||
|
assert!(labels.contains(&"App".into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn suppresses_completions_in_strings_and_comments() {
|
||||||
|
let string = "value = \"hello world\"";
|
||||||
|
assert!(
|
||||||
|
complete(&environment(), "main.dcdl", string, string.len() - 1, false)
|
||||||
|
.unwrap()
|
||||||
|
.is_none()
|
||||||
|
);
|
||||||
|
let comment = "value = true; # hello";
|
||||||
|
assert!(
|
||||||
|
complete(&environment(), "main.dcdl", comment, comment.len(), false)
|
||||||
|
.unwrap()
|
||||||
|
.is_none()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_environment_remains_supported() {
|
||||||
|
let result = complete(&EmptyEnvironment, "main.dcdl", "Str", 3, false)
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert!(labels(result).contains(&"String".into()));
|
||||||
|
let result = complete(&EmptyEnvironment, "main.dcdl", "Unk", 3, false)
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert!(labels(result).contains(&"Unknown".into()));
|
||||||
|
}
|
||||||
|
|
||||||
|
struct EmptyEnvironment;
|
||||||
|
|
||||||
|
impl HostEnvironment for EmptyEnvironment {
|
||||||
|
type Loader = EmptyLoader;
|
||||||
|
|
||||||
|
fn create_loader(&self) -> Self::Loader {
|
||||||
|
EmptyLoader
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_virtual(current: &str, specifier: &str) -> String {
|
||||||
|
let mut parts = current.split('/').collect::<Vec<_>>();
|
||||||
|
parts.pop();
|
||||||
|
for part in specifier.split('/') {
|
||||||
|
match part {
|
||||||
|
"" | "." => {}
|
||||||
|
".." => {
|
||||||
|
parts.pop();
|
||||||
|
}
|
||||||
|
part => parts.push(part),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parts.join("/")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,204 @@
|
|||||||
|
use decodal::{Data, Diagnostic, HostEnvironment, Result};
|
||||||
|
|
||||||
|
mod completion;
|
||||||
|
|
||||||
|
pub use completion::{CompletionItem, CompletionKind, CompletionResult};
|
||||||
|
|
||||||
|
/// Semantic tooling backed by the same host environment as production.
|
||||||
|
pub struct LanguageService<E> {
|
||||||
|
environment: E,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E> LanguageService<E> {
|
||||||
|
pub fn new(environment: E) -> Self {
|
||||||
|
Self { environment }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn environment(&self) -> &E {
|
||||||
|
&self.environment
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn environment_mut(&mut self) -> &mut E {
|
||||||
|
&mut self.environment
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_environment(self) -> E {
|
||||||
|
self.environment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<E: HostEnvironment> LanguageService<E> {
|
||||||
|
/// Runs the production parse, evaluation, and materialization pipeline.
|
||||||
|
pub fn evaluate(
|
||||||
|
&self,
|
||||||
|
key: impl Into<String>,
|
||||||
|
name: impl Into<String>,
|
||||||
|
source: &str,
|
||||||
|
) -> Result<Data> {
|
||||||
|
let mut engine = self.environment.create_engine()?;
|
||||||
|
let module = engine.add_root_source(key, name, source)?;
|
||||||
|
let value = engine.eval_module(module)?;
|
||||||
|
engine.materialize(&value)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluates a document and exposes failures in an editor-friendly form.
|
||||||
|
///
|
||||||
|
/// The evaluator currently stops at the first failure, so an invalid
|
||||||
|
/// analysis contains one diagnostic. The collection leaves room for a
|
||||||
|
/// future diagnostic-accumulation pass without changing this API.
|
||||||
|
pub fn analyze(
|
||||||
|
&self,
|
||||||
|
key: impl Into<String>,
|
||||||
|
name: impl Into<String>,
|
||||||
|
source: &str,
|
||||||
|
) -> SemanticAnalysis {
|
||||||
|
match self.evaluate(key, name, source) {
|
||||||
|
Ok(data) => SemanticAnalysis {
|
||||||
|
data: Some(data),
|
||||||
|
diagnostics: Vec::new(),
|
||||||
|
},
|
||||||
|
Err(diagnostic) => SemanticAnalysis {
|
||||||
|
data: None,
|
||||||
|
diagnostics: vec![diagnostic],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Completes the document using the same host globals and import loader as
|
||||||
|
/// production evaluation.
|
||||||
|
pub fn complete(
|
||||||
|
&self,
|
||||||
|
key: impl AsRef<str>,
|
||||||
|
source: &str,
|
||||||
|
position: usize,
|
||||||
|
explicit: bool,
|
||||||
|
) -> Result<Option<CompletionResult>> {
|
||||||
|
completion::complete(&self.environment, key.as_ref(), source, position, explicit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
|
pub struct SemanticAnalysis {
|
||||||
|
pub data: Option<Data>,
|
||||||
|
pub diagnostics: Vec<Diagnostic>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SemanticAnalysis {
|
||||||
|
pub fn is_valid(&self) -> bool {
|
||||||
|
self.diagnostics.is_empty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use decodal::{
|
||||||
|
Data, Diagnostic, DiagnosticKind, Engine, ImportLoader, LoadedImport, Span, Value,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
const ROOT: &str = r#"Post & import "./post.md""#;
|
||||||
|
|
||||||
|
struct ContentEnvironment {
|
||||||
|
draft: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ContentLoader {
|
||||||
|
draft: Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImportLoader for ContentLoader {
|
||||||
|
fn load(
|
||||||
|
&mut self,
|
||||||
|
_current_key: Option<&str>,
|
||||||
|
specifier: &str,
|
||||||
|
) -> decodal::Result<LoadedImport> {
|
||||||
|
if specifier != "./post.md" {
|
||||||
|
return Err(Diagnostic::new(
|
||||||
|
DiagnosticKind::Import,
|
||||||
|
Span::default(),
|
||||||
|
"unknown content import",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
Ok(LoadedImport::value(
|
||||||
|
"content/post.md",
|
||||||
|
Value::object([
|
||||||
|
(
|
||||||
|
"frontmatter",
|
||||||
|
Value::object([
|
||||||
|
("title", Value::string("Hello")),
|
||||||
|
("draft", self.draft.clone()),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
("body", Value::string("# Hello")),
|
||||||
|
]),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HostEnvironment for ContentEnvironment {
|
||||||
|
type Loader = ContentLoader;
|
||||||
|
|
||||||
|
fn create_loader(&self) -> Self::Loader {
|
||||||
|
ContentLoader {
|
||||||
|
draft: self.draft.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configure_engine(&self, engine: &mut Engine<Self::Loader>) -> decodal::Result<()> {
|
||||||
|
engine.bind_global(
|
||||||
|
"Post",
|
||||||
|
Value::object([
|
||||||
|
(
|
||||||
|
"frontmatter",
|
||||||
|
Value::object([
|
||||||
|
("title", Value::string_type()),
|
||||||
|
("draft", Value::bool_type()),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
("body", Value::string_type()),
|
||||||
|
]),
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn evaluate_direct(environment: &ContentEnvironment) -> decodal::Result<Data> {
|
||||||
|
let mut engine = environment.create_engine()?;
|
||||||
|
let module = engine.add_root_source("main.dcdl", "main.dcdl", ROOT)?;
|
||||||
|
let value = engine.eval_module(module)?;
|
||||||
|
engine.materialize(&value)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn injected_environment_matches_direct_evaluation() {
|
||||||
|
let environment = ContentEnvironment {
|
||||||
|
draft: Value::bool(false),
|
||||||
|
};
|
||||||
|
let direct = evaluate_direct(&environment).unwrap();
|
||||||
|
let service = LanguageService::new(&environment);
|
||||||
|
let analysis = service.analyze("main.dcdl", "main.dcdl", ROOT);
|
||||||
|
|
||||||
|
assert!(analysis.is_valid());
|
||||||
|
assert_eq!(analysis.data, Some(direct));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn injected_environment_preserves_import_diagnostics() {
|
||||||
|
let service = LanguageService::new(ContentEnvironment {
|
||||||
|
draft: Value::string("maybe"),
|
||||||
|
});
|
||||||
|
let analysis = service.analyze("main.dcdl", "main.dcdl", ROOT);
|
||||||
|
|
||||||
|
assert!(!analysis.is_valid());
|
||||||
|
assert_eq!(analysis.data, None);
|
||||||
|
assert_eq!(
|
||||||
|
analysis.diagnostics[0].message,
|
||||||
|
"expected Bool, found String"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
analysis.diagnostics[0].notes,
|
||||||
|
["imported `content/post.md` at `frontmatter.draft` supplied a value of type String"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
[package]
|
||||||
|
name = "decodal-language-tools"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
readme.workspace = true
|
||||||
|
description = "Source-level language tooling for Decodal."
|
||||||
|
keywords = ["decodal", "formatter", "lsp", "language-tools"]
|
||||||
|
categories = ["development-tools", "text-processing"]
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
decodal = { version = "0.4.0", path = "../decodal-core" }
|
||||||
|
serde_json.workspace = true
|
||||||
|
wasm-bindgen.workspace = true
|
||||||
|
|
||||||
|
[package.metadata.wasm-pack.profile.release]
|
||||||
|
wasm-opt = false
|
||||||
@@ -0,0 +1,838 @@
|
|||||||
|
use std::{error::Error, fmt};
|
||||||
|
|
||||||
|
use decodal::{
|
||||||
|
Ast, BinaryOp, CompareOp, Expr, ExprId, Field, ObjectRest, Param, SourceForm, Span,
|
||||||
|
SyntaxToken, SyntaxTokenKind,
|
||||||
|
ast::{MatchArm, UnaryOp},
|
||||||
|
parse_source, tokenize_source,
|
||||||
|
};
|
||||||
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
const INDENT: usize = 4;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct FormatError {
|
||||||
|
message: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FormatError {
|
||||||
|
fn new(message: impl Into<String>) -> Self {
|
||||||
|
Self {
|
||||||
|
message: message.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn message(&self) -> &str {
|
||||||
|
&self.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for FormatError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.write_str(&self.message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for FormatError {}
|
||||||
|
|
||||||
|
/// Formats a complete Decodal source with the canonical parser and syntax tokens.
|
||||||
|
///
|
||||||
|
/// Native and WebAssembly callers execute this exact implementation. Comments
|
||||||
|
/// are recovered from the lossless token stream while expression structure and
|
||||||
|
/// precedence come from the same AST used by evaluation.
|
||||||
|
pub fn format_source(source: &str) -> Result<String, FormatError> {
|
||||||
|
let parsed = parse_source(source)
|
||||||
|
.map_err(|diagnostic| FormatError::new(format!("parse error: {}", diagnostic.message)))?;
|
||||||
|
let tokens = tokenize_source(source)
|
||||||
|
.map_err(|diagnostic| FormatError::new(format!("lex error: {}", diagnostic.message)))?;
|
||||||
|
Ok(Formatter::new(source, &parsed.ast, tokens).format(parsed.root, parsed.source_form))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[wasm_bindgen(js_name = formatSource)]
|
||||||
|
pub fn format_source_json(source: &str) -> String {
|
||||||
|
match format_source(source) {
|
||||||
|
Ok(formatted) => serde_json::json!({ "ok": true, "source": formatted }).to_string(),
|
||||||
|
Err(error) => serde_json::json!({ "ok": false, "error": error.message() }).to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Formatter<'a> {
|
||||||
|
source: &'a str,
|
||||||
|
ast: &'a Ast,
|
||||||
|
tokens: Vec<SyntaxToken>,
|
||||||
|
comments: Vec<Span>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> Formatter<'a> {
|
||||||
|
fn new(source: &'a str, ast: &'a Ast, tokens: Vec<SyntaxToken>) -> Self {
|
||||||
|
let comments = tokens
|
||||||
|
.iter()
|
||||||
|
.filter(|token| token.kind == SyntaxTokenKind::Comment)
|
||||||
|
.map(|token| token.span)
|
||||||
|
.collect();
|
||||||
|
Self {
|
||||||
|
source,
|
||||||
|
ast,
|
||||||
|
tokens,
|
||||||
|
comments,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format(&self, root: ExprId, source_form: SourceForm) -> String {
|
||||||
|
let mut out = String::new();
|
||||||
|
if source_form == SourceForm::Fields {
|
||||||
|
let Expr::Object { fields, .. } = &self.ast.get(root).expr else {
|
||||||
|
unreachable!("field-form sources parse to an object")
|
||||||
|
};
|
||||||
|
self.write_field_list(&mut out, fields, 0, self.source.len(), 0);
|
||||||
|
} else {
|
||||||
|
let span = self.ast.span(root);
|
||||||
|
self.write_between(&mut out, 0, span.start as usize, None, 0);
|
||||||
|
self.write_expr(&mut out, root, 0, 0);
|
||||||
|
self.write_between(
|
||||||
|
&mut out,
|
||||||
|
span.end as usize,
|
||||||
|
self.source.len(),
|
||||||
|
Some(span.end as usize),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
trim_trailing_whitespace(&mut out);
|
||||||
|
out.push('\n');
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_field_list(
|
||||||
|
&self,
|
||||||
|
out: &mut String,
|
||||||
|
fields: &[Field],
|
||||||
|
start: usize,
|
||||||
|
end: usize,
|
||||||
|
indent: usize,
|
||||||
|
) {
|
||||||
|
let mut cursor = start;
|
||||||
|
let mut previous_end = None;
|
||||||
|
for field in fields {
|
||||||
|
let field_start = field.span.start as usize;
|
||||||
|
self.write_between(out, cursor, field_start, previous_end, indent);
|
||||||
|
write_indent(out, indent);
|
||||||
|
self.write_field(out, field, indent);
|
||||||
|
out.push(';');
|
||||||
|
cursor = field.span.end as usize;
|
||||||
|
previous_end = Some(cursor);
|
||||||
|
}
|
||||||
|
self.write_between(out, cursor, end, previous_end, indent);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_field(&self, out: &mut String, field: &Field, indent: usize) {
|
||||||
|
let value_start = self.ast.span(field.value).start as usize;
|
||||||
|
if self.has_comment_between(field.span.start as usize, value_start) {
|
||||||
|
out.push_str(self.raw(field.span));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (index, part) in field.path.iter().enumerate() {
|
||||||
|
if index > 0 {
|
||||||
|
out.push('.');
|
||||||
|
}
|
||||||
|
out.push_str(part);
|
||||||
|
}
|
||||||
|
out.push_str(" = ");
|
||||||
|
self.write_expr(out, field.value, indent, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_expr(&self, out: &mut String, id: ExprId, indent: usize, parent_prec: u8) {
|
||||||
|
let node = self.ast.get(id);
|
||||||
|
if self.has_comment(node.span)
|
||||||
|
&& !matches!(
|
||||||
|
node.expr,
|
||||||
|
Expr::Object { .. } | Expr::Array(_) | Expr::Let { .. } | Expr::Match { .. }
|
||||||
|
)
|
||||||
|
{
|
||||||
|
out.push_str(self.raw(node.span));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let precedence = self.precedence(id);
|
||||||
|
let parenthesize = precedence < parent_prec;
|
||||||
|
if parenthesize {
|
||||||
|
out.push('(');
|
||||||
|
}
|
||||||
|
|
||||||
|
match &node.expr {
|
||||||
|
Expr::Literal(_) | Expr::Ident(_) | Expr::RegexConstraint(_) | Expr::Wildcard => {
|
||||||
|
out.push_str(self.raw(node.span));
|
||||||
|
}
|
||||||
|
Expr::Object { fields, rest } => {
|
||||||
|
self.write_object(out, node.span, fields, rest.as_ref(), indent)
|
||||||
|
}
|
||||||
|
Expr::Array(items) => self.write_array(out, node.span, items, indent),
|
||||||
|
Expr::ArrayConstraint { item } => {
|
||||||
|
out.push_str("[...");
|
||||||
|
self.write_expr(out, *item, indent, 0);
|
||||||
|
out.push(']');
|
||||||
|
}
|
||||||
|
Expr::MapConstraint { value } => {
|
||||||
|
out.push_str("{...");
|
||||||
|
self.write_expr(out, *value, indent, 0);
|
||||||
|
out.push('}');
|
||||||
|
}
|
||||||
|
Expr::Let { bindings, body } => self.write_let(out, node.span, bindings, *body, indent),
|
||||||
|
Expr::Import(_) => {
|
||||||
|
out.push_str("import ");
|
||||||
|
let raw = self.raw(node.span);
|
||||||
|
out.push_str(raw.strip_prefix("import").unwrap_or(raw).trim());
|
||||||
|
}
|
||||||
|
Expr::Path { base, field } => {
|
||||||
|
self.write_expr(out, *base, indent, precedence);
|
||||||
|
out.push('.');
|
||||||
|
out.push_str(field);
|
||||||
|
}
|
||||||
|
Expr::Call { callee, args } => {
|
||||||
|
self.write_expr(out, *callee, indent, precedence);
|
||||||
|
out.push('(');
|
||||||
|
for (index, argument) in args.iter().enumerate() {
|
||||||
|
if index > 0 {
|
||||||
|
out.push_str(", ");
|
||||||
|
}
|
||||||
|
self.write_expr(out, *argument, indent, 0);
|
||||||
|
}
|
||||||
|
out.push(')');
|
||||||
|
}
|
||||||
|
Expr::Function { params, body } => self.write_function(out, params, *body, indent),
|
||||||
|
Expr::Parenthesized { expr } => {
|
||||||
|
out.push('(');
|
||||||
|
self.write_expr(out, *expr, indent, 0);
|
||||||
|
out.push(')');
|
||||||
|
}
|
||||||
|
Expr::Match { scrutinee, arms } => {
|
||||||
|
self.write_match(out, node.span, *scrutinee, arms, indent)
|
||||||
|
}
|
||||||
|
Expr::Unary { op, expr } => {
|
||||||
|
out.push_str(match op {
|
||||||
|
UnaryOp::Neg => "-",
|
||||||
|
UnaryOp::Not => "!",
|
||||||
|
});
|
||||||
|
self.write_expr(out, *expr, indent, precedence);
|
||||||
|
}
|
||||||
|
Expr::Binary { op, lhs, rhs } => {
|
||||||
|
self.write_expr(out, *lhs, indent, precedence);
|
||||||
|
out.push(' ');
|
||||||
|
out.push_str(binary_operator(*op));
|
||||||
|
out.push(' ');
|
||||||
|
self.write_expr(out, *rhs, indent, precedence + 1);
|
||||||
|
}
|
||||||
|
Expr::Default { base, fallback } => {
|
||||||
|
self.write_expr(out, *base, indent, precedence);
|
||||||
|
out.push_str(" default ");
|
||||||
|
self.write_expr(out, *fallback, indent, precedence + 1);
|
||||||
|
}
|
||||||
|
Expr::As { narrower, wider } => {
|
||||||
|
self.write_expr(out, *narrower, indent, precedence);
|
||||||
|
out.push_str(" as ");
|
||||||
|
self.write_expr(out, *wider, indent, precedence + 1);
|
||||||
|
}
|
||||||
|
Expr::CompareConstraint { op, value } => {
|
||||||
|
out.push_str(compare_operator(*op));
|
||||||
|
out.push(' ');
|
||||||
|
self.write_expr(out, *value, indent, precedence);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if parenthesize {
|
||||||
|
out.push(')');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_object(
|
||||||
|
&self,
|
||||||
|
out: &mut String,
|
||||||
|
span: Span,
|
||||||
|
fields: &[Field],
|
||||||
|
rest: Option<&ObjectRest>,
|
||||||
|
indent: usize,
|
||||||
|
) {
|
||||||
|
let (start, end) =
|
||||||
|
self.delimited_range(span, SyntaxTokenKind::LBrace, SyntaxTokenKind::RBrace);
|
||||||
|
if fields.is_empty() && rest.is_none() && !self.has_comment_between(start, end) {
|
||||||
|
out.push_str("{}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
out.push_str("{\n");
|
||||||
|
let field_end = rest.map_or(end, |rest| rest.span.start as usize);
|
||||||
|
self.write_field_list(out, fields, start, field_end, indent + INDENT);
|
||||||
|
if let Some(rest) = rest {
|
||||||
|
write_indent(out, indent + INDENT);
|
||||||
|
out.push_str("...");
|
||||||
|
let value_start = self.ast.span(rest.value).start as usize;
|
||||||
|
let ellipsis = self
|
||||||
|
.tokens_between(rest.span.start as usize, value_start)
|
||||||
|
.find(|token| token.kind == SyntaxTokenKind::Ellipsis)
|
||||||
|
.expect("parsed object rest constraints contain `...`");
|
||||||
|
if self.has_comment_between(ellipsis.span.end as usize, value_start) {
|
||||||
|
self.write_between(
|
||||||
|
out,
|
||||||
|
ellipsis.span.end as usize,
|
||||||
|
value_start,
|
||||||
|
Some(ellipsis.span.end as usize),
|
||||||
|
indent + INDENT,
|
||||||
|
);
|
||||||
|
write_indent(out, indent + INDENT);
|
||||||
|
}
|
||||||
|
self.write_expr(out, rest.value, indent + INDENT, 0);
|
||||||
|
self.write_between(
|
||||||
|
out,
|
||||||
|
rest.span.end as usize,
|
||||||
|
end,
|
||||||
|
Some(rest.span.end as usize),
|
||||||
|
indent + INDENT,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
write_indent(out, indent);
|
||||||
|
out.push('}');
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_array(&self, out: &mut String, span: Span, items: &[ExprId], indent: usize) {
|
||||||
|
let (start, end) =
|
||||||
|
self.delimited_range(span, SyntaxTokenKind::LBracket, SyntaxTokenKind::RBracket);
|
||||||
|
if items.is_empty() && !self.has_comment_between(start, end) {
|
||||||
|
out.push_str("[]");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let inline = !self.has_comment_between(start, end)
|
||||||
|
&& items.iter().all(|item| self.is_inline_expr(*item));
|
||||||
|
if inline {
|
||||||
|
out.push('[');
|
||||||
|
for (index, item) in items.iter().enumerate() {
|
||||||
|
if index > 0 {
|
||||||
|
out.push_str(", ");
|
||||||
|
}
|
||||||
|
self.write_expr(out, *item, indent, 0);
|
||||||
|
}
|
||||||
|
out.push(']');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
out.push_str("[\n");
|
||||||
|
let mut cursor = start;
|
||||||
|
let mut previous_end = None;
|
||||||
|
for item in items {
|
||||||
|
let item_span = self.ast.span(*item);
|
||||||
|
self.write_between(
|
||||||
|
out,
|
||||||
|
cursor,
|
||||||
|
item_span.start as usize,
|
||||||
|
previous_end,
|
||||||
|
indent + INDENT,
|
||||||
|
);
|
||||||
|
write_indent(out, indent + INDENT);
|
||||||
|
self.write_expr(out, *item, indent + INDENT, 0);
|
||||||
|
out.push(',');
|
||||||
|
cursor = item_span.end as usize;
|
||||||
|
previous_end = Some(cursor);
|
||||||
|
}
|
||||||
|
self.write_between(out, cursor, end, previous_end, indent + INDENT);
|
||||||
|
write_indent(out, indent);
|
||||||
|
out.push(']');
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_let(
|
||||||
|
&self,
|
||||||
|
out: &mut String,
|
||||||
|
span: Span,
|
||||||
|
bindings: &[Field],
|
||||||
|
body: ExprId,
|
||||||
|
indent: usize,
|
||||||
|
) {
|
||||||
|
let body_span = self.ast.span(body);
|
||||||
|
let in_token = self
|
||||||
|
.tokens_between(span.start as usize, body_span.start as usize)
|
||||||
|
.rev()
|
||||||
|
.find(|token| token.kind == SyntaxTokenKind::In)
|
||||||
|
.expect("parsed let expressions contain `in`");
|
||||||
|
let let_token = self
|
||||||
|
.tokens_between(span.start as usize, span.end as usize)
|
||||||
|
.find(|token| token.kind == SyntaxTokenKind::Let)
|
||||||
|
.expect("parsed let expressions contain `let`");
|
||||||
|
|
||||||
|
out.push_str("let\n");
|
||||||
|
self.write_field_list(
|
||||||
|
out,
|
||||||
|
bindings,
|
||||||
|
let_token.span.end as usize,
|
||||||
|
in_token.span.start as usize,
|
||||||
|
indent + INDENT,
|
||||||
|
);
|
||||||
|
write_indent(out, indent);
|
||||||
|
out.push_str("in");
|
||||||
|
self.write_between(
|
||||||
|
out,
|
||||||
|
in_token.span.end as usize,
|
||||||
|
body_span.start as usize,
|
||||||
|
Some(in_token.span.end as usize),
|
||||||
|
indent + INDENT,
|
||||||
|
);
|
||||||
|
write_indent(out, indent + INDENT);
|
||||||
|
self.write_expr(out, body, indent + INDENT, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_function(&self, out: &mut String, params: &[Param], body: ExprId, indent: usize) {
|
||||||
|
out.push('(');
|
||||||
|
for (index, parameter) in params.iter().enumerate() {
|
||||||
|
if index > 0 {
|
||||||
|
out.push_str(", ");
|
||||||
|
}
|
||||||
|
out.push_str(¶meter.name);
|
||||||
|
if let Some(constraint) = parameter.constraint {
|
||||||
|
out.push_str(": ");
|
||||||
|
self.write_expr(out, constraint, indent, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out.push_str(") =>");
|
||||||
|
if self.is_inline_expr(body) {
|
||||||
|
out.push(' ');
|
||||||
|
self.write_expr(out, body, indent, 0);
|
||||||
|
} else {
|
||||||
|
out.push('\n');
|
||||||
|
write_indent(out, indent + INDENT);
|
||||||
|
self.write_expr(out, body, indent + INDENT, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_match(
|
||||||
|
&self,
|
||||||
|
out: &mut String,
|
||||||
|
span: Span,
|
||||||
|
scrutinee: ExprId,
|
||||||
|
arms: &[MatchArm],
|
||||||
|
indent: usize,
|
||||||
|
) {
|
||||||
|
out.push_str("match ");
|
||||||
|
self.write_expr(out, scrutinee, indent, 0);
|
||||||
|
let scrutinee_end = self.ast.span(scrutinee).end as usize;
|
||||||
|
let open = self
|
||||||
|
.tokens_between(scrutinee_end, span.end as usize)
|
||||||
|
.find(|token| token.kind == SyntaxTokenKind::LBrace)
|
||||||
|
.expect("parsed match expressions contain `{`");
|
||||||
|
let close = self
|
||||||
|
.tokens_between(open.span.end as usize, span.end as usize)
|
||||||
|
.rev()
|
||||||
|
.find(|token| token.kind == SyntaxTokenKind::RBrace)
|
||||||
|
.expect("parsed match expressions contain `}`");
|
||||||
|
let start = open.span.end as usize;
|
||||||
|
let end = close.span.start as usize;
|
||||||
|
if self.has_comment_between(scrutinee_end, open.span.start as usize) {
|
||||||
|
self.write_between(
|
||||||
|
out,
|
||||||
|
scrutinee_end,
|
||||||
|
open.span.start as usize,
|
||||||
|
Some(scrutinee_end),
|
||||||
|
indent,
|
||||||
|
);
|
||||||
|
write_indent(out, indent);
|
||||||
|
out.push('{');
|
||||||
|
} else {
|
||||||
|
out.push_str(" {");
|
||||||
|
}
|
||||||
|
if arms.is_empty() && !self.has_comment_between(start, end) {
|
||||||
|
out.push('}');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
out.push('\n');
|
||||||
|
let mut cursor = start;
|
||||||
|
let mut previous_end = None;
|
||||||
|
for arm in arms {
|
||||||
|
self.write_between(
|
||||||
|
out,
|
||||||
|
cursor,
|
||||||
|
arm.span.start as usize,
|
||||||
|
previous_end,
|
||||||
|
indent + INDENT,
|
||||||
|
);
|
||||||
|
write_indent(out, indent + INDENT);
|
||||||
|
if self.has_comment_between(
|
||||||
|
self.ast.span(arm.pattern).end as usize,
|
||||||
|
self.ast.span(arm.body).start as usize,
|
||||||
|
) {
|
||||||
|
out.push_str(self.raw(arm.span));
|
||||||
|
} else {
|
||||||
|
self.write_expr(out, arm.pattern, indent + INDENT, 0);
|
||||||
|
out.push_str(": ");
|
||||||
|
self.write_expr(out, arm.body, indent + INDENT, 0);
|
||||||
|
}
|
||||||
|
out.push(';');
|
||||||
|
cursor = arm.span.end as usize;
|
||||||
|
previous_end = Some(cursor);
|
||||||
|
}
|
||||||
|
self.write_between(out, cursor, end, previous_end, indent + INDENT);
|
||||||
|
write_indent(out, indent);
|
||||||
|
out.push('}');
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_between(
|
||||||
|
&self,
|
||||||
|
out: &mut String,
|
||||||
|
start: usize,
|
||||||
|
end: usize,
|
||||||
|
previous_end: Option<usize>,
|
||||||
|
indent: usize,
|
||||||
|
) {
|
||||||
|
let mut cursor = start.min(end);
|
||||||
|
for comment in self.comments_between(start, end) {
|
||||||
|
let comment_start = comment.start as usize;
|
||||||
|
let trailing = previous_end.is_some_and(|previous| {
|
||||||
|
!out.ends_with('\n') && self.same_line(previous, comment_start)
|
||||||
|
});
|
||||||
|
if trailing {
|
||||||
|
out.push(' ');
|
||||||
|
out.push_str(self.raw(*comment));
|
||||||
|
out.push('\n');
|
||||||
|
} else {
|
||||||
|
ensure_line_break(out);
|
||||||
|
if self.has_blank_line(cursor, comment_start) {
|
||||||
|
ensure_blank_line(out);
|
||||||
|
}
|
||||||
|
write_indent(out, indent);
|
||||||
|
out.push_str(self.raw(*comment));
|
||||||
|
out.push('\n');
|
||||||
|
}
|
||||||
|
cursor = comment.end as usize;
|
||||||
|
}
|
||||||
|
if previous_end.is_some() {
|
||||||
|
ensure_line_break(out);
|
||||||
|
}
|
||||||
|
if self.has_blank_line(cursor, end) {
|
||||||
|
ensure_blank_line(out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn precedence(&self, id: ExprId) -> u8 {
|
||||||
|
match &self.ast.get(id).expr {
|
||||||
|
Expr::As { .. } => 0,
|
||||||
|
Expr::Default { .. } => 1,
|
||||||
|
Expr::Binary { op, .. } => match op {
|
||||||
|
BinaryOp::Patch => 3,
|
||||||
|
BinaryOp::And => 5,
|
||||||
|
BinaryOp::LogicalOr => 7,
|
||||||
|
BinaryOp::LogicalAnd => 9,
|
||||||
|
BinaryOp::Equal
|
||||||
|
| BinaryOp::NotEqual
|
||||||
|
| BinaryOp::Greater
|
||||||
|
| BinaryOp::GreaterEqual
|
||||||
|
| BinaryOp::Less
|
||||||
|
| BinaryOp::LessEqual => 11,
|
||||||
|
BinaryOp::Concat => 12,
|
||||||
|
BinaryOp::Add | BinaryOp::Sub => 13,
|
||||||
|
BinaryOp::Mul | BinaryOp::Div => 15,
|
||||||
|
},
|
||||||
|
Expr::Unary { .. } | Expr::CompareConstraint { .. } => 17,
|
||||||
|
Expr::Call { .. } | Expr::Path { .. } => 19,
|
||||||
|
Expr::Parenthesized { .. } => 20,
|
||||||
|
_ => 20,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn is_inline_expr(&self, id: ExprId) -> bool {
|
||||||
|
let node = self.ast.get(id);
|
||||||
|
if self.has_comment(node.span) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
match &node.expr {
|
||||||
|
Expr::Literal(_)
|
||||||
|
| Expr::Ident(_)
|
||||||
|
| Expr::RegexConstraint(_)
|
||||||
|
| Expr::Wildcard
|
||||||
|
| Expr::Import(_)
|
||||||
|
| Expr::CompareConstraint { .. } => true,
|
||||||
|
Expr::Path { base, .. } => self.is_inline_expr(*base),
|
||||||
|
Expr::Call { callee, args } => {
|
||||||
|
self.is_inline_expr(*callee)
|
||||||
|
&& args.iter().all(|argument| self.is_inline_expr(*argument))
|
||||||
|
}
|
||||||
|
Expr::Unary { expr, .. } => self.is_inline_expr(*expr),
|
||||||
|
Expr::Binary { lhs, rhs, .. } => self.is_inline_expr(*lhs) && self.is_inline_expr(*rhs),
|
||||||
|
Expr::Default { base, fallback } => {
|
||||||
|
self.is_inline_expr(*base) && self.is_inline_expr(*fallback)
|
||||||
|
}
|
||||||
|
Expr::Array(items) => items.iter().all(|item| self.is_inline_expr(*item)),
|
||||||
|
Expr::ArrayConstraint { item } => self.is_inline_expr(*item),
|
||||||
|
Expr::MapConstraint { value } => self.is_inline_expr(*value),
|
||||||
|
Expr::Parenthesized { expr } => self.is_inline_expr(*expr),
|
||||||
|
Expr::As { narrower, wider } => {
|
||||||
|
self.is_inline_expr(*narrower) && self.is_inline_expr(*wider)
|
||||||
|
}
|
||||||
|
Expr::Object { .. } | Expr::Let { .. } | Expr::Function { .. } | Expr::Match { .. } => {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn delimited_range(
|
||||||
|
&self,
|
||||||
|
span: Span,
|
||||||
|
open_kind: SyntaxTokenKind,
|
||||||
|
close_kind: SyntaxTokenKind,
|
||||||
|
) -> (usize, usize) {
|
||||||
|
let open = self
|
||||||
|
.tokens_between(span.start as usize, span.end as usize)
|
||||||
|
.find(|token| same_kind(&token.kind, &open_kind))
|
||||||
|
.expect("parsed delimited expressions contain an opening token");
|
||||||
|
let close = self
|
||||||
|
.tokens_between(open.span.end as usize, span.end as usize)
|
||||||
|
.rev()
|
||||||
|
.find(|token| same_kind(&token.kind, &close_kind))
|
||||||
|
.expect("parsed delimited expressions contain a closing token");
|
||||||
|
(open.span.end as usize, close.span.start as usize)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tokens_between(
|
||||||
|
&self,
|
||||||
|
start: usize,
|
||||||
|
end: usize,
|
||||||
|
) -> impl DoubleEndedIterator<Item = &SyntaxToken> {
|
||||||
|
self.tokens.iter().filter(move |token| {
|
||||||
|
token.span.start as usize >= start && token.span.end as usize <= end
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn comments_between(&self, start: usize, end: usize) -> impl Iterator<Item = &Span> {
|
||||||
|
self.comments
|
||||||
|
.iter()
|
||||||
|
.filter(move |comment| comment.start as usize >= start && comment.end as usize <= end)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_comment(&self, span: Span) -> bool {
|
||||||
|
self.has_comment_between(span.start as usize, span.end as usize)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_comment_between(&self, start: usize, end: usize) -> bool {
|
||||||
|
self.comments_between(start, end).next().is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn same_line(&self, start: usize, end: usize) -> bool {
|
||||||
|
!self.slice(start, end).contains('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
fn has_blank_line(&self, start: usize, end: usize) -> bool {
|
||||||
|
self.slice(start, end)
|
||||||
|
.bytes()
|
||||||
|
.filter(|byte| *byte == b'\n')
|
||||||
|
.count()
|
||||||
|
>= 2
|
||||||
|
}
|
||||||
|
|
||||||
|
fn raw(&self, span: Span) -> &'a str {
|
||||||
|
self.slice(span.start as usize, span.end as usize).trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn slice(&self, start: usize, end: usize) -> &'a str {
|
||||||
|
&self.source[start.min(self.source.len())..end.min(self.source.len())]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn binary_operator(operator: BinaryOp) -> &'static str {
|
||||||
|
match operator {
|
||||||
|
BinaryOp::Add => "+",
|
||||||
|
BinaryOp::Sub => "-",
|
||||||
|
BinaryOp::Mul => "*",
|
||||||
|
BinaryOp::Div => "/",
|
||||||
|
BinaryOp::Concat => "++",
|
||||||
|
BinaryOp::Equal => "==",
|
||||||
|
BinaryOp::NotEqual => "!=",
|
||||||
|
BinaryOp::Greater => ">",
|
||||||
|
BinaryOp::GreaterEqual => ">=",
|
||||||
|
BinaryOp::Less => "<",
|
||||||
|
BinaryOp::LessEqual => "<=",
|
||||||
|
BinaryOp::LogicalAnd => "&&",
|
||||||
|
BinaryOp::LogicalOr => "||",
|
||||||
|
BinaryOp::And => "&",
|
||||||
|
BinaryOp::Patch => "//",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compare_operator(operator: CompareOp) -> &'static str {
|
||||||
|
match operator {
|
||||||
|
CompareOp::Gt => ">",
|
||||||
|
CompareOp::Gte => ">=",
|
||||||
|
CompareOp::Lt => "<",
|
||||||
|
CompareOp::Lte => "<=",
|
||||||
|
CompareOp::Eq => "==",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn same_kind(left: &SyntaxTokenKind, right: &SyntaxTokenKind) -> bool {
|
||||||
|
core::mem::discriminant(left) == core::mem::discriminant(right)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_indent(out: &mut String, indent: usize) {
|
||||||
|
out.extend(core::iter::repeat_n(' ', indent));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_line_break(out: &mut String) {
|
||||||
|
if !out.is_empty() && !out.ends_with('\n') {
|
||||||
|
out.push('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ensure_blank_line(out: &mut String) {
|
||||||
|
if !out.is_empty() && !out.ends_with("\n\n") {
|
||||||
|
ensure_line_break(out);
|
||||||
|
out.push('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn trim_trailing_whitespace(out: &mut String) {
|
||||||
|
while out.ends_with(char::is_whitespace) {
|
||||||
|
out.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn formats_fields_and_preserves_comments() {
|
||||||
|
let source =
|
||||||
|
"Server={\n# host\nhost=String default \"localhost\"; # trailing\n\nport=Int&>0;\n};";
|
||||||
|
let formatted = format_source(source).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
formatted,
|
||||||
|
"Server = {\n # host\n host = String default \"localhost\"; # trailing\n\n port = Int & > 0;\n};\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn formats_arrays_and_match_comments() {
|
||||||
|
let source = "result=match x{# c\n_: [1,{a=2;}];};";
|
||||||
|
let formatted = format_source(source).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
formatted,
|
||||||
|
"result = match x {\n # c\n _: [\n 1,\n {\n a = 2;\n },\n ];\n};\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn formats_array_constraints() {
|
||||||
|
let source = "tags=[...String];ports=[...(Int&>=1)];";
|
||||||
|
let formatted = format_source(source).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
formatted,
|
||||||
|
"tags = [...String];\nports = [...(Int & >= 1)];\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn formats_map_constraints_and_range_ascription() {
|
||||||
|
let source =
|
||||||
|
"config={service={port=8080;};}as{service={port=Int;};};labels={a=1;b=2;}as{...Int};";
|
||||||
|
let formatted = format_source(source).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
formatted,
|
||||||
|
"config = {\n service = {\n port = 8080;\n };\n} as {\n service = {\n port = Int;\n };\n};\nlabels = {\n a = 1;\n b = 2;\n} as {...Int};\n"
|
||||||
|
);
|
||||||
|
assert_eq!(format_source(&formatted).unwrap(), formatted);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn formats_let_functions_and_precedence_from_the_canonical_ast() {
|
||||||
|
let source = "value=let x=1+2*3;in(a:Int)=>{result=(x+a)*2;};";
|
||||||
|
let formatted = format_source(source).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
formatted,
|
||||||
|
"value = let\n x = 1 + 2 * 3;\nin\n (a: Int) =>\n {\n result = (x + a) * 2;\n };\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn formats_unknown_and_object_rest_constraints() {
|
||||||
|
let source =
|
||||||
|
"value={hoge=Int;fuga=String;# remaining fields\n...# unconstrained\nUnknown};";
|
||||||
|
let formatted = format_source(source).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
formatted,
|
||||||
|
"value = {\n hoge = Int;\n fuga = String; # remaining fields\n ... # unconstrained\n Unknown\n};\n"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn preserves_regex_and_escaped_strings() {
|
||||||
|
let source = r#"value={pattern=/^api\/.+$/;text="a\n\"b";};"#;
|
||||||
|
let formatted = format_source(source).unwrap();
|
||||||
|
assert!(formatted.contains(r#"pattern = /^api\/.+$/;"#));
|
||||||
|
assert!(formatted.contains(r#"text = "a\n\"b";"#));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn preserves_comments_inside_grouping_and_between_structural_tokens() {
|
||||||
|
let source = "value = (# grouped\n 1 + 2); result = match x # before brace\n { _ # before body\n : value; };";
|
||||||
|
let formatted = format_source(source).unwrap();
|
||||||
|
assert!(formatted.contains("(# grouped\n 1 + 2)"));
|
||||||
|
assert!(formatted.contains("match x # before brace\n{"));
|
||||||
|
assert!(formatted.contains("_ # before body\n : value;"));
|
||||||
|
assert_eq!(format_source(&formatted).unwrap(), formatted);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wasm_export_returns_the_same_formatter_result() {
|
||||||
|
let source = "value={a=1;};";
|
||||||
|
let expected = format_source(source).unwrap();
|
||||||
|
let output: serde_json::Value = serde_json::from_str(&format_source_json(source)).unwrap();
|
||||||
|
assert_eq!(output["source"], expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn formats_all_repository_examples_idempotently() {
|
||||||
|
let examples = [
|
||||||
|
(
|
||||||
|
"advanced/main",
|
||||||
|
include_str!("../../../examples/advanced/main.dcdl"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"advanced/profiles",
|
||||||
|
include_str!("../../../examples/advanced/profiles.dcdl"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"advanced/schema",
|
||||||
|
include_str!("../../../examples/advanced/schema.dcdl"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"arithmetic",
|
||||||
|
include_str!("../../../examples/arithmetic.dcdl"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"array-concat",
|
||||||
|
include_str!("../../../examples/array-concat.dcdl"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"array-constraint",
|
||||||
|
include_str!("../../../examples/array-constraint.dcdl"),
|
||||||
|
),
|
||||||
|
("basic", include_str!("../../../examples/basic.dcdl")),
|
||||||
|
(
|
||||||
|
"import/main",
|
||||||
|
include_str!("../../../examples/import/main.dcdl"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"import/schema",
|
||||||
|
include_str!("../../../examples/import/schema.dcdl"),
|
||||||
|
),
|
||||||
|
("logical", include_str!("../../../examples/logical.dcdl")),
|
||||||
|
(
|
||||||
|
"map-ascription",
|
||||||
|
include_str!("../../../examples/map-ascription.dcdl"),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"regex/main",
|
||||||
|
include_str!("../../../examples/regex/main.dcdl"),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
for (name, source) in examples {
|
||||||
|
let once = format_source(source).unwrap_or_else(|error| panic!("{name}: {error}"));
|
||||||
|
let twice = format_source(&once).unwrap_or_else(|error| {
|
||||||
|
panic!("{name} reformatted to invalid source: {error}\n{once}")
|
||||||
|
});
|
||||||
|
assert_eq!(once, twice, "{name}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
[package]
|
||||||
|
name = "decodal-lsp"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
repository.workspace = true
|
||||||
|
readme.workspace = true
|
||||||
|
description = "Language Server Protocol implementation for Decodal."
|
||||||
|
keywords = ["decodal", "lsp", "language-server", "editor"]
|
||||||
|
categories = ["development-tools", "text-editors"]
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
decodal = { version = "0.4.0", path = "../decodal-core" }
|
||||||
|
decodal-language-service = { version = "0.4.0", path = "../decodal-language-service" }
|
||||||
|
decodal-language-tools = { version = "0.4.0", path = "../decodal-language-tools" }
|
||||||
|
lsp-server = "0.10"
|
||||||
|
lsp-types = "0.97"
|
||||||
|
serde_json.workspace = true
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
|||||||
|
use std::process::ExitCode;
|
||||||
|
|
||||||
|
fn main() -> ExitCode {
|
||||||
|
match decodal_lsp::run_stdio(|_| Ok(decodal_lsp::FileSystemEnvironment::default())) {
|
||||||
|
Ok(()) => ExitCode::SUCCESS,
|
||||||
|
Err(error) => {
|
||||||
|
eprintln!("decodal-lsp failed: {error}");
|
||||||
|
ExitCode::FAILURE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,7 +6,7 @@ rust-version.workspace = true
|
|||||||
license.workspace = true
|
license.workspace = true
|
||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
readme.workspace = true
|
readme.workspace = true
|
||||||
description = "WebAssembly wrapper for evaluating Decodal in browser playgrounds."
|
description = "Host-configurable Decodal evaluator and language service for JavaScript runtimes."
|
||||||
keywords = ["decodal", "wasm", "dsl", "config"]
|
keywords = ["decodal", "wasm", "dsl", "config"]
|
||||||
categories = ["wasm", "config"]
|
categories = ["wasm", "config"]
|
||||||
publish = false
|
publish = false
|
||||||
@@ -15,9 +15,14 @@ publish = false
|
|||||||
crate-type = ["cdylib", "rlib"]
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
decodal = { version = "0.1.2", path = "../decodal-core" }
|
decodal = { version = "0.4.0", path = "../decodal-core" }
|
||||||
|
decodal-language-service = { version = "0.4.0", path = "../decodal-language-service" }
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
wasm-bindgen.workspace = true
|
wasm-bindgen.workspace = true
|
||||||
|
|
||||||
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
|
js-sys = "0.3"
|
||||||
|
serde-wasm-bindgen = "0.6"
|
||||||
|
|
||||||
[package.metadata.wasm-pack.profile.release]
|
[package.metadata.wasm-pack.profile.release]
|
||||||
wasm-opt = false
|
wasm-opt = false
|
||||||
|
|||||||
+144
-137
@@ -1,25 +1,67 @@
|
|||||||
use std::collections::BTreeMap;
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
use decodal::HostEnvironment;
|
||||||
use decodal::{
|
use decodal::{Data, EmptyLoader, Engine, SourceId, format_diagnostic_with};
|
||||||
Data, Diagnostic, DiagnosticKind, EmptyLoader, Engine, LoadedSource, SourceId, SourceLoader,
|
#[cfg(target_arch = "wasm32")]
|
||||||
Span, format_diagnostic_with,
|
use decodal_language_service::{CompletionKind, LanguageService};
|
||||||
};
|
|
||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
|
|
||||||
|
#[cfg(any(target_arch = "wasm32", test))]
|
||||||
|
mod value;
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
mod web_environment;
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
use web_environment::JsEnvironment;
|
||||||
|
|
||||||
|
/// Evaluates one standalone Decodal source with no host globals or imports.
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub fn evaluate(source: &str) -> String {
|
pub fn evaluate(source: &str) -> String {
|
||||||
encode_result(evaluate_inner(source))
|
encode_result(evaluate_inner(source))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen(js_name = evaluateProject)]
|
/// A browser-facing language service configured entirely by its JavaScript host.
|
||||||
pub fn evaluate_project(entry: &str, files_json: &str) -> String {
|
///
|
||||||
encode_result(evaluate_project_inner(entry, files_json))
|
/// The host owns globals, import loading, and import completion. This keeps
|
||||||
|
/// filesystem, network, and virtual-project policy outside the WASM package.
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
#[wasm_bindgen(js_name = DecodalLanguageService)]
|
||||||
|
pub struct WebLanguageService {
|
||||||
|
environment: JsEnvironment,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
#[wasm_bindgen(js_class = DecodalLanguageService)]
|
||||||
|
impl WebLanguageService {
|
||||||
|
#[wasm_bindgen(constructor)]
|
||||||
|
pub fn new(options: JsValue) -> Result<WebLanguageService, JsValue> {
|
||||||
|
Ok(Self {
|
||||||
|
environment: JsEnvironment::from_options(options)?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Evaluates a source using the injected globals and import loader.
|
||||||
|
pub fn evaluate(&self, key: &str, name: &str, source: &str) -> String {
|
||||||
|
encode_result(evaluate_with_environment(
|
||||||
|
&self.environment,
|
||||||
|
key,
|
||||||
|
name,
|
||||||
|
source,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Completes a source using the same injected environment as evaluation.
|
||||||
|
///
|
||||||
|
/// Positions and returned ranges are UTF-16 offsets, matching browser
|
||||||
|
/// editors and the Language Server Protocol.
|
||||||
|
pub fn complete(&self, key: &str, source: &str, position: usize, explicit: bool) -> String {
|
||||||
|
encode_completion(&self.environment, key, source, position, explicit)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn encode_result(result: Result<String, String>) -> String {
|
fn encode_result(result: Result<String, String>) -> String {
|
||||||
match result {
|
match result {
|
||||||
Ok(output) => format!("{{\"ok\":true,\"output\":{}}}", json_string(&output)),
|
Ok(output) => serde_json::json!({ "ok": true, "output": output }).to_string(),
|
||||||
Err(error) => format!("{{\"ok\":false,\"error\":{}}}", json_string(&error)),
|
Err(error) => serde_json::json!({ "ok": false, "error": error }).to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,103 +82,106 @@ fn evaluate_inner(source: &str) -> Result<String, String> {
|
|||||||
Ok(format_data(&data, 0))
|
Ok(format_data(&data, 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn evaluate_project_inner(entry: &str, files_json: &str) -> Result<String, String> {
|
#[cfg(target_arch = "wasm32")]
|
||||||
let raw_files: BTreeMap<String, String> = serde_json::from_str(files_json)
|
fn evaluate_with_environment(
|
||||||
.map_err(|error| format!("failed to read playground files: {error}"))?;
|
environment: &JsEnvironment,
|
||||||
let mut files = BTreeMap::new();
|
key: &str,
|
||||||
for (path, source) in raw_files {
|
name: &str,
|
||||||
let path = normalize_path(&path).ok_or_else(|| format!("invalid file path `{path}`"))?;
|
source: &str,
|
||||||
files.insert(path, source);
|
) -> Result<String, String> {
|
||||||
}
|
let mut engine = environment
|
||||||
|
.create_engine()
|
||||||
let entry = normalize_path(entry).ok_or_else(|| format!("invalid entry path `{entry}`"))?;
|
.map_err(|diagnostic| diagnostic.message)?;
|
||||||
let source = files
|
let module = match engine.add_root_source(key, name, source) {
|
||||||
.get(&entry)
|
|
||||||
.cloned()
|
|
||||||
.ok_or_else(|| format!("entry file `{entry}` was not found"))?;
|
|
||||||
|
|
||||||
let mut engine = Engine::new(VirtualLoader { files });
|
|
||||||
let module = match engine.add_root_source(entry.clone(), entry.clone(), &source) {
|
|
||||||
Ok(module) => module,
|
Ok(module) => module,
|
||||||
Err(error) => return Err(format_diagnostic_with_root(&error, &entry)),
|
Err(diagnostic) => return Err(engine.format_diagnostic(&diagnostic)),
|
||||||
};
|
};
|
||||||
let value = match engine.eval_module(module) {
|
let value = match engine.eval_module(module) {
|
||||||
Ok(value) => value,
|
Ok(value) => value,
|
||||||
Err(error) => return Err(engine.format_diagnostic(&error)),
|
Err(diagnostic) => return Err(engine.format_diagnostic(&diagnostic)),
|
||||||
};
|
};
|
||||||
let data = match engine.materialize(&value) {
|
let data = match engine.materialize(&value) {
|
||||||
Ok(data) => data,
|
Ok(data) => data,
|
||||||
Err(error) => return Err(engine.format_diagnostic(&error)),
|
Err(diagnostic) => return Err(engine.format_diagnostic(&diagnostic)),
|
||||||
};
|
};
|
||||||
Ok(format_data(&data, 0))
|
Ok(format_data(&data, 0))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[cfg(target_arch = "wasm32")]
|
||||||
struct VirtualLoader {
|
fn encode_completion(
|
||||||
files: BTreeMap<String, String>,
|
environment: &JsEnvironment,
|
||||||
}
|
key: &str,
|
||||||
|
source: &str,
|
||||||
impl SourceLoader for VirtualLoader {
|
position: usize,
|
||||||
fn load(
|
explicit: bool,
|
||||||
&mut self,
|
) -> String {
|
||||||
current_key: Option<&str>,
|
let byte_position = utf16_offset_to_byte(source, position);
|
||||||
specifier: &str,
|
let service = LanguageService::new(environment);
|
||||||
) -> decodal::Result<LoadedSource> {
|
let completion = match service.complete(key, source, byte_position, explicit) {
|
||||||
let key = resolve_import(current_key, specifier).ok_or_else(|| {
|
Ok(completion) => completion,
|
||||||
Diagnostic::new(
|
Err(diagnostic) => {
|
||||||
DiagnosticKind::Import,
|
return serde_json::json!({
|
||||||
Span::default(),
|
"ok": false,
|
||||||
format!("invalid import path `{specifier}`"),
|
"error": diagnostic.message,
|
||||||
)
|
|
||||||
})?;
|
|
||||||
let source = self.files.get(&key).cloned().ok_or_else(|| {
|
|
||||||
Diagnostic::new(
|
|
||||||
DiagnosticKind::Import,
|
|
||||||
Span::default(),
|
|
||||||
format!("import `{specifier}` resolved to `{key}`, but that file does not exist"),
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
Ok(LoadedSource {
|
|
||||||
key: key.clone(),
|
|
||||||
name: key,
|
|
||||||
source,
|
|
||||||
})
|
})
|
||||||
|
.to_string();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let completion = completion.map(|completion| {
|
||||||
|
let from = byte_offset_to_utf16(source, completion.from);
|
||||||
|
let options = completion
|
||||||
|
.items
|
||||||
|
.into_iter()
|
||||||
|
.map(|item| {
|
||||||
|
serde_json::json!({
|
||||||
|
"label": item.label,
|
||||||
|
"kind": completion_kind_name(item.kind),
|
||||||
|
"detail": item.detail,
|
||||||
|
"priority": item.priority,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
serde_json::json!({ "from": from, "options": options })
|
||||||
|
});
|
||||||
|
serde_json::json!({ "ok": true, "completion": completion }).to_string()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
fn completion_kind_name(kind: CompletionKind) -> &'static str {
|
||||||
|
match kind {
|
||||||
|
CompletionKind::Keyword => "keyword",
|
||||||
|
CompletionKind::Constant => "constant",
|
||||||
|
CompletionKind::Type => "type",
|
||||||
|
CompletionKind::Variable => "variable",
|
||||||
|
CompletionKind::Namespace => "namespace",
|
||||||
|
CompletionKind::Property => "property",
|
||||||
|
CompletionKind::File => "file",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn resolve_import(current_key: Option<&str>, specifier: &str) -> Option<String> {
|
#[cfg(any(target_arch = "wasm32", test))]
|
||||||
if specifier.starts_with('/') {
|
fn utf16_offset_to_byte(source: &str, offset: usize) -> usize {
|
||||||
return normalize_path(specifier);
|
let mut utf16 = 0usize;
|
||||||
|
for (byte, ch) in source.char_indices() {
|
||||||
|
if utf16 >= offset {
|
||||||
|
return byte;
|
||||||
}
|
}
|
||||||
|
let next = utf16 + ch.len_utf16();
|
||||||
let mut base = String::new();
|
if next > offset {
|
||||||
if let Some(current_key) = current_key {
|
return byte;
|
||||||
if let Some((parent, _file)) = current_key.rsplit_once('/') {
|
|
||||||
base.push_str(parent);
|
|
||||||
base.push('/');
|
|
||||||
}
|
}
|
||||||
|
utf16 = next;
|
||||||
}
|
}
|
||||||
base.push_str(specifier);
|
source.len()
|
||||||
normalize_path(&base)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn normalize_path(path: &str) -> Option<String> {
|
#[cfg(any(target_arch = "wasm32", test))]
|
||||||
let mut parts = Vec::new();
|
fn byte_offset_to_utf16(source: &str, mut offset: usize) -> usize {
|
||||||
let normalized = path.replace('\\', "/");
|
offset = offset.min(source.len());
|
||||||
for part in normalized.split('/') {
|
while !source.is_char_boundary(offset) {
|
||||||
match part {
|
offset = offset.saturating_sub(1);
|
||||||
"" | "." => {}
|
|
||||||
".." => {
|
|
||||||
parts.pop()?;
|
|
||||||
}
|
|
||||||
part => parts.push(part),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if parts.is_empty() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(parts.join("/"))
|
|
||||||
}
|
}
|
||||||
|
source[..offset].encode_utf16().count()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn format_diagnostic_with_root(diagnostic: &decodal::Diagnostic, root_name: &str) -> String {
|
fn format_diagnostic_with_root(diagnostic: &decodal::Diagnostic, root_name: &str) -> String {
|
||||||
@@ -191,66 +236,28 @@ fn format_data(data: &Data, indent: usize) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn json_string(value: &str) -> String {
|
fn json_string(value: &str) -> String {
|
||||||
let mut out = String::from("\"");
|
serde_json::to_string(value).expect("strings are always JSON-serializable")
|
||||||
for ch in value.chars() {
|
|
||||||
match ch {
|
|
||||||
'"' => out.push_str("\\\""),
|
|
||||||
'\\' => out.push_str("\\\\"),
|
|
||||||
'\n' => out.push_str("\\n"),
|
|
||||||
'\r' => out.push_str("\\r"),
|
|
||||||
'\t' => out.push_str("\\t"),
|
|
||||||
ch if ch.is_control() => {
|
|
||||||
use core::fmt::Write;
|
|
||||||
let _ = write!(out, "\\u{:04x}", ch as u32);
|
|
||||||
}
|
|
||||||
ch => out.push(ch),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out.push('"');
|
|
||||||
out
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{evaluate_project_inner, normalize_path, resolve_import};
|
use super::{byte_offset_to_utf16, evaluate_inner, utf16_offset_to_byte};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn normalizes_virtual_paths() {
|
fn evaluates_standalone_sources() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
normalize_path("/schemas/../main.dcdl"),
|
evaluate_inner("value = 1;").unwrap(),
|
||||||
Some("main.dcdl".into())
|
"{\n \"value\": 1\n}"
|
||||||
);
|
|
||||||
assert_eq!(normalize_path("../main.dcdl"), None);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn resolves_imports_relative_to_current_file() {
|
|
||||||
assert_eq!(
|
|
||||||
resolve_import(Some("schemas/service.dcdl"), "./types.dcdl"),
|
|
||||||
Some("schemas/types.dcdl".into())
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn evaluates_project_imports() {
|
fn converts_web_utf16_offsets() {
|
||||||
let files = r#"{
|
let source = "a😀β";
|
||||||
"main.dcdl":"let dep = import \"./schemas/service.dcdl\"; in dep.Service & { port = 9443; }",
|
assert_eq!(utf16_offset_to_byte(source, 0), 0);
|
||||||
"schemas/service.dcdl":"Service = { name = String default \"api\"; port = Int & > 443 default 8443; }"
|
assert_eq!(utf16_offset_to_byte(source, 1), 1);
|
||||||
}"#;
|
assert_eq!(utf16_offset_to_byte(source, 3), 5);
|
||||||
let output = evaluate_project_inner("main.dcdl", files).unwrap();
|
assert_eq!(byte_offset_to_utf16(source, 5), 3);
|
||||||
assert!(output.contains("\"port\": 9443"));
|
assert_eq!(byte_offset_to_utf16(source, source.len()), 4);
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn project_diagnostics_use_virtual_file_names() {
|
|
||||||
let files = r#"{
|
|
||||||
"main.dcdl":"let dep = import \"./schemas/service.dcdl\"; in dep.Service & { port = 80; }",
|
|
||||||
"schemas/service.dcdl":"Service = { port = Int & > 443 default 8443; }"
|
|
||||||
}"#;
|
|
||||||
let error = evaluate_project_inner("main.dcdl", files).unwrap_err();
|
|
||||||
assert!(error.contains("main.dcdl:"));
|
|
||||||
assert!(error.contains("schemas/service.dcdl:"));
|
|
||||||
assert!(!error.contains("source 0:"));
|
|
||||||
assert!(!error.contains("source 1:"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,320 @@
|
|||||||
|
use decodal::{CompareOp, Constraint, LiteralValue, PrimitiveType, Value};
|
||||||
|
use serde_json::{Map, Value as JsonValue};
|
||||||
|
|
||||||
|
pub(crate) fn from_json(value: &JsonValue) -> Result<Value, String> {
|
||||||
|
match value {
|
||||||
|
JsonValue::Null => Err(String::from("null is not a Decodal value")),
|
||||||
|
JsonValue::Bool(value) => Ok(Value::bool(*value)),
|
||||||
|
JsonValue::Number(value) => number(value),
|
||||||
|
JsonValue::String(value) => Ok(Value::string(value)),
|
||||||
|
JsonValue::Array(items) => items
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, value)| {
|
||||||
|
from_json(value).map_err(|error| format!("array item {index}: {error}"))
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.map(Value::array),
|
||||||
|
JsonValue::Object(fields) => object(fields),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn number(value: &serde_json::Number) -> Result<Value, String> {
|
||||||
|
if let Some(value) = value.as_i64() {
|
||||||
|
return Ok(Value::int(value));
|
||||||
|
}
|
||||||
|
if let Some(value) = value.as_u64() {
|
||||||
|
return i64::try_from(value)
|
||||||
|
.map(Value::int)
|
||||||
|
.map_err(|_| String::from("integer value is outside the signed 64-bit range"));
|
||||||
|
}
|
||||||
|
value
|
||||||
|
.as_f64()
|
||||||
|
.filter(|value| value.is_finite())
|
||||||
|
.map(Value::float)
|
||||||
|
.ok_or_else(|| String::from("invalid numeric value"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn object(fields: &Map<String, JsonValue>) -> Result<Value, String> {
|
||||||
|
let Some(descriptor) = fields.get("$decodal") else {
|
||||||
|
return fields
|
||||||
|
.iter()
|
||||||
|
.map(|(name, value)| {
|
||||||
|
from_json(value)
|
||||||
|
.map(|value| (name.clone(), value))
|
||||||
|
.map_err(|error| format!("field `{name}`: {error}"))
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.map(Value::object);
|
||||||
|
};
|
||||||
|
|
||||||
|
let descriptor = descriptor
|
||||||
|
.as_str()
|
||||||
|
.ok_or_else(|| String::from("`$decodal` must be a descriptor name"))?;
|
||||||
|
let constraints = parse_constraints(fields.get("constraints"))?;
|
||||||
|
let default = fields
|
||||||
|
.get("default")
|
||||||
|
.map(from_json)
|
||||||
|
.transpose()?
|
||||||
|
.map(Box::new);
|
||||||
|
|
||||||
|
match descriptor {
|
||||||
|
"Unknown" => {
|
||||||
|
let mut all_constraints = vec![Constraint::Unknown];
|
||||||
|
all_constraints.extend(constraints);
|
||||||
|
Ok(Value::Range {
|
||||||
|
constraints: all_constraints,
|
||||||
|
default,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"String" | "Int" | "Float" | "Bool" => {
|
||||||
|
let primitive = match descriptor {
|
||||||
|
"String" => PrimitiveType::String,
|
||||||
|
"Int" => PrimitiveType::Int,
|
||||||
|
"Float" => PrimitiveType::Float,
|
||||||
|
"Bool" => PrimitiveType::Bool,
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
let mut all_constraints = vec![Constraint::Type(primitive)];
|
||||||
|
all_constraints.extend(constraints);
|
||||||
|
Ok(Value::Range {
|
||||||
|
constraints: all_constraints,
|
||||||
|
default,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"Array" => {
|
||||||
|
let item = fields
|
||||||
|
.get("item")
|
||||||
|
.ok_or_else(|| String::from("Array descriptor requires `item`"))?;
|
||||||
|
Ok(Value::ArrayRange {
|
||||||
|
item: Box::new(from_json(item)?),
|
||||||
|
constraints,
|
||||||
|
default,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"Map" => {
|
||||||
|
let value = fields
|
||||||
|
.get("value")
|
||||||
|
.ok_or_else(|| String::from("Map descriptor requires `value`"))?;
|
||||||
|
Ok(Value::MapRange {
|
||||||
|
value: Box::new(from_json(value)?),
|
||||||
|
constraints,
|
||||||
|
default,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"Object" => {
|
||||||
|
if !constraints.is_empty() {
|
||||||
|
return Err(String::from(
|
||||||
|
"Object descriptor does not accept `constraints`; constrain its fields or rest range",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if default.is_some() {
|
||||||
|
return Err(String::from(
|
||||||
|
"Object descriptor does not accept `default`; place defaults on its fields",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let object_fields = fields
|
||||||
|
.get("fields")
|
||||||
|
.and_then(JsonValue::as_object)
|
||||||
|
.ok_or_else(|| String::from("Object descriptor requires object `fields`"))?;
|
||||||
|
let value_fields = object_fields
|
||||||
|
.iter()
|
||||||
|
.map(|(name, value)| {
|
||||||
|
from_json(value)
|
||||||
|
.map(|value| (name.clone(), value))
|
||||||
|
.map_err(|error| format!("field `{name}`: {error}"))
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, _>>()?;
|
||||||
|
let rest = fields
|
||||||
|
.get("rest")
|
||||||
|
.ok_or_else(|| String::from("Object descriptor requires `rest`"))?;
|
||||||
|
Ok(Value::object_with_rest(value_fields, from_json(rest)?))
|
||||||
|
}
|
||||||
|
"Range" => Ok(Value::Range {
|
||||||
|
constraints,
|
||||||
|
default,
|
||||||
|
}),
|
||||||
|
name => Err(format!("unknown Decodal value descriptor `{name}`")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_constraints(value: Option<&JsonValue>) -> Result<Vec<Constraint>, String> {
|
||||||
|
let Some(value) = value else {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
};
|
||||||
|
let items = value
|
||||||
|
.as_array()
|
||||||
|
.ok_or_else(|| String::from("`constraints` must be an array"))?;
|
||||||
|
items
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, value)| {
|
||||||
|
parse_constraint(value).map_err(|error| format!("constraint {index}: {error}"))
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_constraint(value: &JsonValue) -> Result<Constraint, String> {
|
||||||
|
let fields = value
|
||||||
|
.as_object()
|
||||||
|
.ok_or_else(|| String::from("constraint must be an object"))?;
|
||||||
|
let kind = required_string(fields, "kind")?;
|
||||||
|
match kind {
|
||||||
|
"unknown" => Ok(Constraint::Unknown),
|
||||||
|
"type" => match required_string(fields, "value")? {
|
||||||
|
"String" => Ok(Constraint::Type(PrimitiveType::String)),
|
||||||
|
"Int" => Ok(Constraint::Type(PrimitiveType::Int)),
|
||||||
|
"Float" => Ok(Constraint::Type(PrimitiveType::Float)),
|
||||||
|
"Bool" => Ok(Constraint::Type(PrimitiveType::Bool)),
|
||||||
|
value => Err(format!("unknown primitive type `{value}`")),
|
||||||
|
},
|
||||||
|
"compare" => {
|
||||||
|
let operation = match required_string(fields, "op")? {
|
||||||
|
">" => CompareOp::Gt,
|
||||||
|
">=" => CompareOp::Gte,
|
||||||
|
"<" => CompareOp::Lt,
|
||||||
|
"<=" => CompareOp::Lte,
|
||||||
|
operation => return Err(format!("unknown comparison operator `{operation}`")),
|
||||||
|
};
|
||||||
|
let value = fields
|
||||||
|
.get("value")
|
||||||
|
.ok_or_else(|| String::from("compare constraint requires `value`"))?;
|
||||||
|
Ok(Constraint::Compare(operation, literal(value)?))
|
||||||
|
}
|
||||||
|
"regex" => Ok(Constraint::Regex(
|
||||||
|
required_string(fields, "value")?.to_owned(),
|
||||||
|
)),
|
||||||
|
"predicate" => Ok(Constraint::BuiltinPredicate(
|
||||||
|
required_string(fields, "value")?.to_owned(),
|
||||||
|
)),
|
||||||
|
kind => Err(format!("unknown constraint kind `{kind}`")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn literal(value: &JsonValue) -> Result<LiteralValue, String> {
|
||||||
|
match value {
|
||||||
|
JsonValue::String(value) => Ok(LiteralValue::String(value.clone())),
|
||||||
|
JsonValue::Bool(value) => Ok(LiteralValue::Bool(*value)),
|
||||||
|
JsonValue::Number(value) => match number(value)? {
|
||||||
|
Value::Int(value) => Ok(LiteralValue::Int(value)),
|
||||||
|
Value::Float(value) => Ok(LiteralValue::Float(value)),
|
||||||
|
_ => unreachable!(),
|
||||||
|
},
|
||||||
|
_ => Err(String::from("comparison value must be a primitive literal")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn required_string<'a>(fields: &'a Map<String, JsonValue>, name: &str) -> Result<&'a str, String> {
|
||||||
|
fields
|
||||||
|
.get(name)
|
||||||
|
.and_then(JsonValue::as_str)
|
||||||
|
.ok_or_else(|| format!("constraint requires string `{name}`"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use decodal::{Constraint, LiteralValue, PrimitiveType, Value};
|
||||||
|
|
||||||
|
use super::from_json;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn converts_plain_js_shaped_values() {
|
||||||
|
let value = serde_json::json!({
|
||||||
|
"frontmatter": { "draft": false },
|
||||||
|
"body": "# Hello",
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
from_json(&value).unwrap(),
|
||||||
|
Value::object([
|
||||||
|
("body", Value::string("# Hello")),
|
||||||
|
(
|
||||||
|
"frontmatter",
|
||||||
|
Value::object([("draft", Value::bool(false))]),
|
||||||
|
),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn converts_schema_descriptors() {
|
||||||
|
let value = serde_json::json!({
|
||||||
|
"$decodal": "Array",
|
||||||
|
"item": {
|
||||||
|
"$decodal": "Int",
|
||||||
|
"constraints": [{ "kind": "compare", "op": ">", "value": 0 }],
|
||||||
|
},
|
||||||
|
"default": [1, 2],
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
from_json(&value).unwrap(),
|
||||||
|
Value::ArrayRange {
|
||||||
|
item: Box::new(Value::Range {
|
||||||
|
constraints: vec![
|
||||||
|
Constraint::Type(PrimitiveType::Int),
|
||||||
|
Constraint::Compare(decodal::CompareOp::Gt, LiteralValue::Int(0)),
|
||||||
|
],
|
||||||
|
default: None,
|
||||||
|
}),
|
||||||
|
constraints: Vec::new(),
|
||||||
|
default: Some(Box::new(Value::array([Value::int(1), Value::int(2),]))),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn converts_map_schema_descriptors() {
|
||||||
|
let value = serde_json::json!({
|
||||||
|
"$decodal": "Map",
|
||||||
|
"value": { "$decodal": "Bool" },
|
||||||
|
"default": {},
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
from_json(&value).unwrap(),
|
||||||
|
Value::MapRange {
|
||||||
|
value: Box::new(Value::bool_type()),
|
||||||
|
constraints: Vec::new(),
|
||||||
|
default: Some(Box::new(Value::object([] as [(&str, Value); 0]))),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn converts_unknown_and_open_object_descriptors() {
|
||||||
|
let value = serde_json::json!({
|
||||||
|
"$decodal": "Object",
|
||||||
|
"fields": {
|
||||||
|
"name": { "$decodal": "String" },
|
||||||
|
},
|
||||||
|
"rest": { "$decodal": "Unknown" },
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
from_json(&value).unwrap(),
|
||||||
|
Value::object_with_rest([("name", Value::string_type())], Value::unknown(),)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn converts_general_range_descriptors() {
|
||||||
|
let value = serde_json::json!({
|
||||||
|
"$decodal": "Range",
|
||||||
|
"constraints": [{ "kind": "predicate", "value": "available" }],
|
||||||
|
"default": true,
|
||||||
|
});
|
||||||
|
assert_eq!(
|
||||||
|
from_json(&value).unwrap(),
|
||||||
|
Value::Range {
|
||||||
|
constraints: vec![Constraint::BuiltinPredicate("available".into())],
|
||||||
|
default: Some(Box::new(Value::bool(true))),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_null_values() {
|
||||||
|
assert!(
|
||||||
|
from_json(&serde_json::Value::Null)
|
||||||
|
.unwrap_err()
|
||||||
|
.contains("null")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
use decodal::{
|
||||||
|
Diagnostic, DiagnosticKind, Engine, HostEnvironment, ImportCandidate, ImportLoader,
|
||||||
|
LoadedImport, Span, Value as DecodalValue,
|
||||||
|
};
|
||||||
|
use js_sys::{Function, Reflect};
|
||||||
|
use serde_json::Value;
|
||||||
|
use wasm_bindgen::{JsCast, JsValue};
|
||||||
|
|
||||||
|
use crate::value;
|
||||||
|
|
||||||
|
pub(crate) struct JsEnvironment {
|
||||||
|
globals: BTreeMap<String, DecodalValue>,
|
||||||
|
load_import: Option<Function>,
|
||||||
|
complete_import: Option<Function>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl JsEnvironment {
|
||||||
|
pub(crate) fn from_options(options: JsValue) -> Result<Self, JsValue> {
|
||||||
|
if !options.is_null() && !options.is_undefined() && !options.is_object() {
|
||||||
|
return Err(JsValue::from_str(
|
||||||
|
"DecodalLanguageService options must be an object",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let globals = property(&options, "globals")?;
|
||||||
|
let globals = if globals.is_null() || globals.is_undefined() {
|
||||||
|
BTreeMap::new()
|
||||||
|
} else {
|
||||||
|
let globals: Value = serde_wasm_bindgen::from_value(globals).map_err(|error| {
|
||||||
|
JsValue::from_str(&format!("failed to read `globals`: {error}"))
|
||||||
|
})?;
|
||||||
|
let globals = globals.as_object().ok_or_else(|| {
|
||||||
|
JsValue::from_str("DecodalLanguageService `globals` must be an object")
|
||||||
|
})?;
|
||||||
|
globals
|
||||||
|
.iter()
|
||||||
|
.map(|(name, value)| {
|
||||||
|
value::from_json(value)
|
||||||
|
.map(|value| (name.clone(), value))
|
||||||
|
.map_err(|error| {
|
||||||
|
JsValue::from_str(&format!("invalid global `{name}`: {error}"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Result<BTreeMap<_, _>, _>>()?
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
globals,
|
||||||
|
load_import: optional_function(&options, "loadImport")?,
|
||||||
|
complete_import: optional_function(&options, "completeImport")?,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HostEnvironment for JsEnvironment {
|
||||||
|
type Loader = JsLoader;
|
||||||
|
|
||||||
|
fn create_loader(&self) -> Self::Loader {
|
||||||
|
JsLoader {
|
||||||
|
load_import: self.load_import.clone(),
|
||||||
|
complete_import: self.complete_import.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configure_engine(&self, engine: &mut Engine<Self::Loader>) -> decodal::Result<()> {
|
||||||
|
for (name, value) in &self.globals {
|
||||||
|
engine.bind_global(name, value.clone())?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct JsLoader {
|
||||||
|
load_import: Option<Function>,
|
||||||
|
complete_import: Option<Function>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImportLoader for JsLoader {
|
||||||
|
fn load(
|
||||||
|
&mut self,
|
||||||
|
current_key: Option<&str>,
|
||||||
|
specifier: &str,
|
||||||
|
) -> decodal::Result<LoadedImport> {
|
||||||
|
let callback = self.load_import.as_ref().ok_or_else(|| {
|
||||||
|
import_error(format!(
|
||||||
|
"no JavaScript `loadImport` callback is configured for `{specifier}`"
|
||||||
|
))
|
||||||
|
})?;
|
||||||
|
let current_key = current_key.map(JsValue::from_str).unwrap_or(JsValue::NULL);
|
||||||
|
let specifier = JsValue::from_str(specifier);
|
||||||
|
let loaded = callback
|
||||||
|
.call2(&JsValue::UNDEFINED, ¤t_key, &specifier)
|
||||||
|
.map_err(|error| import_error(js_error_message(error)))?;
|
||||||
|
let loaded: Value = serde_wasm_bindgen::from_value(loaded)
|
||||||
|
.map_err(|error| import_error(format!("invalid `loadImport` result: {error}")))?;
|
||||||
|
loaded_import(&loaded).map_err(import_error)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn complete_import(
|
||||||
|
&mut self,
|
||||||
|
current_key: Option<&str>,
|
||||||
|
prefix: &str,
|
||||||
|
) -> decodal::Result<Vec<ImportCandidate>> {
|
||||||
|
let Some(callback) = &self.complete_import else {
|
||||||
|
return Ok(Vec::new());
|
||||||
|
};
|
||||||
|
let current_key = current_key.map(JsValue::from_str).unwrap_or(JsValue::NULL);
|
||||||
|
let prefix = JsValue::from_str(prefix);
|
||||||
|
let candidates = callback
|
||||||
|
.call2(&JsValue::UNDEFINED, ¤t_key, &prefix)
|
||||||
|
.map_err(|error| import_error(js_error_message(error)))?;
|
||||||
|
let candidates: Value = serde_wasm_bindgen::from_value(candidates)
|
||||||
|
.map_err(|error| import_error(format!("invalid `completeImport` result: {error}")))?;
|
||||||
|
import_candidates(&candidates).map_err(import_error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn loaded_import(value: &Value) -> Result<LoadedImport, String> {
|
||||||
|
let fields = value
|
||||||
|
.as_object()
|
||||||
|
.ok_or_else(|| String::from("`loadImport` must return an object"))?;
|
||||||
|
let kind = string_field(fields, "kind")?;
|
||||||
|
let key = string_field(fields, "key")?;
|
||||||
|
match kind {
|
||||||
|
"source" => {
|
||||||
|
let source = string_field(fields, "source")?;
|
||||||
|
let name = fields.get("name").and_then(Value::as_str).unwrap_or(key);
|
||||||
|
Ok(LoadedImport::source(key, name, source))
|
||||||
|
}
|
||||||
|
"value" => {
|
||||||
|
let value = fields
|
||||||
|
.get("value")
|
||||||
|
.ok_or_else(|| String::from("value import requires `value`"))?;
|
||||||
|
let value = value::from_json(value)
|
||||||
|
.map_err(|error| format!("invalid imported value: {error}"))?;
|
||||||
|
Ok(LoadedImport::value(key, value))
|
||||||
|
}
|
||||||
|
kind => Err(format!("unknown `loadImport` result kind `{kind}`")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn import_candidates(value: &Value) -> Result<Vec<ImportCandidate>, String> {
|
||||||
|
value
|
||||||
|
.as_array()
|
||||||
|
.ok_or_else(|| String::from("`completeImport` must return an array"))?
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, value)| match value {
|
||||||
|
Value::String(specifier) => Ok(ImportCandidate::new(specifier)),
|
||||||
|
Value::Object(fields) => {
|
||||||
|
let mut candidate = ImportCandidate::new(string_field(fields, "specifier")?);
|
||||||
|
if let Some(detail) = fields.get("detail") {
|
||||||
|
let detail = detail.as_str().ok_or_else(|| {
|
||||||
|
format!("import candidate {index} `detail` must be a string")
|
||||||
|
})?;
|
||||||
|
candidate = candidate.with_detail(detail);
|
||||||
|
}
|
||||||
|
Ok(candidate)
|
||||||
|
}
|
||||||
|
_ => Err(format!(
|
||||||
|
"import candidate {index} must be a string or object"
|
||||||
|
)),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn string_field<'a>(
|
||||||
|
fields: &'a serde_json::Map<String, Value>,
|
||||||
|
name: &str,
|
||||||
|
) -> Result<&'a str, String> {
|
||||||
|
fields
|
||||||
|
.get(name)
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.ok_or_else(|| format!("`loadImport` result requires string `{name}`"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn property(options: &JsValue, name: &str) -> Result<JsValue, JsValue> {
|
||||||
|
if options.is_null() || options.is_undefined() {
|
||||||
|
return Ok(JsValue::UNDEFINED);
|
||||||
|
}
|
||||||
|
Reflect::get(options, &JsValue::from_str(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn optional_function(options: &JsValue, name: &str) -> Result<Option<Function>, JsValue> {
|
||||||
|
let value = property(options, name)?;
|
||||||
|
if value.is_null() || value.is_undefined() {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
value
|
||||||
|
.dyn_into::<Function>()
|
||||||
|
.map(Some)
|
||||||
|
.map_err(|_| JsValue::from_str(&format!("`{name}` must be a function")))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn js_error_message(value: JsValue) -> String {
|
||||||
|
if let Some(message) = value.as_string() {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
Reflect::get(&value, &JsValue::from_str("message"))
|
||||||
|
.ok()
|
||||||
|
.and_then(|message| message.as_string())
|
||||||
|
.unwrap_or_else(|| String::from("JavaScript import callback failed"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn import_error(message: impl Into<String>) -> Diagnostic {
|
||||||
|
Diagnostic::new(DiagnosticKind::Import, Span::default(), message)
|
||||||
|
}
|
||||||
@@ -30,16 +30,52 @@ Important paths:
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
crates/decodal-wasm/
|
crates/decodal-wasm/
|
||||||
site/decodal-site/src/wasm/
|
packages/decodal-wasm/
|
||||||
```
|
```
|
||||||
|
|
||||||
The generated files in `site/decodal-site/src/wasm/` are committed so the site can build without requiring every consumer to run `wasm-pack` first.
|
The npm package is `decodal-wasm`.
|
||||||
|
The JSR package is `@hare/decodal-wasm`.
|
||||||
|
|
||||||
|
The generated files in `packages/decodal-wasm/` are committed so the site can build without requiring every consumer to run `wasm-pack` first.
|
||||||
The WebAssembly package is for execution, not syntax highlighting.
|
The WebAssembly package is for execution, not syntax highlighting.
|
||||||
|
|
||||||
|
## Language tools
|
||||||
|
|
||||||
|
Semantic editor integration lives in the host-configurable language service crate.
|
||||||
|
It depends only on the runtime and accepts the same `HostEnvironment` implementation used by a production application.
|
||||||
|
The LSP crate adapts that service to the Language Server Protocol over stdin/stdout.
|
||||||
|
It provides full document synchronization, semantic diagnostics, and whole-document formatting.
|
||||||
|
A host-specific LSP binary injects its loader and global schema configuration without reimplementing evaluation rules.
|
||||||
|
|
||||||
|
Important paths:
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/decodal-language-service/
|
||||||
|
crates/decodal-lsp/
|
||||||
|
```
|
||||||
|
|
||||||
|
The default `decodal-lsp` binary reads Decodal imports from the filesystem.
|
||||||
|
Embedded hosts can call its library entry point with a custom `LspEnvironment` to reuse structured imports and to make unsaved external documents, such as Markdown, visible to the loader.
|
||||||
|
|
||||||
|
Source formatting lives in a separate Rust language tools crate.
|
||||||
|
It uses the canonical Decodal AST together with the runtime lexer's lossless syntax tokens, so native LSP and WebAssembly callers execute the same formatter implementation.
|
||||||
|
|
||||||
|
This component is responsible for operations that must preserve source text details such as comments and whitespace.
|
||||||
|
It is used by the CodeMirror package's bundled formatter WebAssembly and can also be used by an LSP adapter for formatting.
|
||||||
|
It does not depend on Tree-sitter or Lezer.
|
||||||
|
|
||||||
|
Important paths:
|
||||||
|
|
||||||
|
```text
|
||||||
|
crates/decodal-language-tools/
|
||||||
|
```
|
||||||
|
|
||||||
|
The current language tools crate exposes the formatter.
|
||||||
|
|
||||||
## Web editor components
|
## Web editor components
|
||||||
|
|
||||||
The Web playground editor uses CodeMirror 6 with a generated Lezer parser.
|
The Web playground editor uses CodeMirror 6 with a generated Lezer parser.
|
||||||
This is the source of syntax highlighting, folding, indentation, and editor syntax tree behavior in the browser UI.
|
Lezer provides syntax highlighting, folding, indentation, and editor syntax tree behavior. The browser formatter command calls the canonical Rust formatter compiled to WebAssembly.
|
||||||
|
|
||||||
Important paths:
|
Important paths:
|
||||||
|
|
||||||
@@ -48,6 +84,8 @@ editors/lezer-decodal/decodal.grammar
|
|||||||
packages/decodal-codemirror/src/decodal.js
|
packages/decodal-codemirror/src/decodal.js
|
||||||
packages/decodal-codemirror/src/decodal-parser.js
|
packages/decodal-codemirror/src/decodal-parser.js
|
||||||
packages/decodal-codemirror/src/decodal-parser.terms.js
|
packages/decodal-codemirror/src/decodal-parser.terms.js
|
||||||
|
packages/decodal-codemirror/src/format.js
|
||||||
|
packages/decodal-codemirror/wasm/
|
||||||
```
|
```
|
||||||
|
|
||||||
The npm package is `decodal-codemirror`.
|
The npm package is `decodal-codemirror`.
|
||||||
@@ -83,15 +121,20 @@ doc/manual/souce/language/grammar.md
|
|||||||
This EBNF is the language-level reference.
|
This EBNF is the language-level reference.
|
||||||
The Rust parser, Lezer grammar, and Tree-sitter grammar should be kept aligned with it, but each implementation may encode precedence and recovery behavior in the form required by its parser generator or runtime.
|
The Rust parser, Lezer grammar, and Tree-sitter grammar should be kept aligned with it, but each implementation may encode precedence and recovery behavior in the form required by its parser generator or runtime.
|
||||||
|
|
||||||
## What is not a public component
|
## Syntax token API
|
||||||
|
|
||||||
The Rust lexer is an implementation detail of the Rust parser.
|
The `decodal` crate exposes `tokenize_source`, `tokenize_source_with_source_id`, `SyntaxToken`, and `SyntaxTokenKind` for source-preserving tooling.
|
||||||
Decodal does not expose a standalone public tokenizer API for editor tooling.
|
Comments have explicit tokens, while whitespace is represented by gaps between token spans and can be recovered from the original source.
|
||||||
Consumers that need syntax information should use the component that matches their environment:
|
The evaluator parser and formatter therefore share one lexical definition without making Tree-sitter an upstream dependency.
|
||||||
|
|
||||||
|
Consumers should otherwise use the component matching their environment:
|
||||||
|
|
||||||
- Rust execution and embedding: `decodal`
|
- Rust execution and embedding: `decodal`
|
||||||
- Browser execution: `decodal-wasm`
|
- Browser execution: `decodal-wasm`
|
||||||
- Web editor syntax: Lezer / CodeMirror
|
- Web formatting and editor syntax: canonical formatter / CodeMirror / Lezer
|
||||||
|
- Semantic editor analysis: `decodal-language-service`
|
||||||
|
- Language Server Protocol integration: `decodal-lsp`
|
||||||
|
- Rust formatting: `decodal-language-tools`
|
||||||
- General editor syntax: Tree-sitter
|
- General editor syntax: Tree-sitter
|
||||||
|
|
||||||
This avoids having a separate token stream API whose behavior would have to be kept compatible with both runtime parsing and editor grammars.
|
Tree-sitter and Lezer remain downstream editor grammars and are not dependencies of the runtime formatter.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Composition and Materialization
|
# Composition and Materialization
|
||||||
|
|
||||||
`&`、`//`、`default`、materialize は、runtime value の variant に基づいて処理する。
|
`&`、`//`、`as`、`default`、materialize は、runtime value の variant に基づいて処理する。
|
||||||
|
|
||||||
## `&`
|
## `&`
|
||||||
|
|
||||||
@@ -89,7 +89,24 @@ patch(a: RuntimeValue, b: RuntimeValue) -> RuntimeValue
|
|||||||
- 配列、scalar、function は右辺置換とする。
|
- 配列、scalar、function は右辺置換とする。
|
||||||
|
|
||||||
`//` は制約を保持するための演算子ではない。
|
`//` は制約を保持するための演算子ではない。
|
||||||
制約を満たす具体化には `&` を使う。
|
対称な制約合成には `&` を使い、左辺が右辺より狭いことを確認しながら合成する場合は `as` を使う。
|
||||||
|
|
||||||
|
## `as`
|
||||||
|
|
||||||
|
`as` は narrower と wider の向きを固定した範囲包含確認と合成である。
|
||||||
|
|
||||||
|
```text
|
||||||
|
apply_as(narrower: RuntimeValue, wider: RuntimeValue)
|
||||||
|
-> RuntimeValue | Diagnostic
|
||||||
|
```
|
||||||
|
|
||||||
|
abstract / abstract では、左辺 constraints が右辺 constraints を包含することを確認し、左辺を結果にする。
|
||||||
|
concrete / abstract では、concrete value が右辺 constraints を満たすことを確認する。
|
||||||
|
|
||||||
|
object / object では右辺を field domain とする。左辺にしかない field は診断し、両側にある field は再帰的に `apply_as` し、右辺にしかない field は thunk を force せずそのまま結果へ残す。
|
||||||
|
この扱いは default の有無に依存しない。default は包含判定の根拠でも `as` の補完値でもなく、後の materialize だけが選択する。
|
||||||
|
|
||||||
|
`ArrayItems(wider)` は concrete array の全要素を、`MapValues(wider)` は concrete object の全 field value を同じ関数で絞り込む。
|
||||||
|
|
||||||
## materialize
|
## materialize
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ Diagnostic {
|
|||||||
span: Span
|
span: Span
|
||||||
message: String
|
message: String
|
||||||
labels: Vec<DiagnosticLabel>
|
labels: Vec<DiagnosticLabel>
|
||||||
|
notes: Vec<String>
|
||||||
}
|
}
|
||||||
|
|
||||||
DiagnosticLabel {
|
DiagnosticLabel {
|
||||||
@@ -22,6 +23,7 @@ DiagnosticLabel {
|
|||||||
`span` は primary location を示す。
|
`span` は primary location を示す。
|
||||||
表示時には `Span.source` を source id のまま出すのではなく、可能な限り file path や virtual file name に解決する。
|
表示時には `Span.source` を source id のまま出すのではなく、可能な限り file path や virtual file name に解決する。
|
||||||
`labels` は同じ error に関係する追加 location を示す。
|
`labels` は同じ error に関係する追加 location を示す。
|
||||||
|
`notes` は source location を持たない semantic context を示す。
|
||||||
合成や materialize の失敗では、衝突した constraint、value、default、または処理中 field path を label に含める。
|
合成や materialize の失敗では、衝突した constraint、value、default、または処理中 field path を label に含める。
|
||||||
|
|
||||||
代表的な diagnostic kind:
|
代表的な diagnostic kind:
|
||||||
@@ -37,6 +39,9 @@ DiagnosticLabel {
|
|||||||
- match failure
|
- match failure
|
||||||
- materialization failure
|
- materialization failure
|
||||||
|
|
||||||
|
Structured imports use semantic provenance rather than synthetic source spans.
|
||||||
|
Constraint failures report the imported value's stable key and logical field or array path alongside the source span of the Decodal constraint that rejected it.
|
||||||
|
|
||||||
## エラーは値ではない
|
## エラーは値ではない
|
||||||
|
|
||||||
評価失敗は `RuntimeValue` ではなく `Diagnostic` を返す。
|
評価失敗は `RuntimeValue` ではなく `Diagnostic` を返す。
|
||||||
@@ -70,5 +75,5 @@ fallback は有限で明示的な仕組みに限定する。
|
|||||||
- `default`: 未指定値の fallback。
|
- `default`: 未指定値の fallback。
|
||||||
- `match`: 有限 pattern に基づく分岐。
|
- `match`: 有限 pattern に基づく分岐。
|
||||||
|
|
||||||
Decodal は `unknown` / `any` を持たないため、field 不在や未解決 identifier を fallback 可能な通常値として扱わない。
|
`Unknown` は明示的に書く最上位のabstract rangeであり、評価エラーを包む値ではない。
|
||||||
それらは diagnostic として報告する。
|
field 不在や未解決 identifier は `Unknown` へ変換せず、diagnostic として報告する。
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# Embedding API
|
# Embedding API
|
||||||
|
|
||||||
Decodal core can be embedded without giving the core crate access to a filesystem.
|
Decodal core can be embedded without giving the core crate access to a filesystem.
|
||||||
The host supplies imported sources through `SourceLoader` and may also provide global bindings through the host prelude API.
|
The host supplies imports through `ImportLoader` and may also provide global bindings through the host prelude API.
|
||||||
|
|
||||||
## Host prelude
|
## Host prelude
|
||||||
|
|
||||||
@@ -17,23 +17,23 @@ let / function env
|
|||||||
```
|
```
|
||||||
|
|
||||||
Module top-level bindings shadow prelude bindings.
|
Module top-level bindings shadow prelude bindings.
|
||||||
Primitive type names such as `String`, `Int`, `Float`, `Bool`, and `Array` are handled before environment lookup, so they are reserved and cannot be shadowed by host bindings.
|
Primitive type names such as `String`, `Int`, `Float`, and `Bool`, plus the top range `Unknown`, are handled before environment lookup, so they are reserved and cannot be shadowed by host bindings.
|
||||||
|
|
||||||
## Global bindings
|
## Global bindings
|
||||||
|
|
||||||
The host can bind values before adding or evaluating user sources.
|
The host can bind values before adding or evaluating user sources.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use decodal::{EmptyLoader, Engine, HostValue};
|
use decodal::{EmptyLoader, Engine, Value};
|
||||||
|
|
||||||
let mut engine = Engine::new(EmptyLoader);
|
let mut engine = Engine::new(EmptyLoader);
|
||||||
|
|
||||||
engine.bind_global(
|
engine.bind_global(
|
||||||
"Service",
|
"Service",
|
||||||
HostValue::object([
|
Value::object([
|
||||||
("name", HostValue::string_type()),
|
("name", Value::string_type()),
|
||||||
("port", HostValue::int_type().gt(443).default_int(8443)?),
|
("port", Value::int_type().gt(443).default_int(8443)?),
|
||||||
("enabled", HostValue::bool_type().default_bool(true)?),
|
("enabled", Value::bool_type().default_bool(true)?),
|
||||||
]),
|
]),
|
||||||
)?;
|
)?;
|
||||||
```
|
```
|
||||||
@@ -41,38 +41,55 @@ engine.bind_global(
|
|||||||
A user source can then refer to `Service` without importing it.
|
A user source can then refer to `Service` without importing it.
|
||||||
|
|
||||||
```dcdl
|
```dcdl
|
||||||
Service & {
|
{
|
||||||
name = "api";
|
name = "api";
|
||||||
port = 9443;
|
port = 9443;
|
||||||
}
|
} as Service
|
||||||
```
|
```
|
||||||
|
|
||||||
## HostValue
|
## Value
|
||||||
|
|
||||||
`HostValue` is the public builder-facing value representation for embedding.
|
`Value` is the public builder-facing value representation for embedding.
|
||||||
It keeps host code from constructing internal `ThunkId` or `ObjectValue` values directly.
|
It keeps host code from constructing internal `ThunkId` or `ObjectValue` values directly.
|
||||||
|
|
||||||
```text
|
```text
|
||||||
HostValue =
|
Value =
|
||||||
String
|
String
|
||||||
Int
|
Int
|
||||||
Float
|
Float
|
||||||
Bool
|
Bool
|
||||||
Array(Vec<HostValue>)
|
Array(Vec<Value>)
|
||||||
Object(Vec<HostField>)
|
ArrayRange { item, constraints, default }
|
||||||
Abstract { constraints, default }
|
MapRange { value, constraints, default }
|
||||||
|
Object { fields, rest: Option<Value> }
|
||||||
|
Range { constraints, default }
|
||||||
```
|
```
|
||||||
|
|
||||||
When a host value is bound, the engine internalizes it into `RuntimeValue` and allocates value thunks for object fields, array items, and defaults.
|
When a value is bound by the host, the engine internalizes it into `RuntimeValue` and allocates value thunks for object fields, array items, and defaults.
|
||||||
|
|
||||||
## Abstract host objects
|
`Value::array_of(item)` builds an array constraint with a required element schema.
|
||||||
|
There is no `Value` constructor for an unconstrained abstract array.
|
||||||
|
|
||||||
A host-provided schema object is represented as a concrete object structure whose fields may contain abstract values.
|
`Value::map_of(value)` builds a map constraint whose arbitrary object field values must satisfy `value`.
|
||||||
|
`BTreeMap<String, T>` and, with `std`, `HashMap<String, T>` implement `DecodalSchema`, `DecodalDecode`, and `IntoValue` using this representation.
|
||||||
|
|
||||||
|
`Value::unknown()` builds the top abstract range. `Value::object_with_rest(fields, rest)` builds an object with named fields and a range for all remaining fields.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
HostValue::object([
|
Value::object_with_rest(
|
||||||
("name", HostValue::string_type()),
|
[("enabled", Value::bool_type().default_bool(true)?)],
|
||||||
("port", HostValue::int_type().gt(443).default_int(8443)?),
|
Value::unknown(),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Objects containing ranges
|
||||||
|
|
||||||
|
A host-provided schema object is represented as a concrete object structure whose fields may contain ranges.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
Value::object([
|
||||||
|
("name", Value::string_type()),
|
||||||
|
("port", Value::int_type().gt(443).default_int(8443)?),
|
||||||
])
|
])
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -92,9 +109,28 @@ This matches the runtime model used for Decodal source-defined schema objects.
|
|||||||
Hosts can enable the `derive` feature on `decodal` to keep a Rust struct, the Decodal schema, and the decoded result in sync.
|
Hosts can enable the `derive` feature on `decodal` to keep a Rust struct, the Decodal schema, and the decoded result in sync.
|
||||||
The derive implements two traits from the `decodal` crate:
|
The derive implements two traits from the `decodal` crate:
|
||||||
|
|
||||||
- `DecodalSchema`: builds a `HostValue` schema that can be passed to `Engine::bind_global`.
|
- `DecodalSchema`: builds a `Value` schema that can be passed to `Engine::bind_global`.
|
||||||
- `DecodalDecode`: decodes materialized `Data` back into the Rust type.
|
- `DecodalDecode`: decodes materialized `Data` back into the Rust type.
|
||||||
|
|
||||||
|
An explicitly marked map field can receive the open portion of an object.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use decodal::{Data, Decodal};
|
||||||
|
|
||||||
|
#[derive(Decodal)]
|
||||||
|
struct OpenConfig {
|
||||||
|
enabled: bool,
|
||||||
|
|
||||||
|
#[decodal(rest)]
|
||||||
|
extra: BTreeMap<String, Data>,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The schema generated for `extra` is `...Unknown`. A typed receiver such as `BTreeMap<String, String>` generates `...String` instead. During decode, named top-level fields are excluded and all remaining fields are collected into the map.
|
||||||
|
|
||||||
|
Only one `#[decodal(rest)]` field is allowed. It must implement `DecodalRest`; the core provides implementations for `BTreeMap<String, T>` and, with `std`, `HashMap<String, T>`. `rename`, `default`, and field constraints cannot be combined with `rest`. Without a rest receiver, a derived struct remains a closed object range.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
use decodal::{Decodal, DecodalDecode, DecodalSchema, EmptyLoader, Engine};
|
use decodal::{Decodal, DecodalDecode, DecodalSchema, EmptyLoader, Engine};
|
||||||
|
|
||||||
@@ -127,11 +163,96 @@ Supported field attributes are intentionally small:
|
|||||||
The derive does not add host callbacks or reflection.
|
The derive does not add host callbacks or reflection.
|
||||||
It only generates schema construction and typed decoding code.
|
It only generates schema construction and typed decoding code.
|
||||||
|
|
||||||
## SourceLoader and prelude together
|
## ImportLoader and prelude together
|
||||||
|
|
||||||
`SourceLoader` and host prelude bindings are independent mechanisms.
|
`ImportLoader` and host prelude bindings are independent mechanisms.
|
||||||
|
|
||||||
- Use `SourceLoader` when user sources should explicitly import host-provided modules.
|
- Use `ImportLoader` when user sources should explicitly import host-provided sources or values.
|
||||||
- Use prelude bindings when host-provided schemas or constants should be globally available.
|
- Use prelude bindings when host-provided schemas or constants should be globally available.
|
||||||
|
|
||||||
Both mechanisms share the same runtime evaluator, thunk model, and materialization rules.
|
Both mechanisms share the same runtime evaluator, thunk model, and materialization rules.
|
||||||
|
|
||||||
|
## Shared host environment
|
||||||
|
|
||||||
|
An embedded application can implement `HostEnvironment` to keep loader creation and global binding setup in one place.
|
||||||
|
Both production evaluation and semantic editor tooling create their engines from this environment, preventing the editor from drifting onto a separate validation path.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use decodal::{Engine, HostEnvironment};
|
||||||
|
|
||||||
|
struct AppEnvironment;
|
||||||
|
|
||||||
|
impl HostEnvironment for AppEnvironment {
|
||||||
|
type Loader = ContentLoader;
|
||||||
|
|
||||||
|
fn create_loader(&self) -> Self::Loader {
|
||||||
|
ContentLoader::new()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn configure_engine(
|
||||||
|
&self,
|
||||||
|
engine: &mut Engine<Self::Loader>,
|
||||||
|
) -> decodal::Result<()> {
|
||||||
|
engine.bind_global("Site", site_schema())?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let environment = AppEnvironment;
|
||||||
|
let mut engine = environment.create_engine()?;
|
||||||
|
```
|
||||||
|
|
||||||
|
The semantic service accepts the same environment by value:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use decodal_language_service::LanguageService;
|
||||||
|
|
||||||
|
let service = LanguageService::new(&environment);
|
||||||
|
let analysis = service.analyze("site.dcdl", "site.dcdl", source);
|
||||||
|
```
|
||||||
|
|
||||||
|
Each analysis uses a fresh engine and runs the normal parse, evaluate, and materialize pipeline.
|
||||||
|
An environment may create loaders backed by shared filesystem, database, or editor-overlay state when repeated analysis needs a current workspace snapshot.
|
||||||
|
|
||||||
|
The LSP adapter constructs that environment after receiving the client's initialization parameters:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use decodal_lsp::{LspEnvironment, run_stdio};
|
||||||
|
|
||||||
|
impl LspEnvironment for AppEnvironment {}
|
||||||
|
|
||||||
|
run_stdio(|initialize| {
|
||||||
|
let _ = initialize;
|
||||||
|
Ok(AppEnvironment)
|
||||||
|
})?;
|
||||||
|
```
|
||||||
|
|
||||||
|
`LspEnvironment` adds document lifecycle hooks on top of `HostEnvironment`.
|
||||||
|
A `run_stdio` environment factory always receives the client's `InitializeParams`; the host decides whether to use its workspace folders, initialization options, and capabilities or ignore them.
|
||||||
|
A host that keeps unsaved buffers in shared state can update them from `open_document`, `change_document`, and `close_document`; every subsequent diagnostic pass creates the normal import loader from that updated environment.
|
||||||
|
Synchronized non-Decodal documents are passed through these hooks but are not evaluated as Decodal roots, so a loader can parse an unsaved Markdown file into `Value` and immediately revalidate the open Decodal documents that import it.
|
||||||
|
|
||||||
|
## Structured imports
|
||||||
|
|
||||||
|
`ImportLoader::load` returns either `LoadedImport::Source` or `LoadedImport::Value`.
|
||||||
|
The value variant carries a `Value`, allowing a host to parse non-Decodal resources such as Markdown into an application-specific structure.
|
||||||
|
|
||||||
|
```text
|
||||||
|
import "./post.md"
|
||||||
|
-> host parses content
|
||||||
|
-> LoadedImport::Value {
|
||||||
|
key: "content/post.md",
|
||||||
|
value: { frontmatter: {...}, body: "..." }
|
||||||
|
}
|
||||||
|
-> Engine internalizes Value
|
||||||
|
-> normal composition and materialization
|
||||||
|
```
|
||||||
|
|
||||||
|
The core does not select content types or bundle Markdown/frontmatter parsers.
|
||||||
|
The loader owns path resolution, media or extension dispatch, parsing rules, and parse diagnostics.
|
||||||
|
The stable loader key is also used to cache structured imports.
|
||||||
|
|
||||||
|
When a structured value fails a Decodal constraint, the diagnostic keeps the Decodal constraint span and identifies the imported value by its stable key and logical value path, such as `content/post.md` and `frontmatter.draft`.
|
||||||
|
`Value` does not need source spans: syntax diagnostics for the external format remain the loader's responsibility, while cross-value validation reports semantic provenance.
|
||||||
|
|
||||||
|
`load` is the single import hook: loaders dispatch by extension, media type, or another host-defined rule and return the appropriate variant directly.
|
||||||
|
|||||||
@@ -16,13 +16,17 @@ demand-driven evaluation
|
|||||||
├─ load imported module on demand
|
├─ load imported module on demand
|
||||||
├─ evaluate expression
|
├─ evaluate expression
|
||||||
├─ compose `&`
|
├─ compose `&`
|
||||||
└─ patch `//`
|
├─ patch `//`
|
||||||
|
└─ validate `as`
|
||||||
↓
|
↓
|
||||||
materialize
|
materialize
|
||||||
↓
|
↓
|
||||||
data / diagnostics
|
data / diagnostics
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Production applications and semantic language services can construct this pipeline through the same host-defined `HostEnvironment`.
|
||||||
|
The environment creates the `ImportLoader` and installs host globals before the root module is registered.
|
||||||
|
|
||||||
## lexer / parser
|
## lexer / parser
|
||||||
|
|
||||||
lexer / parser は source を AST に変換する。
|
lexer / parser は source を AST に変換する。
|
||||||
@@ -66,14 +70,16 @@ ModuleRegistry:
|
|||||||
処理系は、まず root module を parse / desugar して registry に登録する。
|
処理系は、まず root module を parse / desugar して registry に登録する。
|
||||||
import 先 module は、この段階で全て読み込む必要はない。
|
import 先 module は、この段階で全て読み込む必要はない。
|
||||||
|
|
||||||
import expression が評価されたとき、処理系は `SourceLoader` に現在の module key と import specifier を渡す。
|
import expression が評価されたとき、処理系は `ImportLoader::load` に現在の module key と import specifier を渡す。
|
||||||
loader は module key、表示名、source text を返す。
|
loader は module key、表示名、および DCDL source text または構造化済み `Value` を返す。
|
||||||
module registry は key が未登録なら対象 module を parse / desugar して登録する。
|
DCDL source の場合、module registry は key が未登録なら対象 module を parse / desugar して登録する。
|
||||||
登録された module は module root thunk を持つ。
|
登録された source module は module root thunk を持つ。
|
||||||
同じ module が複数回 import された場合は、同じ `ModuleId` を返す。
|
構造化値の場合、host が返した `Value` を runtime value に internalize した root thunk を key でキャッシュする。
|
||||||
|
同じ key が複数回 import された場合は、同じ source module または structured root thunk を使う。
|
||||||
|
|
||||||
つまり import は module を即時評価しない。
|
つまり source import は module を即時評価しない。
|
||||||
module を読み込み、module root を thunk として登録するだけにする。
|
module を読み込み、module root を thunk として登録するだけにする。
|
||||||
|
structured import も root value を thunk として保持し、object field や array item の既存 thunk model を利用する。
|
||||||
|
|
||||||
AST の `ExprId` は module-local である。
|
AST の `ExprId` は module-local である。
|
||||||
そのため runtime が保持する式参照は `ExprRef { module, expr }` として module-qualified にする。
|
そのため runtime が保持する式参照は `ExprRef { module, expr }` として module-qualified にする。
|
||||||
@@ -100,9 +106,16 @@ eval(A // B):
|
|||||||
a = eval(A)
|
a = eval(A)
|
||||||
b = eval(B)
|
b = eval(B)
|
||||||
patch(a, b)
|
patch(a, b)
|
||||||
|
|
||||||
|
eval(A as S):
|
||||||
|
narrower = eval(A)
|
||||||
|
wider = eval(S)
|
||||||
|
apply_as(narrower, wider)
|
||||||
```
|
```
|
||||||
|
|
||||||
`&` は制約を保った合成を行い、`//` は右辺優先の deep patch を行う。
|
`&` は制約を保った対称な合成を行い、`//` は右辺優先の deep patch を行う。
|
||||||
|
`as` は左辺が右辺より狭いことを再帰的に確認する。左辺で絞られた部分は左辺を使い、右辺だけの部分は abstract のまま保持する。
|
||||||
|
default は `as` では選択せず、materialize まで遅延する。
|
||||||
詳細は [Composition and Materialization](./composition-and-materialization.md) に置く。
|
詳細は [Composition and Materialization](./composition-and-materialization.md) に置く。
|
||||||
|
|
||||||
## resolver / binder
|
## resolver / binder
|
||||||
@@ -135,7 +148,7 @@ resolver / binder を追加すると、以下を早期に診断しやすくな
|
|||||||
- import path の一部静的解決
|
- import path の一部静的解決
|
||||||
- span 付き diagnostic の精度向上
|
- span 付き diagnostic の精度向上
|
||||||
|
|
||||||
ただし、Decodal の制約検証は独立した type checking pass ではなく、`&` の合成時や materialize 時に行う。
|
ただし、Decodal の制約検証は独立した type checking pass ではなく、`&` の合成時、`as` の適用時、materialize 時に行う。
|
||||||
|
|
||||||
## materialize
|
## materialize
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ Core に入れる機能は、基本的に deterministic な value transformation
|
|||||||
- arithmetic / logical / comparison operators
|
- arithmetic / logical / comparison operators
|
||||||
- array concat
|
- array concat
|
||||||
- object / constraint composition
|
- object / constraint composition
|
||||||
|
- asymmetric range refinement, `Unknown`, map constraints, and object rest constraints
|
||||||
- default materialization
|
- default materialization
|
||||||
- pure function evaluation
|
- pure function evaluation
|
||||||
- host supplied import evaluation
|
- host supplied import evaluation
|
||||||
@@ -23,7 +24,7 @@ Core に入れないものは以下である。
|
|||||||
- arbitrary host function calls
|
- arbitrary host function calls
|
||||||
- symbolic constraint solving beyond simple normalization
|
- symbolic constraint solving beyond simple normalization
|
||||||
|
|
||||||
未解決 identifier や missing field は `unknown` として流れず、diagnostic になる。
|
未解決 identifier や missing field は明示的な `Unknown` range とは異なり、diagnostic になる。
|
||||||
この方針により、存在チェックや optional chaining のような dynamic object inspection は core language の対象外とする。
|
この方針により、存在チェックや optional chaining のような dynamic object inspection は core language の対象外とする。
|
||||||
|
|
||||||
## Constraint reasoning
|
## Constraint reasoning
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
## 方針
|
## 方針
|
||||||
|
|
||||||
初期処理系は AST interpreter として実装する。
|
初期処理系は AST interpreter として実装する。
|
||||||
bytecode VM や JIT ではなく、AST を demand-driven に評価することで、遅延評価、循環参照、`default`、`&`、`//` の意味論を小さく実装する。
|
bytecode VM や JIT ではなく、AST を demand-driven に評価することで、遅延評価、循環参照、`default`、`&`、`//`、`as` の意味論を小さく実装する。
|
||||||
|
|
||||||
## 構成
|
## 構成
|
||||||
|
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ RuntimeValue =
|
|||||||
`Concrete` は明示的な値である。
|
`Concrete` は明示的な値である。
|
||||||
`Abstract` は、まだ具体値に確定していない制約付きの値である。
|
`Abstract` は、まだ具体値に確定していない制約付きの値である。
|
||||||
|
|
||||||
Decodal は `unknown`、`any`、`null` のような「存在するが意味が未確定な値」を runtime value として持たない。
|
Decodal は `Unknown` を、すべてのDecodal値を包含する最上位の abstract range として持つ。
|
||||||
識別子や field が解決できない場合は、その場で diagnostic になる。
|
これは存在する具体値の型情報を消す `Any` ではない。具体値を `Unknown` に対して検証した場合は具体値を保持し、`Unknown` のままmaterializeしようとした場合は diagnostic になる。
|
||||||
未解決値を後続の演算へ流して推論することはしない。
|
識別子や field が解決できない状態とは区別し、それらは従来どおりその場で diagnostic になる。
|
||||||
|
|
||||||
## ConcreteValue
|
## ConcreteValue
|
||||||
|
|
||||||
@@ -37,10 +37,15 @@ object は concrete structure として扱う。
|
|||||||
```text
|
```text
|
||||||
ObjectValue:
|
ObjectValue:
|
||||||
fields: Map<Symbol, ObjectField>
|
fields: Map<Symbol, ObjectField>
|
||||||
|
rest: Option<ObjectRest>
|
||||||
|
|
||||||
ObjectField:
|
ObjectField:
|
||||||
value: ThunkId
|
value: ThunkId
|
||||||
span: Span
|
span: Span
|
||||||
|
|
||||||
|
ObjectRest:
|
||||||
|
value: ThunkId
|
||||||
|
span: Span
|
||||||
```
|
```
|
||||||
|
|
||||||
例えば以下の schema object は、object 自体は concrete だが、field の値は abstract value になる。
|
例えば以下の schema object は、object 自体は concrete だが、field の値は abstract value になる。
|
||||||
@@ -104,7 +109,7 @@ Concrete(Int(8000))
|
|||||||
## Runtime scope
|
## Runtime scope
|
||||||
|
|
||||||
Decodal runtime は application runtime ではなく、pure value evaluator である。
|
Decodal runtime は application runtime ではなく、pure value evaluator である。
|
||||||
同じ source、同じ import sources、同じ host globals が与えられた場合、評価結果は決定的である。
|
同じ source、同じ import results、同じ host globals が与えられた場合、評価結果は決定的である。
|
||||||
|
|
||||||
runtime が扱う責務は以下に限る。
|
runtime が扱う責務は以下に限る。
|
||||||
|
|
||||||
@@ -114,7 +119,7 @@ runtime が扱う責務は以下に限る。
|
|||||||
- materialize 時に constraint を検証する。
|
- materialize 時に constraint を検証する。
|
||||||
|
|
||||||
runtime は filesystem、network、environment variable、time、random、mutation を扱わない。
|
runtime は filesystem、network、environment variable、time、random、mutation を扱わない。
|
||||||
core における import は host supplied source を受け取る境界であり、filesystem access ではない。
|
core における import は host supplied source または structured value を受け取る境界であり、filesystem access ではない。
|
||||||
|
|
||||||
## Constraint
|
## Constraint
|
||||||
|
|
||||||
@@ -122,13 +127,24 @@ constraint は concrete value とは別の型として扱う。
|
|||||||
|
|
||||||
```text
|
```text
|
||||||
Constraint =
|
Constraint =
|
||||||
|
Unknown
|
||||||
Type(PrimitiveType)
|
Type(PrimitiveType)
|
||||||
|
ArrayItems(ThunkId)
|
||||||
|
MapValues(ThunkId)
|
||||||
Compare(Op, Literal)
|
Compare(Op, Literal)
|
||||||
Regex(Pattern)
|
Regex(Pattern)
|
||||||
BuiltinPredicate(Symbol)
|
BuiltinPredicate(Symbol)
|
||||||
ObjectConstraint(...)
|
ObjectConstraint(...)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`ArrayItems` は配列そのものの型と、すべての要素へ合成する schema thunk を表す。
|
||||||
|
配列用の primitive type は持たず、抽象配列には必ず要素制約が必要である。
|
||||||
|
|
||||||
|
`MapValues` は object の key 集合を制限せず、すべての field value へ適用する schema thunk を表す。
|
||||||
|
連想配列は materialize 後も `Data::Object` になり、別の data variant は持たない。
|
||||||
|
|
||||||
|
`ObjectValue.rest` は名前付きfieldを持つobjectの残余field rangeを表す。rest自体はfieldを生成せず、materializeは実在するfieldだけを出力する。
|
||||||
|
|
||||||
初期実装では、object の形は主に `Concrete(Object)` の field に `Abstract` を置くことで表現する。
|
初期実装では、object の形は主に `Concrete(Object)` の field に `Abstract` を置くことで表現する。
|
||||||
object 全体にかかる constraint は必要になった時点で追加する。
|
object 全体にかかる constraint は必要になった時点で追加する。
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,18 @@ cargo check -p decodal --no-default-features
|
|||||||
nix flake check
|
nix flake check
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Run the default stdio language server with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo run -q -p decodal-lsp
|
||||||
|
```
|
||||||
|
|
||||||
|
The protocol integration tests use an in-memory LSP connection and can be run independently:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo test -p decodal-lsp
|
||||||
|
```
|
||||||
|
|
||||||
Regex support is optional and should be tested explicitly when touched.
|
Regex support is optional and should be tested explicitly when touched.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -22,32 +34,13 @@ cargo run -q -p decodal-cli --features regex -- examples/regex/main.dcdl
|
|||||||
|
|
||||||
## crates.io release
|
## crates.io release
|
||||||
|
|
||||||
The primary crates.io package is `decodal`, which contains the embeddable library.
|
The crates.io release contains `decodal`, `decodal-derive`, `decodal-language-service`, `decodal-language-tools`, and `decodal-lsp`.
|
||||||
`decodal-derive` provides optional derive macros for Rust struct integration and is published only when the derive crate changes.
|
`decodal-cli` and the Rust source crate `decodal-wasm` remain repository-only packages.
|
||||||
Workspace support crates such as `decodal-cli` and the Rust source crate `decodal-wasm` are not published to crates.io.
|
The generated WebAssembly package under `packages/decodal-wasm/` is published to npm and JSR.
|
||||||
The generated WebAssembly package under `site/decodal-site/src/wasm/` is published to npm and JSR.
|
The CodeMirror package bundles the generated formatter WebAssembly from `decodal-language-tools`.
|
||||||
The project is dual licensed as `MIT OR Apache-2.0`.
|
The project is dual licensed as `MIT OR Apache-2.0`.
|
||||||
|
|
||||||
```sh
|
The authoritative validation commands, dependency order, and publish commands are maintained in `RELEASING.md` at the repository root.
|
||||||
cargo publish -p decodal
|
|
||||||
```
|
|
||||||
|
|
||||||
Publish `decodal-derive` first only when that crate has a new version:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cargo publish -p decodal-derive
|
|
||||||
```
|
|
||||||
|
|
||||||
Before publishing, run:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cargo fmt --check
|
|
||||||
cargo test
|
|
||||||
cargo check -p decodal --no-default-features
|
|
||||||
cargo publish -p decodal --dry-run
|
|
||||||
```
|
|
||||||
|
|
||||||
If `decodal-derive` changed, also run `cargo publish -p decodal-derive --dry-run`.
|
|
||||||
|
|
||||||
## Web site and playground
|
## Web site and playground
|
||||||
|
|
||||||
@@ -58,7 +51,7 @@ site/decodal-site/
|
|||||||
```
|
```
|
||||||
|
|
||||||
The site imports Markdown files from `doc/manual/souce/` and renders them as mdBook-style pages.
|
The site imports Markdown files from `doc/manual/souce/` and renders them as mdBook-style pages.
|
||||||
The playground loads `decodal-wasm` and evaluates DCDL entirely in the browser.
|
The playground loads `decodal-wasm` for evaluation and `decodal-codemirror` for editor features and source formatting.
|
||||||
|
|
||||||
Important files:
|
Important files:
|
||||||
|
|
||||||
@@ -72,10 +65,15 @@ site/decodal-site/src/lib/docs.js
|
|||||||
site/decodal-site/src/lib/highlight.js
|
site/decodal-site/src/lib/highlight.js
|
||||||
packages/decodal-codemirror/src/decodal.js
|
packages/decodal-codemirror/src/decodal.js
|
||||||
packages/decodal-codemirror/src/decodal-parser.js
|
packages/decodal-codemirror/src/decodal-parser.js
|
||||||
|
packages/decodal-wasm/decodal_wasm.js
|
||||||
|
packages/decodal-wasm/decodal_wasm_bg.wasm
|
||||||
|
packages/decodal-codemirror/wasm/decodal_language_tools.js
|
||||||
|
packages/decodal-codemirror/wasm/decodal_language_tools_bg.wasm
|
||||||
crates/decodal-wasm/src/lib.rs
|
crates/decodal-wasm/src/lib.rs
|
||||||
|
crates/decodal-language-tools/src/lib.rs
|
||||||
```
|
```
|
||||||
|
|
||||||
Build the WebAssembly package before building the site:
|
Build the WebAssembly packages before building the site:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd site/decodal-site
|
cd site/decodal-site
|
||||||
@@ -84,18 +82,18 @@ npm run build:wasm
|
|||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
`npm run build:wasm` writes generated files into:
|
`npm run build:wasm` builds both generated WebAssembly packages.
|
||||||
|
`npm run build:wasm:runtime` writes generated runtime files into `packages/decodal-wasm/`.
|
||||||
|
`npm run build:wasm:formatter` writes generated formatter files into `packages/decodal-codemirror/wasm/`.
|
||||||
|
`npm run build` builds only the static site from the current package files.
|
||||||
|
Use `npm run build:all` to regenerate both WebAssembly packages and then build the static site in one command.
|
||||||
|
|
||||||
```text
|
These generated files are committed so the site can be built without requiring every consumer to regenerate the wasm packages first.
|
||||||
site/decodal-site/src/wasm/
|
|
||||||
```
|
|
||||||
|
|
||||||
These generated files are committed so the site can be built without requiring every consumer to regenerate the wasm package first.
|
|
||||||
|
|
||||||
Publish the generated WebAssembly package from its package directory:
|
Publish the generated WebAssembly package from its package directory:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cd site/decodal-site/src/wasm
|
cd packages/decodal-wasm
|
||||||
npm publish
|
npm publish
|
||||||
npx jsr publish
|
npx jsr publish
|
||||||
```
|
```
|
||||||
@@ -109,7 +107,7 @@ npx jsr publish --dry-run
|
|||||||
|
|
||||||
The npm package name is `decodal-wasm`.
|
The npm package name is `decodal-wasm`.
|
||||||
The JSR package name is `@hare/decodal-wasm`.
|
The JSR package name is `@hare/decodal-wasm`.
|
||||||
JSR currently expects a single SPDX license identifier in `jsr.json`; the WebAssembly package metadata uses `MIT` while the Rust workspace remains dual licensed as `MIT OR Apache-2.0`.
|
The npm package advertises `MIT OR Apache-2.0`; JSR metadata uses `MIT` because its publisher requires a single recognized license identifier. Both license files are included in the package.
|
||||||
|
|
||||||
The playground editor uses the `decodal-codemirror` package with the generated Lezer parser in `packages/decodal-codemirror/src/decodal-parser.js`.
|
The playground editor uses the `decodal-codemirror` package with the generated Lezer parser in `packages/decodal-codemirror/src/decodal-parser.js`.
|
||||||
The canonical grammar is documented in `doc/manual/souce/language/grammar.md`; regenerate the Lezer parser when that grammar or `editors/lezer-decodal/decodal.grammar` changes.
|
The canonical grammar is documented in `doc/manual/souce/language/grammar.md`; regenerate the Lezer parser when that grammar or `editors/lezer-decodal/decodal.grammar` changes.
|
||||||
@@ -133,7 +131,7 @@ npx jsr publish --dry-run
|
|||||||
|
|
||||||
The npm package name is `decodal-codemirror`.
|
The npm package name is `decodal-codemirror`.
|
||||||
The JSR package name is `@hare/decodal-codemirror`.
|
The JSR package name is `@hare/decodal-codemirror`.
|
||||||
JSR currently expects a single SPDX license identifier in `jsr.json`; the CodeMirror package metadata uses `MIT` while the Rust workspace remains dual licensed as `MIT OR Apache-2.0`.
|
As with `decodal-wasm`, JSR metadata uses `MIT` while both the MIT and Apache-2.0 license files remain included.
|
||||||
|
|
||||||
The documentation build still uses the lightweight JavaScript fallback highlighter so Astro can render Markdown without initializing WASM at build time.
|
The documentation build still uses the lightweight JavaScript fallback highlighter so Astro can render Markdown without initializing WASM at build time.
|
||||||
|
|
||||||
@@ -162,7 +160,7 @@ CLOUDFLARE_PROJECT_NAME=my-pages-project npm run deploy
|
|||||||
If the committed WASM package must be regenerated before deploy, use:
|
If the committed WASM package must be regenerated before deploy, use:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm run deploy:wasm
|
npm run deploy:all
|
||||||
```
|
```
|
||||||
|
|
||||||
## Tree-sitter grammar
|
## Tree-sitter grammar
|
||||||
|
|||||||
@@ -26,8 +26,9 @@ Decodal は Deferred Constraint Data Language、略称 DCDL のプロジェク
|
|||||||
9. [Match](./language/expression/match.md)
|
9. [Match](./language/expression/match.md)
|
||||||
10. [Import](./language/expression/import.md)
|
10. [Import](./language/expression/import.md)
|
||||||
11. [Composition](./language/expression/composition.md)
|
11. [Composition](./language/expression/composition.md)
|
||||||
12. [Default](./language/expression/default.md)
|
12. [Range Refinement](./language/expression/ascription.md)
|
||||||
13. [String Interpolation](./language/expression/string-interpolation.md)
|
13. [Default](./language/expression/default.md)
|
||||||
|
14. [String Interpolation](./language/expression/string-interpolation.md)
|
||||||
4. [Constraints and Defaults](./language/constraints-and-defaults.md)
|
4. [Constraints and Defaults](./language/constraints-and-defaults.md)
|
||||||
5. [Composition Operators](./language/operators.md)
|
5. [Composition Operators](./language/operators.md)
|
||||||
6. [Functions](./language/functions.md)
|
6. [Functions](./language/functions.md)
|
||||||
|
|||||||
@@ -82,12 +82,27 @@ Int & >= 10 & <= 10 # OK
|
|||||||
最小の組み込み制約は以下である。
|
最小の組み込み制約は以下である。
|
||||||
|
|
||||||
```dcdl
|
```dcdl
|
||||||
|
Unknown
|
||||||
String
|
String
|
||||||
Int
|
Int
|
||||||
Float
|
Float
|
||||||
Bool
|
Bool
|
||||||
```
|
```
|
||||||
|
|
||||||
|
`Unknown` はすべてのDecodal値を含む最上位rangeである。検査を無効化する `Any` ではなく、具体的な値またはより狭いrangeがまだ決まっていないことを表す。
|
||||||
|
|
||||||
|
```dcdl
|
||||||
|
Int & Unknown # Int
|
||||||
|
42 as Unknown # 42
|
||||||
|
Unknown as Int # エラー
|
||||||
|
```
|
||||||
|
|
||||||
|
`Unknown` 自体は具体値を持たないためmaterializeできない。defaultを与えるか、具体値で絞り込む必要がある。
|
||||||
|
|
||||||
|
```dcdl
|
||||||
|
Unknown default {} # {}
|
||||||
|
```
|
||||||
|
|
||||||
追加の述語制約はライブラリまたは組み込みとして提供できる。
|
追加の述語制約はライブラリまたは組み込みとして提供できる。
|
||||||
|
|
||||||
```dcdl
|
```dcdl
|
||||||
@@ -124,6 +139,43 @@ String & /^a$/ & /^b$/
|
|||||||
Host = IPv4Address;
|
Host = IPv4Address;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 配列制約
|
||||||
|
|
||||||
|
配列制約には要素制約が必須であり、`[...T]` と書く。
|
||||||
|
|
||||||
|
```dcdl
|
||||||
|
Names = [...String];
|
||||||
|
PositiveInts = [...(Int & > 0)];
|
||||||
|
```
|
||||||
|
|
||||||
|
複数の配列制約を `&` で合成した場合、各 concrete 要素をすべての要素 range で絞り込む。
|
||||||
|
要素が object range の場合、右辺にしかない field は default の有無にかかわらず abstract のまま各要素へ残る。
|
||||||
|
左辺にしかない field は右辺の field domain 外なのでエラーになる。
|
||||||
|
要素制約のない `Array` primitive type は存在しない。
|
||||||
|
|
||||||
|
## 連想配列制約
|
||||||
|
|
||||||
|
連想配列制約は `{...T}` と書き、object の任意の field value を `T` に対して絞り込む。
|
||||||
|
|
||||||
|
```dcdl
|
||||||
|
Services = {...{
|
||||||
|
port = Int;
|
||||||
|
enabled = Bool default true;
|
||||||
|
}};
|
||||||
|
```
|
||||||
|
|
||||||
|
key は schema で列挙せず、空 object も許容する。
|
||||||
|
固定 object field と任意 key の value constraint は、末尾の `...T` で混在できる。
|
||||||
|
|
||||||
|
```dcdl
|
||||||
|
{
|
||||||
|
enabled = Bool default true;
|
||||||
|
...Unknown
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
このrest constraintは明示されていないfieldだけに適用され、field自体は生成しない。
|
||||||
|
|
||||||
## default
|
## default
|
||||||
|
|
||||||
`default` は制約ではない。
|
`default` は制約ではない。
|
||||||
|
|||||||
@@ -31,6 +31,40 @@ disabled_config = NewConfig & {
|
|||||||
enabled_config = NewConfig;
|
enabled_config = NewConfig;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 配列スキーマ
|
||||||
|
|
||||||
|
```dcdl
|
||||||
|
Services = [...{
|
||||||
|
name = String;
|
||||||
|
port = Int default 8080;
|
||||||
|
}];
|
||||||
|
|
||||||
|
[
|
||||||
|
{ name = "api"; },
|
||||||
|
{ name = "worker"; port = 9000; },
|
||||||
|
] as Services
|
||||||
|
```
|
||||||
|
|
||||||
|
抽象配列には要素制約が必須である。
|
||||||
|
この例では、1 番目の要素の `port` は `8080` に materialize される。
|
||||||
|
|
||||||
|
## 連想配列と範囲絞り込み
|
||||||
|
|
||||||
|
```dcdl
|
||||||
|
Service = {
|
||||||
|
port = Int;
|
||||||
|
enabled = Bool default true;
|
||||||
|
};
|
||||||
|
|
||||||
|
services = {
|
||||||
|
api = { port = 8080; };
|
||||||
|
worker = { port = 8081; enabled = false; };
|
||||||
|
} as {...Service};
|
||||||
|
```
|
||||||
|
|
||||||
|
`api` と `worker` は任意の key であり、それぞれの value は `Service` より狭い範囲へ絞り込まれる。
|
||||||
|
`as` の結果では `api.enabled` は abstract のまま残り、最終的に結果全体を materialize した時点で default の `true` が使われる。
|
||||||
|
|
||||||
## 関数と制約
|
## 関数と制約
|
||||||
|
|
||||||
```dcdl
|
```dcdl
|
||||||
|
|||||||
@@ -7,6 +7,50 @@ array expression は、順序付きの値の列を表す。
|
|||||||
["a", "b", "c"]
|
["a", "b", "c"]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
配列リテラルは concrete value であり、要素型を揃える必要はない。
|
||||||
|
|
||||||
|
```dcdl
|
||||||
|
["api", 8080, true]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Array constraint
|
||||||
|
|
||||||
|
配列制約は `[` の直後に ellipsis を置き、その後へ要素制約を記述する。
|
||||||
|
|
||||||
|
```dcdl
|
||||||
|
Names = [...String];
|
||||||
|
Ports = [...(Int & >= 1 & <= 65535)];
|
||||||
|
```
|
||||||
|
|
||||||
|
`[...T]` は、すべての要素が `T` を満たす長さ 0 以上の配列を表す。
|
||||||
|
空配列は任意の配列制約を満たす。
|
||||||
|
|
||||||
|
```dcdl
|
||||||
|
[...String] & []
|
||||||
|
[...String] & ["api", "worker"]
|
||||||
|
```
|
||||||
|
|
||||||
|
要素制約は object range にもできる。
|
||||||
|
右辺だけにある default 付き field は、配列へ制約を合成した時点では abstract のまま各要素に残る。
|
||||||
|
その配列を後から materialize した場合にだけ default が使われる。
|
||||||
|
|
||||||
|
```dcdl
|
||||||
|
Services = [...{
|
||||||
|
name = String;
|
||||||
|
enabled = Bool default true;
|
||||||
|
}];
|
||||||
|
|
||||||
|
Services & [{ name = "api"; }]
|
||||||
|
```
|
||||||
|
|
||||||
|
配列制約を concrete array に適用すると、各要素は要素 range に対して `as` と同じ規則で絞り込まれる。
|
||||||
|
object の要素 range では左辺にしかない field がエラーになり、右辺にしかない field は abstract のまま残る。
|
||||||
|
|
||||||
|
要素制約のない抽象配列型は提供しない。
|
||||||
|
旧来の `Array` primitive type は使用できず、`[...T]` の `T` は必須である。
|
||||||
|
`[String]` は配列制約ではなく、未解決の `String` 制約を 1 要素に持つ concrete array になる。
|
||||||
|
長さ制約、位置別の tuple 制約、unique 制約は現在サポートしない。
|
||||||
|
|
||||||
## Array concat
|
## Array concat
|
||||||
|
|
||||||
`++` は concrete array 同士を連結する。
|
`++` は concrete array 同士を連結する。
|
||||||
|
|||||||
@@ -0,0 +1,75 @@
|
|||||||
|
# Range Refinement
|
||||||
|
|
||||||
|
`narrower as wider` は、左辺が右辺より具体的で狭い範囲であることを確認し、両者を非対称に合成する演算である。
|
||||||
|
|
||||||
|
```dcdl
|
||||||
|
Int & > 10 as Int & > 0
|
||||||
|
```
|
||||||
|
|
||||||
|
この例は成功し、結果は左辺の狭い範囲 `Int & > 10` のままになる。逆向きの `Int as Int & > 0` は、左辺が右辺を満たすとは限らないため失敗する。
|
||||||
|
|
||||||
|
## Object
|
||||||
|
|
||||||
|
object では、右辺を field domain として左辺の field を確認する。
|
||||||
|
|
||||||
|
- 左辺にしかない field は、右辺の domain 外なのでエラーになる。
|
||||||
|
- 両側にある field は、左辺が右辺より狭いかを再帰的に確認し、左辺の狭い結果を使う。
|
||||||
|
- 右辺にしかない field は、default の有無に関係なく未評価のまま結果へ残す。
|
||||||
|
- 結果の field order は右辺の順序に従う。
|
||||||
|
|
||||||
|
```dcdl
|
||||||
|
partial = {
|
||||||
|
port = 8080;
|
||||||
|
} as {
|
||||||
|
host = String;
|
||||||
|
port = Int;
|
||||||
|
enabled = Bool default true;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
`partial.port` は `8080` に具体化される。`partial.host` と `partial.enabled` は右辺由来の abstract field のままであり、`as` は `enabled` の default を選択も force もしない。
|
||||||
|
|
||||||
|
その後 `partial` 全体を materialize すれば、通常の materialize 規則が適用される。この例では unresolved な `host` がエラーになり、`host` も具体化されていれば `enabled` の default がその時点で利用される。
|
||||||
|
|
||||||
|
## Default
|
||||||
|
|
||||||
|
default は範囲包含の根拠ではない。両側に同じ field がある場合、結果には左辺を使うため、右辺の default を左辺へ注入しない。
|
||||||
|
|
||||||
|
```dcdl
|
||||||
|
Int & > 0 as (Int default 1)
|
||||||
|
```
|
||||||
|
|
||||||
|
結果は default のない `Int & > 0` である。一方、右辺だけに残る object field は field 全体を保持するため、その abstract value が元から持つ default も保持される。
|
||||||
|
|
||||||
|
## Map and array ranges
|
||||||
|
|
||||||
|
`{...T}` は任意の object key を許可し、各 value が `T` より狭いことを確認する。
|
||||||
|
|
||||||
|
```dcdl
|
||||||
|
ports = {
|
||||||
|
http = 80;
|
||||||
|
https = 443;
|
||||||
|
} as {...(Int & >= 1 & <= 65535)};
|
||||||
|
```
|
||||||
|
|
||||||
|
`[...T]` を右辺に使う場合も、すべての array element を `T` に対して絞り込む。
|
||||||
|
|
||||||
|
## Concrete right-hand ranges
|
||||||
|
|
||||||
|
primitive constraint や合成 constraint は通常どおり値を検証する。
|
||||||
|
右辺が concrete scalar または array literal の場合は、左辺も同じ値または同じ長さ・要素構造である必要がある。
|
||||||
|
function は右辺の範囲として使用できない。
|
||||||
|
|
||||||
|
左辺は concrete value に限らない。処理系が包含を確認できる constraint 同士であれば abstract value も使用できる。primitive type、numeric bound、同一 regex / predicate、array/map の要素範囲は包含確認の対象になる。
|
||||||
|
|
||||||
|
関数 parameter の `name: range` も、引数が force された時点で `as` と同じ絞り込み規則を使う。
|
||||||
|
|
||||||
|
## Precedence
|
||||||
|
|
||||||
|
`as` は `default` より低い、最も低い優先順位を持ち、左結合である。
|
||||||
|
|
||||||
|
```dcdl
|
||||||
|
narrow & overrides as Wider
|
||||||
|
```
|
||||||
|
|
||||||
|
これは `(narrow & overrides) as Wider` と解釈される。
|
||||||
@@ -11,6 +11,9 @@ Port = Int & >= 1 & <= 65535;
|
|||||||
Config = MyConfig & { port = 8000; };
|
Config = MyConfig & { port = 8000; };
|
||||||
```
|
```
|
||||||
|
|
||||||
|
object 同士では片側だけにある field も保持するため、`&` は方向を持たない。
|
||||||
|
左辺が右辺より狭いことを確認し、右辺を field domain として合成したい場合は [`as`](./ascription.md) を使う。
|
||||||
|
|
||||||
## `//`
|
## `//`
|
||||||
|
|
||||||
`//` は右辺優先の構造的 patch を行う。
|
`//` は右辺優先の構造的 patch を行う。
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ increment(41)
|
|||||||
|
|
||||||
引数は thunk として渡せる。
|
引数は thunk として渡せる。
|
||||||
関数本体内で引数が参照されたときに評価する。
|
関数本体内で引数が参照されたときに評価する。
|
||||||
|
parameter に range が指定されている場合、引数は `narrower as wider` と同じ規則で絞り込まれる。
|
||||||
|
parameter 側だけにある field は abstract のまま残り、default はこの時点では選択されない。
|
||||||
|
|
||||||
関数呼び出し結果そのものはグローバルには memoize しない。
|
関数呼び出し結果そのものはグローバルには memoize しない。
|
||||||
フィールドに束縛された呼び出し結果は、そのフィールド thunk の評価結果として memoize される。
|
フィールドに束縛された呼び出し結果は、そのフィールド thunk の評価結果として memoize される。
|
||||||
|
|||||||
@@ -10,13 +10,16 @@ Expr
|
|||||||
├─ identifier
|
├─ identifier
|
||||||
├─ path reference
|
├─ path reference
|
||||||
├─ object
|
├─ object
|
||||||
|
├─ map constraint
|
||||||
├─ array
|
├─ array
|
||||||
|
├─ array constraint
|
||||||
├─ function
|
├─ function
|
||||||
├─ function call
|
├─ function call
|
||||||
├─ let
|
├─ let
|
||||||
├─ match
|
├─ match
|
||||||
├─ import
|
├─ import
|
||||||
├─ composition
|
├─ composition
|
||||||
|
├─ range refinement (`as`)
|
||||||
├─ default
|
├─ default
|
||||||
└─ string interpolation
|
└─ string interpolation
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -37,3 +37,52 @@ MyConfig = {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Map constraint
|
||||||
|
|
||||||
|
連想配列の制約は、object の任意の field value に同じ schema を適用する。
|
||||||
|
|
||||||
|
```dcdl
|
||||||
|
Services = {...{
|
||||||
|
port = Int;
|
||||||
|
enabled = Bool default true;
|
||||||
|
}};
|
||||||
|
```
|
||||||
|
|
||||||
|
`{...T}` は key の集合を固定せず、すべての value が `T` を満たす object を表す。
|
||||||
|
空 object も許容される。runtime と materialize 後の表現は通常の object と共通であり、別の map value variant は持たない。
|
||||||
|
|
||||||
|
```dcdl
|
||||||
|
services = {
|
||||||
|
api = { port = 8080; };
|
||||||
|
worker = { port = 8081; enabled = false; };
|
||||||
|
} as Services;
|
||||||
|
```
|
||||||
|
|
||||||
|
各 entry は `as` によって右辺の field domain 内へ絞り込まれるため、左辺に `port` や `enabled` 以外の field があればエラーになる。
|
||||||
|
右辺にしかない field は default の有無にかかわらず abstract のまま残る。
|
||||||
|
host から渡した object は識別子構文に収まらない文字列 key も保持できるが、DCDL source の object field name は通常の識別子に限られる。
|
||||||
|
|
||||||
|
## Object rest constraint
|
||||||
|
|
||||||
|
固定 field と任意 key の value range は、一つの object に混在できる。
|
||||||
|
|
||||||
|
```dcdl
|
||||||
|
OpenConfig = {
|
||||||
|
enabled = Bool default true;
|
||||||
|
...Unknown
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
`...T` は明示されていない残余 field にだけ適用する。明示 field にはそれぞれの field range を適用し、rest range を重ねて適用しない。
|
||||||
|
rest constraint は field を生成せず、materialize 時には実際に存在する追加 field だけを出力する。
|
||||||
|
|
||||||
|
```dcdl
|
||||||
|
{
|
||||||
|
enabled = false;
|
||||||
|
plugin = { name = "cache"; };
|
||||||
|
} as OpenConfig
|
||||||
|
```
|
||||||
|
|
||||||
|
`...T` は object の末尾に一つだけ書ける。省略した object は閉じており、`as` の左辺に未宣言 field があればエラーになる。
|
||||||
|
名前付き field を持たない `{...T}` は従来どおり抽象的な map constraint であり、単独でmaterializeするにはdefaultまたは具体値が必要になる。
|
||||||
|
|||||||
@@ -13,4 +13,4 @@ config.feature_hoge.enable
|
|||||||
参照先 field は必要になるまで評価されない。
|
参照先 field は必要になるまで評価されない。
|
||||||
|
|
||||||
存在しない field への参照は diagnostic になる。
|
存在しない field への参照は diagnostic になる。
|
||||||
Decodal は unknown / Any のような値を伝播せず、field の有無を曖昧にしない。
|
明示的な `Unknown` range は存在しないfieldを表さないため、fieldの有無を曖昧にはしない。
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ in
|
|||||||
- 関数はレキシカルスコープを持つ。
|
- 関数はレキシカルスコープを持つ。
|
||||||
- 関数は定義時の環境を参照として保持する。
|
- 関数は定義時の環境を参照として保持する。
|
||||||
- 引数は thunk として渡され、必要になるまで評価されない。
|
- 引数は thunk として渡され、必要になるまで評価されない。
|
||||||
|
- parameter range がある引数は、force 時に `as` と同じ範囲包含・絞り込み規則で合成される。
|
||||||
- 関数呼び出し結果そのものはグローバルには memoize しない。
|
- 関数呼び出し結果そのものはグローバルには memoize しない。
|
||||||
- フィールドに束縛された関数呼び出し結果は、そのフィールド thunk の評価結果として memoize される。
|
- フィールドに束縛された関数呼び出し結果は、そのフィールド thunk の評価結果として memoize される。
|
||||||
- 再帰的な依存は thunk cycle として diagnostic になる。
|
- 再帰的な依存は thunk cycle として diagnostic になる。
|
||||||
|
|||||||
@@ -35,8 +35,9 @@ module = { statement } ;
|
|||||||
statement = field_definition , [ ";" ]
|
statement = field_definition , [ ";" ]
|
||||||
| expression , [ ";" ] ;
|
| expression , [ ";" ] ;
|
||||||
|
|
||||||
expression = default_expression ;
|
expression = as_expression ;
|
||||||
|
|
||||||
|
as_expression = default_expression , { "as" , default_expression } ;
|
||||||
default_expression = patch_expression , [ "default" , default_expression ] ;
|
default_expression = patch_expression , [ "default" , default_expression ] ;
|
||||||
patch_expression = compose_expression , { "//" , compose_expression } ;
|
patch_expression = compose_expression , { "//" , compose_expression } ;
|
||||||
compose_expression = logical_or_expression , { "&" , logical_or_expression } ;
|
compose_expression = logical_or_expression , { "&" , logical_or_expression } ;
|
||||||
@@ -56,7 +57,9 @@ path_suffix = "." , identifier ;
|
|||||||
primary_expression = literal
|
primary_expression = literal
|
||||||
| identifier
|
| identifier
|
||||||
| comparison_constraint
|
| comparison_constraint
|
||||||
|
| map_constraint
|
||||||
| object
|
| object
|
||||||
|
| array_constraint
|
||||||
| array
|
| array
|
||||||
| let_expression
|
| let_expression
|
||||||
| function_expression
|
| function_expression
|
||||||
@@ -68,11 +71,15 @@ literal = string | integer | float | "true" | "false" | regex ;
|
|||||||
comparison_operator = "==" | "!=" | "<" | "<=" | ">" | ">=" ;
|
comparison_operator = "==" | "!=" | "<" | "<=" | ">" | ">=" ;
|
||||||
comparison_constraint = ( "<" | "<=" | ">" | ">=" ) , expression ;
|
comparison_constraint = ( "<" | "<=" | ">" | ">=" ) , expression ;
|
||||||
|
|
||||||
object = "{" , [ field_definition , { ";" , field_definition } , [ ";" ] ] , "}" ;
|
object = "{" , [ field_definition , { ";" , field_definition }
|
||||||
|
, [ ";" , object_rest ] , [ ";" ] ] , "}" ;
|
||||||
|
object_rest = "..." , expression ;
|
||||||
|
map_constraint = "{" , "..." , expression , "}" ;
|
||||||
field_definition = field_path , "=" , expression ;
|
field_definition = field_path , "=" , expression ;
|
||||||
field_path = identifier , { "." , identifier } ;
|
field_path = identifier , { "." , identifier } ;
|
||||||
|
|
||||||
array = "[" , [ expression , { "," , expression } , [ "," ] ] , "]" ;
|
array = "[" , [ expression , { "," , expression } , [ "," ] ] , "]" ;
|
||||||
|
array_constraint = "[" , "..." , expression , [ "," ] , "]" ;
|
||||||
|
|
||||||
let_expression = "let" , { field_definition , ";" } , "in" , expression ;
|
let_expression = "let" , { field_definition , ";" } , "in" , expression ;
|
||||||
function_expression = "(" , [ parameter_list ] , ")" , "=>" , expression ;
|
function_expression = "(" , [ parameter_list ] , ")" , "=>" , expression ;
|
||||||
@@ -102,6 +109,7 @@ Precedence is highest first.
|
|||||||
9. `&`
|
9. `&`
|
||||||
10. `//`
|
10. `//`
|
||||||
11. `default`
|
11. `default`
|
||||||
|
12. `as`
|
||||||
|
|
||||||
Binary operators are left-associative except `default`, which is right-associative.
|
Binary operators are left-associative except `default`, which is right-associative.
|
||||||
|
|
||||||
|
|||||||
@@ -32,15 +32,77 @@ result = schema;
|
|||||||
通常の object literal 内の field は、その object 内の sibling field を暗黙には識別子として参照できない。
|
通常の object literal 内の field は、その object 内の sibling field を暗黙には識別子として参照できない。
|
||||||
object 内の値を参照する場合は、外側で束縛された値や明示的な path reference を使う。
|
object 内の値を参照する場合は、外側で束縛された値や明示的な path reference を使う。
|
||||||
|
|
||||||
## SourceLoader
|
## ImportLoader
|
||||||
|
|
||||||
`import` specifier の解決は処理系 core ではなく host 側の `SourceLoader` が行う。
|
`import` specifier の解決は処理系 core ではなく host 側の `ImportLoader` が行う。
|
||||||
CLI では、specifier を現在の module path からの相対 path として解決する。
|
CLI では、specifier を現在の module path からの相対 path として解決する。
|
||||||
組み込み利用では、resource table や static source map など、filesystem 以外の loader を使える。
|
組み込み利用では、resource table や static source map など、filesystem 以外の loader を使える。
|
||||||
|
|
||||||
module cache の key は loader が返す安定 key を使う。
|
module cache の key は loader が返す安定 key を使う。
|
||||||
CLI では canonical path を key とする。
|
CLI では canonical path を key とする。
|
||||||
|
|
||||||
|
### 構造化 import
|
||||||
|
|
||||||
|
`ImportLoader::load` は DCDL source または host が構築した `Value` を import 結果として返す。
|
||||||
|
Markdown、JSON、TOML などの解釈規則は core に固定せず、loader がファイル種別を判定して構造化する。
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use decodal::{Value, ImportLoader, LoadedImport};
|
||||||
|
|
||||||
|
struct ContentLoader;
|
||||||
|
|
||||||
|
impl ImportLoader for ContentLoader {
|
||||||
|
fn load(
|
||||||
|
&mut self,
|
||||||
|
current_key: Option<&str>,
|
||||||
|
specifier: &str,
|
||||||
|
) -> decodal::Result<LoadedImport> {
|
||||||
|
if specifier.ends_with(".md") {
|
||||||
|
let markdown = read_content(current_key, specifier)?;
|
||||||
|
let parsed = parse_frontmatter(&markdown)?;
|
||||||
|
return Ok(LoadedImport::value(
|
||||||
|
parsed.key,
|
||||||
|
Value::object([
|
||||||
|
("frontmatter", parsed.frontmatter),
|
||||||
|
("body", Value::string(parsed.body)),
|
||||||
|
]),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let source = read_dcdl(current_key, specifier)?;
|
||||||
|
Ok(LoadedImport::source(
|
||||||
|
source.key,
|
||||||
|
specifier,
|
||||||
|
source.text,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`read_content` と `parse_frontmatter` は host 独自の処理であり、Decodal core は Markdown や YAML parser に依存しない。
|
||||||
|
上の loader を使うと、DCDL 側から次のように扱える。
|
||||||
|
|
||||||
|
```dcdl
|
||||||
|
Post = {
|
||||||
|
frontmatter = {
|
||||||
|
title = String;
|
||||||
|
draft = Bool default false;
|
||||||
|
};
|
||||||
|
body = String;
|
||||||
|
};
|
||||||
|
|
||||||
|
post = Post & import "./hello.md";
|
||||||
|
title = post.frontmatter.title;
|
||||||
|
body = post.body;
|
||||||
|
```
|
||||||
|
|
||||||
|
`LoadedImport::Value` は通常の concrete runtime value に internalize される。
|
||||||
|
そのため、path reference、object composition、constraint validation、materialize は source 由来の値と同じ規則を使う。
|
||||||
|
安定した `key` が同じ構造化 import は、engine 内で同じ値としてキャッシュされる。
|
||||||
|
|
||||||
|
`load` が唯一の import hook である。
|
||||||
|
loader は拡張子、media type、または host 独自の規則で振り分け、対応する `LoadedImport` variant を直接返す。
|
||||||
|
|
||||||
## 循環 import
|
## 循環 import
|
||||||
|
|
||||||
モジュール間に循環参照があっても、必要なフィールドの依存関係が循環していなければ評価できる。
|
モジュール間に循環参照があっても、必要なフィールドの依存関係が循環していなければ評価できる。
|
||||||
@@ -93,4 +155,5 @@ Module func
|
|||||||
- ファイルが読めない。
|
- ファイルが読めない。
|
||||||
- import 先の構文解析に失敗する。
|
- import 先の構文解析に失敗する。
|
||||||
- import 先の評価で必要な値がエラーになる。
|
- import 先の評価で必要な値がエラーになる。
|
||||||
|
- host による非 DCDL content の読み込みまたは構造化に失敗する。
|
||||||
- 実装が禁止する import 循環に該当する。
|
- 実装が禁止する import 循環に該当する。
|
||||||
|
|||||||
@@ -30,6 +30,7 @@
|
|||||||
| `&` | `lhs & rhs` | composition | value / constraint / object | constraint-preserving composition |
|
| `&` | `lhs & rhs` | composition | value / constraint / object | constraint-preserving composition |
|
||||||
| `//` | `lhs // rhs` | patch | object / value | right-biased structural patch |
|
| `//` | `lhs // rhs` | patch | object / value | right-biased structural patch |
|
||||||
| `default` | `base default fallback` | default | abstract value | materialization fallback |
|
| `default` | `base default fallback` | default | abstract value | materialization fallback |
|
||||||
|
| `as` | `narrower as wider` | range refinement | value / constraint / structure | narrower result plus untouched right-only ranges |
|
||||||
|
|
||||||
`concrete scalar` は `String`、`Bool`、`Int`、`Float` を指す。
|
`concrete scalar` は `String`、`Bool`、`Int`、`Float` を指す。
|
||||||
|
|
||||||
@@ -48,6 +49,7 @@
|
|||||||
9. `&`
|
9. `&`
|
||||||
10. `//`
|
10. `//`
|
||||||
11. `default`
|
11. `default`
|
||||||
|
12. `as`
|
||||||
|
|
||||||
同じ優先順位の二項演算子は左結合である。
|
同じ優先順位の二項演算子は左結合である。
|
||||||
`default` は右結合である。
|
`default` は右結合である。
|
||||||
@@ -91,6 +93,7 @@ A & B
|
|||||||
- 両方が異なる具体値なら conflict になる。
|
- 両方が異なる具体値なら conflict になる。
|
||||||
- 両方が object なら、フィールドごとに合成する。
|
- 両方が object なら、フィールドごとに合成する。
|
||||||
- 同じフィールドが両方にある場合、そのフィールド値を `&` で合成する。
|
- 同じフィールドが両方にある場合、そのフィールド値を `&` で合成する。
|
||||||
|
- 片方にしかないフィールドは、そのまま結果に保持する。
|
||||||
- 矛盾が発生した場合はエラーになる。
|
- 矛盾が発生した場合はエラーになる。
|
||||||
|
|
||||||
例:
|
例:
|
||||||
@@ -188,12 +191,44 @@ object field 全体を特別に置き換えるための `replace(...)` 構文や
|
|||||||
|
|
||||||
object 全体を別構造にしたい場合は、patch 対象より外側で値を作り直す。
|
object 全体を別構造にしたい場合は、patch 対象より外側で値を作り直す。
|
||||||
|
|
||||||
## `&` と `//` の使い分け
|
## `as`: range refinement
|
||||||
|
|
||||||
`&` は制約を満たす具体化に使う。
|
`as` は左辺が右辺より具体的で狭い範囲であることを確認する、非対称な絞り込み演算子である。
|
||||||
|
|
||||||
```dcdl
|
```dcdl
|
||||||
ValidConfig = MyConfig & {
|
Refined = {
|
||||||
|
port = 8000;
|
||||||
|
} as {
|
||||||
|
port = Int & >= 1 & <= 65535;
|
||||||
|
host = String;
|
||||||
|
enabled = Bool default true;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
`port` は左辺の `8000` に具体化される。
|
||||||
|
右辺にしかない `host` と `enabled` は abstract のまま結果へ残る。default の有無は、右辺だけの field を保持するかどうかに影響しない。
|
||||||
|
`as` 自体は default を選択せず、後から結果を materialize した場合だけ通常の default 規則が働く。
|
||||||
|
|
||||||
|
左辺にしかない object field は右辺の field domain 外なのでエラーになる。
|
||||||
|
両側にある nested object、array constraint、map constraint は同じ規則で再帰的に絞り込む。
|
||||||
|
|
||||||
|
abstract range 同士も包含を確認できる。
|
||||||
|
|
||||||
|
```dcdl
|
||||||
|
Int & > 10 as Int & > 0 # 成功し、Int & > 10 を返す
|
||||||
|
Int as Int & > 0 # 失敗
|
||||||
|
```
|
||||||
|
|
||||||
|
`&` は対称な合成であり、片方だけにある object field を保持する。
|
||||||
|
したがって、左右に方向を持つ絞り込みを `&` で代用しない。
|
||||||
|
詳細は [Range Refinement](./expression/ascription.md) を参照する。
|
||||||
|
|
||||||
|
## `&`、`//`、`as` の使い分け
|
||||||
|
|
||||||
|
`&` は制約や部分構造を失わず、対称に合成する。
|
||||||
|
|
||||||
|
```dcdl
|
||||||
|
Combined = MyConfig & {
|
||||||
port = 8000;
|
port = 8000;
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
@@ -208,3 +243,9 @@ ModifiedSchema = MyConfig // {
|
|||||||
|
|
||||||
`//` は右辺優先の patch であり、左辺の制約を常に保持するとは限らない。
|
`//` は右辺優先の patch であり、左辺の制約を常に保持するとは限らない。
|
||||||
制約を保持したい場合は `&` を使う。
|
制約を保持したい場合は `&` を使う。
|
||||||
|
|
||||||
|
左辺が右辺より狭いことを検証しながら合成する場合は `as` を使う。
|
||||||
|
|
||||||
|
```dcdl
|
||||||
|
RefinedConfig = NarrowConfig as WideConfig;
|
||||||
|
```
|
||||||
|
|||||||
@@ -103,6 +103,7 @@ in
|
|||||||
match
|
match
|
||||||
import
|
import
|
||||||
default
|
default
|
||||||
|
as
|
||||||
true
|
true
|
||||||
false
|
false
|
||||||
```
|
```
|
||||||
@@ -119,8 +120,10 @@ false
|
|||||||
& 制約合成
|
& 制約合成
|
||||||
// patch 合成
|
// patch 合成
|
||||||
default fallback 指定
|
default fallback 指定
|
||||||
|
as 左辺から右辺への範囲包含確認と絞り込み
|
||||||
=> 関数
|
=> 関数
|
||||||
. フィールド参照 / ドットパス定義
|
. フィールド参照 / ドットパス定義
|
||||||
|
... 配列または連想配列の値制約
|
||||||
```
|
```
|
||||||
|
|
||||||
演算子の優先順位は [合成演算子](./operators.md) で定義する。
|
演算子の優先順位は [合成演算子](./operators.md) で定義する。
|
||||||
|
|||||||
@@ -9,10 +9,12 @@ name = String;
|
|||||||
retry = Int default 3;
|
retry = Int default 3;
|
||||||
ratio = Float;
|
ratio = Float;
|
||||||
enable = Bool default true;
|
enable = Bool default true;
|
||||||
tags = Array;
|
tags = [...String];
|
||||||
```
|
```
|
||||||
|
|
||||||
現在の primitive type は `String`、`Int`、`Float`、`Bool`、`Array` である。
|
現在の primitive type は `String`、`Int`、`Float`、`Bool` である。
|
||||||
|
`Unknown` はprimitive typeではなく、すべてのDecodal値を含む最上位の抽象rangeである。具体値またはdefaultがない `Unknown` はmaterializeできない。
|
||||||
|
配列は primitive type ではなく、必須の要素制約を持つ `[...T]` で表現する。
|
||||||
各型の個別仕様へのリンクは [Manual Index](../../index.md) に集約する。
|
各型の個別仕様へのリンクは [Manual Index](../../index.md) に集約する。
|
||||||
|
|
||||||
primitive type と制約合成の詳細は [制約と default](../constraints-and-defaults.md) も参照する。
|
primitive type と制約合成の詳細は [制約と default](../constraints-and-defaults.md) も参照する。
|
||||||
|
|||||||
@@ -5,7 +5,12 @@ Statement {
|
|||||||
Expression Semicolon?
|
Expression Semicolon?
|
||||||
}
|
}
|
||||||
|
|
||||||
Expression { DefaultExpression }
|
Expression { AsExpression }
|
||||||
|
|
||||||
|
AsExpression {
|
||||||
|
DefaultExpression |
|
||||||
|
AsExpression !as As DefaultExpression
|
||||||
|
}
|
||||||
|
|
||||||
DefaultExpression {
|
DefaultExpression {
|
||||||
PatchExpression |
|
PatchExpression |
|
||||||
@@ -70,7 +75,9 @@ PrimaryExpression {
|
|||||||
Literal |
|
Literal |
|
||||||
Identifier |
|
Identifier |
|
||||||
ComparisonConstraint |
|
ComparisonConstraint |
|
||||||
|
MapConstraint |
|
||||||
Object |
|
Object |
|
||||||
|
ArrayConstraint |
|
||||||
Array |
|
Array |
|
||||||
LetExpression |
|
LetExpression |
|
||||||
FunctionExpression |
|
FunctionExpression |
|
||||||
@@ -83,11 +90,18 @@ Literal { String | Integer | Float | True | False | Regex }
|
|||||||
CompareOperator { EqualEqual | BangEqual | Lt | Lte | Gt | Gte }
|
CompareOperator { EqualEqual | BangEqual | Lt | Lte | Gt | Gte }
|
||||||
ComparisonConstraint { (Lt | Lte | Gt | Gte) Expression }
|
ComparisonConstraint { (Lt | Lte | Gt | Gte) Expression }
|
||||||
|
|
||||||
Object { LBrace (FieldDefinition (Semicolon FieldDefinition)* Semicolon?)? RBrace }
|
Object {
|
||||||
|
LBrace
|
||||||
|
(FieldDefinition (Semicolon FieldDefinition)* (Semicolon ObjectRest)? Semicolon?)?
|
||||||
|
RBrace
|
||||||
|
}
|
||||||
|
ObjectRest { Ellipsis Expression }
|
||||||
|
MapConstraint { LBrace Ellipsis Expression RBrace }
|
||||||
FieldDefinition { FieldPath Equal Expression }
|
FieldDefinition { FieldPath Equal Expression }
|
||||||
FieldPath { Identifier !fieldPath (Dot Identifier)* }
|
FieldPath { Identifier !fieldPath (Dot Identifier)* }
|
||||||
|
|
||||||
Array { LBracket (Expression (Comma Expression)* Comma?)? RBracket }
|
Array { LBracket (Expression (Comma Expression)* Comma?)? RBracket }
|
||||||
|
ArrayConstraint { LBracket Ellipsis Expression Comma? RBracket }
|
||||||
|
|
||||||
LetExpression { Let FieldDefinitionList In Expression }
|
LetExpression { Let FieldDefinitionList In Expression }
|
||||||
FieldDefinitionList { (FieldDefinition Semicolon)* }
|
FieldDefinitionList { (FieldDefinition Semicolon)* }
|
||||||
@@ -102,8 +116,11 @@ Pattern { Underscore | Expression }
|
|||||||
|
|
||||||
ImportExpression { Import String }
|
ImportExpression { Import String }
|
||||||
|
|
||||||
|
As { @specialize<Identifier, "as"> }
|
||||||
|
|
||||||
@precedence {
|
@precedence {
|
||||||
fieldPath @left,
|
fieldPath @left,
|
||||||
|
as @left,
|
||||||
default @right,
|
default @right,
|
||||||
patch @left,
|
patch @left,
|
||||||
compose @left,
|
compose @left,
|
||||||
@@ -143,6 +160,7 @@ ImportExpression { Import String }
|
|||||||
Semicolon { ";" }
|
Semicolon { ";" }
|
||||||
Comma { "," }
|
Comma { "," }
|
||||||
Dot { "." }
|
Dot { "." }
|
||||||
|
Ellipsis { "..." }
|
||||||
Colon { ":" }
|
Colon { ":" }
|
||||||
Equal { "=" }
|
Equal { "=" }
|
||||||
EqualEqual { "==" }
|
EqualEqual { "==" }
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ license = "MIT"
|
|||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
keywords = ["incremental", "parsing", "tree-sitter", "decodal"]
|
keywords = ["incremental", "parsing", "tree-sitter", "decodal"]
|
||||||
categories = ["parsing", "text-editors"]
|
categories = ["parsing", "text-editors"]
|
||||||
repository = "https://github.com/tree-sitter/tree-sitter-decodal"
|
repository = "https://gitea.hareworks.net/Hare/Decodal"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
autoexamples = false
|
autoexamples = false
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ include = ["bindings/rust/*", "grammar.js", "queries/*", "src/*"]
|
|||||||
path = "bindings/rust/lib.rs"
|
path = "bindings/rust/lib.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
tree-sitter = ">=0.22.6"
|
tree-sitter = "0.22.6"
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
cc = "1.0.87"
|
cc = "1.0.87"
|
||||||
|
|||||||
@@ -158,3 +158,81 @@ Array concat
|
|||||||
right: (literal (integer))))
|
right: (literal (integer))))
|
||||||
right: (array
|
right: (array
|
||||||
(literal (integer)))))))
|
(literal (integer)))))))
|
||||||
|
|
||||||
|
==================
|
||||||
|
Array constraints
|
||||||
|
==================
|
||||||
|
{
|
||||||
|
tags = [...String];
|
||||||
|
ports = [...(Int & >= 1),];
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
(source_file
|
||||||
|
(object
|
||||||
|
(field_definition
|
||||||
|
path: (field_path (identifier))
|
||||||
|
value: (array_constraint
|
||||||
|
element: (identifier)))
|
||||||
|
(field_definition
|
||||||
|
path: (field_path (identifier))
|
||||||
|
value: (array_constraint
|
||||||
|
element: (parenthesized_expression
|
||||||
|
(binary_expression
|
||||||
|
left: (identifier)
|
||||||
|
right: (comparison_constraint
|
||||||
|
value: (integer))))))))
|
||||||
|
|
||||||
|
==================
|
||||||
|
Map constraint and ascription
|
||||||
|
==================
|
||||||
|
{
|
||||||
|
services = {
|
||||||
|
api = { port = 8080; };
|
||||||
|
} as {...{ port = Int; }};
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
(source_file
|
||||||
|
(object
|
||||||
|
(field_definition
|
||||||
|
path: (field_path (identifier))
|
||||||
|
value: (as_expression
|
||||||
|
narrower: (object
|
||||||
|
(field_definition
|
||||||
|
path: (field_path (identifier))
|
||||||
|
value: (object
|
||||||
|
(field_definition
|
||||||
|
path: (field_path (identifier))
|
||||||
|
value: (literal (integer))))))
|
||||||
|
wider: (map_constraint
|
||||||
|
value: (object
|
||||||
|
(field_definition
|
||||||
|
path: (field_path (identifier))
|
||||||
|
value: (identifier))))))))
|
||||||
|
|
||||||
|
==================
|
||||||
|
Unknown object rest constraint
|
||||||
|
==================
|
||||||
|
{
|
||||||
|
value = {
|
||||||
|
hoge = Int;
|
||||||
|
fuga = String;
|
||||||
|
...Unknown
|
||||||
|
};
|
||||||
|
}
|
||||||
|
---
|
||||||
|
|
||||||
|
(source_file
|
||||||
|
(object
|
||||||
|
(field_definition
|
||||||
|
path: (field_path (identifier))
|
||||||
|
value: (object
|
||||||
|
(field_definition
|
||||||
|
path: (field_path (identifier))
|
||||||
|
value: (identifier))
|
||||||
|
(field_definition
|
||||||
|
path: (field_path (identifier))
|
||||||
|
value: (identifier))
|
||||||
|
(object_rest
|
||||||
|
value: (identifier))))))
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// doc/manual/souce/language/grammar.md.
|
// doc/manual/souce/language/grammar.md.
|
||||||
|
|
||||||
const PREC = {
|
const PREC = {
|
||||||
|
AS: 0,
|
||||||
DEFAULT: 1,
|
DEFAULT: 1,
|
||||||
PATCH: 2,
|
PATCH: 2,
|
||||||
COMPOSE: 3,
|
COMPOSE: 3,
|
||||||
@@ -53,7 +54,9 @@ module.exports = grammar({
|
|||||||
$.identifier,
|
$.identifier,
|
||||||
$.regex_literal,
|
$.regex_literal,
|
||||||
$.comparison_constraint,
|
$.comparison_constraint,
|
||||||
|
$.map_constraint,
|
||||||
$.object,
|
$.object,
|
||||||
|
$.array_constraint,
|
||||||
$.array,
|
$.array,
|
||||||
$.let_expression,
|
$.let_expression,
|
||||||
$.function_expression,
|
$.function_expression,
|
||||||
@@ -65,6 +68,7 @@ module.exports = grammar({
|
|||||||
$.unary_expression,
|
$.unary_expression,
|
||||||
$.binary_expression,
|
$.binary_expression,
|
||||||
$.default_expression,
|
$.default_expression,
|
||||||
|
$.as_expression,
|
||||||
),
|
),
|
||||||
|
|
||||||
literal: $ => choice(
|
literal: $ => choice(
|
||||||
@@ -95,7 +99,24 @@ module.exports = grammar({
|
|||||||
|
|
||||||
object: $ => seq(
|
object: $ => seq(
|
||||||
'{',
|
'{',
|
||||||
semiSep($.field_definition),
|
optional(seq(
|
||||||
|
$.field_definition,
|
||||||
|
repeat(seq(';', $.field_definition)),
|
||||||
|
optional(seq(';', $.object_rest)),
|
||||||
|
optional(';'),
|
||||||
|
)),
|
||||||
|
'}',
|
||||||
|
),
|
||||||
|
|
||||||
|
object_rest: $ => seq(
|
||||||
|
'...',
|
||||||
|
field('value', $._expression),
|
||||||
|
),
|
||||||
|
|
||||||
|
map_constraint: $ => seq(
|
||||||
|
'{',
|
||||||
|
'...',
|
||||||
|
field('value', $._expression),
|
||||||
'}',
|
'}',
|
||||||
),
|
),
|
||||||
|
|
||||||
@@ -116,12 +137,20 @@ module.exports = grammar({
|
|||||||
']',
|
']',
|
||||||
),
|
),
|
||||||
|
|
||||||
let_expression: $ => seq(
|
array_constraint: $ => seq(
|
||||||
|
'[',
|
||||||
|
'...',
|
||||||
|
field('element', $._expression),
|
||||||
|
optional(','),
|
||||||
|
']',
|
||||||
|
),
|
||||||
|
|
||||||
|
let_expression: $ => prec.right(seq(
|
||||||
'let',
|
'let',
|
||||||
repeat(seq($.field_definition, ';')),
|
repeat(seq($.field_definition, ';')),
|
||||||
'in',
|
'in',
|
||||||
field('body', $._expression),
|
field('body', $._expression),
|
||||||
),
|
)),
|
||||||
|
|
||||||
function_expression: $ => prec.right(seq(
|
function_expression: $ => prec.right(seq(
|
||||||
'(',
|
'(',
|
||||||
@@ -228,5 +257,11 @@ module.exports = grammar({
|
|||||||
'default',
|
'default',
|
||||||
field('fallback', $._expression),
|
field('fallback', $._expression),
|
||||||
)),
|
)),
|
||||||
|
|
||||||
|
as_expression: $ => prec.left(PREC.AS, seq(
|
||||||
|
field('narrower', $._expression),
|
||||||
|
'as',
|
||||||
|
field('wider', $._expression),
|
||||||
|
)),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"match"
|
"match"
|
||||||
"import"
|
"import"
|
||||||
"default"
|
"default"
|
||||||
|
"as"
|
||||||
] @keyword
|
] @keyword
|
||||||
|
|
||||||
[
|
[
|
||||||
@@ -22,6 +23,7 @@
|
|||||||
"*"
|
"*"
|
||||||
"/"
|
"/"
|
||||||
"++"
|
"++"
|
||||||
|
"..."
|
||||||
"&&"
|
"&&"
|
||||||
"||"
|
"||"
|
||||||
"!"
|
"!"
|
||||||
@@ -52,7 +54,7 @@
|
|||||||
] @punctuation.delimiter
|
] @punctuation.delimiter
|
||||||
|
|
||||||
((identifier) @type.builtin
|
((identifier) @type.builtin
|
||||||
(#match? @type.builtin "^(String|Int|Float|Bool)$"))
|
(#match? @type.builtin "^(String|Int|Float|Bool|Unknown)$"))
|
||||||
|
|
||||||
(field_definition
|
(field_definition
|
||||||
path: (field_path (identifier) @property))
|
path: (field_path (identifier) @property))
|
||||||
|
|||||||
+145
@@ -91,10 +91,18 @@
|
|||||||
"type": "SYMBOL",
|
"type": "SYMBOL",
|
||||||
"name": "comparison_constraint"
|
"name": "comparison_constraint"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "SYMBOL",
|
||||||
|
"name": "map_constraint"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "SYMBOL",
|
"type": "SYMBOL",
|
||||||
"name": "object"
|
"name": "object"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "SYMBOL",
|
||||||
|
"name": "array_constraint"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "SYMBOL",
|
"type": "SYMBOL",
|
||||||
"name": "array"
|
"name": "array"
|
||||||
@@ -138,6 +146,10 @@
|
|||||||
{
|
{
|
||||||
"type": "SYMBOL",
|
"type": "SYMBOL",
|
||||||
"name": "default_expression"
|
"name": "default_expression"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "SYMBOL",
|
||||||
|
"name": "as_expression"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -290,6 +302,27 @@
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "CHOICE",
|
||||||
|
"members": [
|
||||||
|
{
|
||||||
|
"type": "SEQ",
|
||||||
|
"members": [
|
||||||
|
{
|
||||||
|
"type": "STRING",
|
||||||
|
"value": ";"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "SYMBOL",
|
||||||
|
"name": "object_rest"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "BLANK"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "CHOICE",
|
"type": "CHOICE",
|
||||||
"members": [
|
"members": [
|
||||||
@@ -315,6 +348,48 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"object_rest": {
|
||||||
|
"type": "SEQ",
|
||||||
|
"members": [
|
||||||
|
{
|
||||||
|
"type": "STRING",
|
||||||
|
"value": "..."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "FIELD",
|
||||||
|
"name": "value",
|
||||||
|
"content": {
|
||||||
|
"type": "SYMBOL",
|
||||||
|
"name": "_expression"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"map_constraint": {
|
||||||
|
"type": "SEQ",
|
||||||
|
"members": [
|
||||||
|
{
|
||||||
|
"type": "STRING",
|
||||||
|
"value": "{"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "STRING",
|
||||||
|
"value": "..."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "FIELD",
|
||||||
|
"name": "value",
|
||||||
|
"content": {
|
||||||
|
"type": "SYMBOL",
|
||||||
|
"name": "_expression"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "STRING",
|
||||||
|
"value": "}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"field_definition": {
|
"field_definition": {
|
||||||
"type": "SEQ",
|
"type": "SEQ",
|
||||||
"members": [
|
"members": [
|
||||||
@@ -427,7 +502,47 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"array_constraint": {
|
||||||
|
"type": "SEQ",
|
||||||
|
"members": [
|
||||||
|
{
|
||||||
|
"type": "STRING",
|
||||||
|
"value": "["
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "STRING",
|
||||||
|
"value": "..."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "FIELD",
|
||||||
|
"name": "element",
|
||||||
|
"content": {
|
||||||
|
"type": "SYMBOL",
|
||||||
|
"name": "_expression"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "CHOICE",
|
||||||
|
"members": [
|
||||||
|
{
|
||||||
|
"type": "STRING",
|
||||||
|
"value": ","
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "BLANK"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "STRING",
|
||||||
|
"value": "]"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"let_expression": {
|
"let_expression": {
|
||||||
|
"type": "PREC_RIGHT",
|
||||||
|
"value": 0,
|
||||||
|
"content": {
|
||||||
"type": "SEQ",
|
"type": "SEQ",
|
||||||
"members": [
|
"members": [
|
||||||
{
|
{
|
||||||
@@ -463,6 +578,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"function_expression": {
|
"function_expression": {
|
||||||
"type": "PREC_RIGHT",
|
"type": "PREC_RIGHT",
|
||||||
@@ -1257,6 +1373,35 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"as_expression": {
|
||||||
|
"type": "PREC_LEFT",
|
||||||
|
"value": 0,
|
||||||
|
"content": {
|
||||||
|
"type": "SEQ",
|
||||||
|
"members": [
|
||||||
|
{
|
||||||
|
"type": "FIELD",
|
||||||
|
"name": "narrower",
|
||||||
|
"content": {
|
||||||
|
"type": "SYMBOL",
|
||||||
|
"name": "_expression"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "STRING",
|
||||||
|
"value": "as"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "FIELD",
|
||||||
|
"name": "wider",
|
||||||
|
"content": {
|
||||||
|
"type": "SYMBOL",
|
||||||
|
"name": "_expression"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"extras": [
|
"extras": [
|
||||||
|
|||||||
+681
-7
@@ -11,6 +11,14 @@
|
|||||||
"type": "array",
|
"type": "array",
|
||||||
"named": true
|
"named": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "array_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "as_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "binary_expression",
|
"type": "binary_expression",
|
||||||
"named": true
|
"named": true
|
||||||
@@ -47,6 +55,10 @@
|
|||||||
"type": "literal",
|
"type": "literal",
|
||||||
"named": true
|
"named": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "map_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "match_expression",
|
"type": "match_expression",
|
||||||
"named": true
|
"named": true
|
||||||
@@ -75,10 +87,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "binary_expression",
|
"type": "array_constraint",
|
||||||
"named": true,
|
"named": true,
|
||||||
"fields": {
|
"fields": {
|
||||||
"left": {
|
"element": {
|
||||||
"multiple": false,
|
"multiple": false,
|
||||||
"required": true,
|
"required": true,
|
||||||
"types": [
|
"types": [
|
||||||
@@ -86,6 +98,14 @@
|
|||||||
"type": "array",
|
"type": "array",
|
||||||
"named": true
|
"named": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "array_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "as_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "binary_expression",
|
"type": "binary_expression",
|
||||||
"named": true
|
"named": true
|
||||||
@@ -122,6 +142,268 @@
|
|||||||
"type": "literal",
|
"type": "literal",
|
||||||
"named": true
|
"named": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "map_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "match_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "parenthesized_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "path_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex_literal",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "unary_expression",
|
||||||
|
"named": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "as_expression",
|
||||||
|
"named": true,
|
||||||
|
"fields": {
|
||||||
|
"narrower": {
|
||||||
|
"multiple": false,
|
||||||
|
"required": true,
|
||||||
|
"types": [
|
||||||
|
{
|
||||||
|
"type": "array",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "array_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "as_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "binary_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "call_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "comparison_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "default_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "function_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "identifier",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "import_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "let_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "literal",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "map_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "match_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "parenthesized_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "path_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex_literal",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "unary_expression",
|
||||||
|
"named": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"wider": {
|
||||||
|
"multiple": false,
|
||||||
|
"required": true,
|
||||||
|
"types": [
|
||||||
|
{
|
||||||
|
"type": "array",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "array_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "as_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "binary_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "call_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "comparison_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "default_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "function_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "identifier",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "import_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "let_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "literal",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "map_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "match_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "parenthesized_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "path_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex_literal",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "unary_expression",
|
||||||
|
"named": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "binary_expression",
|
||||||
|
"named": true,
|
||||||
|
"fields": {
|
||||||
|
"left": {
|
||||||
|
"multiple": false,
|
||||||
|
"required": true,
|
||||||
|
"types": [
|
||||||
|
{
|
||||||
|
"type": "array",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "array_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "as_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "binary_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "call_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "comparison_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "default_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "function_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "identifier",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "import_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "let_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "literal",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "map_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "match_expression",
|
"type": "match_expression",
|
||||||
"named": true
|
"named": true
|
||||||
@@ -222,6 +504,14 @@
|
|||||||
"type": "array",
|
"type": "array",
|
||||||
"named": true
|
"named": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "array_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "as_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "binary_expression",
|
"type": "binary_expression",
|
||||||
"named": true
|
"named": true
|
||||||
@@ -258,6 +548,10 @@
|
|||||||
"type": "literal",
|
"type": "literal",
|
||||||
"named": true
|
"named": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "map_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "match_expression",
|
"type": "match_expression",
|
||||||
"named": true
|
"named": true
|
||||||
@@ -303,6 +597,14 @@
|
|||||||
"type": "array",
|
"type": "array",
|
||||||
"named": true
|
"named": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "array_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "as_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "binary_expression",
|
"type": "binary_expression",
|
||||||
"named": true
|
"named": true
|
||||||
@@ -339,6 +641,10 @@
|
|||||||
"type": "literal",
|
"type": "literal",
|
||||||
"named": true
|
"named": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "map_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "match_expression",
|
"type": "match_expression",
|
||||||
"named": true
|
"named": true
|
||||||
@@ -374,6 +680,14 @@
|
|||||||
"type": "array",
|
"type": "array",
|
||||||
"named": true
|
"named": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "array_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "as_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "binary_expression",
|
"type": "binary_expression",
|
||||||
"named": true
|
"named": true
|
||||||
@@ -410,6 +724,10 @@
|
|||||||
"type": "literal",
|
"type": "literal",
|
||||||
"named": true
|
"named": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "map_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "match_expression",
|
"type": "match_expression",
|
||||||
"named": true
|
"named": true
|
||||||
@@ -471,6 +789,14 @@
|
|||||||
"type": "array",
|
"type": "array",
|
||||||
"named": true
|
"named": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "array_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "as_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "binary_expression",
|
"type": "binary_expression",
|
||||||
"named": true
|
"named": true
|
||||||
@@ -507,6 +833,10 @@
|
|||||||
"type": "literal",
|
"type": "literal",
|
||||||
"named": true
|
"named": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "map_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "match_expression",
|
"type": "match_expression",
|
||||||
"named": true
|
"named": true
|
||||||
@@ -547,6 +877,14 @@
|
|||||||
"type": "array",
|
"type": "array",
|
||||||
"named": true
|
"named": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "array_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "as_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "binary_expression",
|
"type": "binary_expression",
|
||||||
"named": true
|
"named": true
|
||||||
@@ -583,6 +921,10 @@
|
|||||||
"type": "literal",
|
"type": "literal",
|
||||||
"named": true
|
"named": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "map_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "match_expression",
|
"type": "match_expression",
|
||||||
"named": true
|
"named": true
|
||||||
@@ -617,6 +959,14 @@
|
|||||||
"type": "array",
|
"type": "array",
|
||||||
"named": true
|
"named": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "array_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "as_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "binary_expression",
|
"type": "binary_expression",
|
||||||
"named": true
|
"named": true
|
||||||
@@ -653,6 +1003,10 @@
|
|||||||
"type": "literal",
|
"type": "literal",
|
||||||
"named": true
|
"named": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "map_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "match_expression",
|
"type": "match_expression",
|
||||||
"named": true
|
"named": true
|
||||||
@@ -703,6 +1057,14 @@
|
|||||||
"type": "array",
|
"type": "array",
|
||||||
"named": true
|
"named": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "array_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "as_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "binary_expression",
|
"type": "binary_expression",
|
||||||
"named": true
|
"named": true
|
||||||
@@ -739,6 +1101,10 @@
|
|||||||
"type": "literal",
|
"type": "literal",
|
||||||
"named": true
|
"named": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "map_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "match_expression",
|
"type": "match_expression",
|
||||||
"named": true
|
"named": true
|
||||||
@@ -794,6 +1160,14 @@
|
|||||||
"type": "array",
|
"type": "array",
|
||||||
"named": true
|
"named": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "array_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "as_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "binary_expression",
|
"type": "binary_expression",
|
||||||
"named": true
|
"named": true
|
||||||
@@ -830,6 +1204,10 @@
|
|||||||
"type": "literal",
|
"type": "literal",
|
||||||
"named": true
|
"named": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "map_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "match_expression",
|
"type": "match_expression",
|
||||||
"named": true
|
"named": true
|
||||||
@@ -896,6 +1274,14 @@
|
|||||||
"type": "array",
|
"type": "array",
|
||||||
"named": true
|
"named": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "array_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "as_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "binary_expression",
|
"type": "binary_expression",
|
||||||
"named": true
|
"named": true
|
||||||
@@ -932,6 +1318,10 @@
|
|||||||
"type": "literal",
|
"type": "literal",
|
||||||
"named": true
|
"named": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "map_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "match_expression",
|
"type": "match_expression",
|
||||||
"named": true
|
"named": true
|
||||||
@@ -998,10 +1388,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "match_arm",
|
"type": "map_constraint",
|
||||||
"named": true,
|
"named": true,
|
||||||
"fields": {
|
"fields": {
|
||||||
"body": {
|
"value": {
|
||||||
"multiple": false,
|
"multiple": false,
|
||||||
"required": true,
|
"required": true,
|
||||||
"types": [
|
"types": [
|
||||||
@@ -1009,6 +1399,14 @@
|
|||||||
"type": "array",
|
"type": "array",
|
||||||
"named": true
|
"named": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "array_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "as_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "binary_expression",
|
"type": "binary_expression",
|
||||||
"named": true
|
"named": true
|
||||||
@@ -1045,6 +1443,98 @@
|
|||||||
"type": "literal",
|
"type": "literal",
|
||||||
"named": true
|
"named": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "map_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "match_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "parenthesized_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "path_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex_literal",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "unary_expression",
|
||||||
|
"named": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "match_arm",
|
||||||
|
"named": true,
|
||||||
|
"fields": {
|
||||||
|
"body": {
|
||||||
|
"multiple": false,
|
||||||
|
"required": true,
|
||||||
|
"types": [
|
||||||
|
{
|
||||||
|
"type": "array",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "array_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "as_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "binary_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "call_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "comparison_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "default_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "function_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "identifier",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "import_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "let_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "literal",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "map_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "match_expression",
|
"type": "match_expression",
|
||||||
"named": true
|
"named": true
|
||||||
@@ -1083,6 +1573,14 @@
|
|||||||
"type": "array",
|
"type": "array",
|
||||||
"named": true
|
"named": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "array_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "as_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "binary_expression",
|
"type": "binary_expression",
|
||||||
"named": true
|
"named": true
|
||||||
@@ -1119,6 +1617,10 @@
|
|||||||
"type": "literal",
|
"type": "literal",
|
||||||
"named": true
|
"named": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "map_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "match_expression",
|
"type": "match_expression",
|
||||||
"named": true
|
"named": true
|
||||||
@@ -1159,6 +1661,14 @@
|
|||||||
"type": "array",
|
"type": "array",
|
||||||
"named": true
|
"named": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "array_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "as_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "binary_expression",
|
"type": "binary_expression",
|
||||||
"named": true
|
"named": true
|
||||||
@@ -1195,6 +1705,10 @@
|
|||||||
"type": "literal",
|
"type": "literal",
|
||||||
"named": true
|
"named": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "map_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "match_expression",
|
"type": "match_expression",
|
||||||
"named": true
|
"named": true
|
||||||
@@ -1244,22 +1758,34 @@
|
|||||||
{
|
{
|
||||||
"type": "field_definition",
|
"type": "field_definition",
|
||||||
"named": true
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object_rest",
|
||||||
|
"named": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "parameter",
|
"type": "object_rest",
|
||||||
"named": true,
|
"named": true,
|
||||||
"fields": {
|
"fields": {
|
||||||
"constraint": {
|
"value": {
|
||||||
"multiple": false,
|
"multiple": false,
|
||||||
"required": false,
|
"required": true,
|
||||||
"types": [
|
"types": [
|
||||||
{
|
{
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"named": true
|
"named": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "array_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "as_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "binary_expression",
|
"type": "binary_expression",
|
||||||
"named": true
|
"named": true
|
||||||
@@ -1296,6 +1822,98 @@
|
|||||||
"type": "literal",
|
"type": "literal",
|
||||||
"named": true
|
"named": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "map_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "match_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "parenthesized_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "path_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "regex_literal",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "unary_expression",
|
||||||
|
"named": true
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "parameter",
|
||||||
|
"named": true,
|
||||||
|
"fields": {
|
||||||
|
"constraint": {
|
||||||
|
"multiple": false,
|
||||||
|
"required": false,
|
||||||
|
"types": [
|
||||||
|
{
|
||||||
|
"type": "array",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "array_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "as_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "binary_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "call_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "comparison_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "default_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "function_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "identifier",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "import_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "let_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "literal",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "map_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "match_expression",
|
"type": "match_expression",
|
||||||
"named": true
|
"named": true
|
||||||
@@ -1346,6 +1964,14 @@
|
|||||||
"type": "array",
|
"type": "array",
|
||||||
"named": true
|
"named": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "array_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "as_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "binary_expression",
|
"type": "binary_expression",
|
||||||
"named": true
|
"named": true
|
||||||
@@ -1382,6 +2008,10 @@
|
|||||||
"type": "literal",
|
"type": "literal",
|
||||||
"named": true
|
"named": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "map_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "match_expression",
|
"type": "match_expression",
|
||||||
"named": true
|
"named": true
|
||||||
@@ -1431,6 +2061,14 @@
|
|||||||
"type": "array",
|
"type": "array",
|
||||||
"named": true
|
"named": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "array_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "as_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "binary_expression",
|
"type": "binary_expression",
|
||||||
"named": true
|
"named": true
|
||||||
@@ -1467,6 +2105,10 @@
|
|||||||
"type": "literal",
|
"type": "literal",
|
||||||
"named": true
|
"named": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "map_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "match_expression",
|
"type": "match_expression",
|
||||||
"named": true
|
"named": true
|
||||||
@@ -1507,6 +2149,14 @@
|
|||||||
"type": "array",
|
"type": "array",
|
||||||
"named": true
|
"named": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "array_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "as_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "binary_expression",
|
"type": "binary_expression",
|
||||||
"named": true
|
"named": true
|
||||||
@@ -1547,6 +2197,10 @@
|
|||||||
"type": "literal",
|
"type": "literal",
|
||||||
"named": true
|
"named": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "map_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "match_expression",
|
"type": "match_expression",
|
||||||
"named": true
|
"named": true
|
||||||
@@ -1586,6 +2240,14 @@
|
|||||||
"type": "array",
|
"type": "array",
|
||||||
"named": true
|
"named": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "array_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "as_expression",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "binary_expression",
|
"type": "binary_expression",
|
||||||
"named": true
|
"named": true
|
||||||
@@ -1622,6 +2284,10 @@
|
|||||||
"type": "literal",
|
"type": "literal",
|
||||||
"named": true
|
"named": true
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "map_constraint",
|
||||||
|
"named": true
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "match_expression",
|
"type": "match_expression",
|
||||||
"named": true
|
"named": true
|
||||||
@@ -1712,6 +2378,10 @@
|
|||||||
"type": ".",
|
"type": ".",
|
||||||
"named": false
|
"named": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "...",
|
||||||
|
"named": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "/",
|
"type": "/",
|
||||||
"named": false
|
"named": false
|
||||||
@@ -1768,6 +2438,10 @@
|
|||||||
"type": "_",
|
"type": "_",
|
||||||
"named": false
|
"named": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "as",
|
||||||
|
"named": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "comment",
|
"type": "comment",
|
||||||
"named": true
|
"named": true
|
||||||
|
|||||||
Generated
+6873
-5130
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
|||||||
|
let
|
||||||
|
Service = {
|
||||||
|
name = String;
|
||||||
|
enabled = Bool default true;
|
||||||
|
};
|
||||||
|
Services = [...Service];
|
||||||
|
in
|
||||||
|
Services & [
|
||||||
|
{ name = "api"; },
|
||||||
|
{ name = "worker"; enabled = false; },
|
||||||
|
]
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
let
|
||||||
|
Service = {
|
||||||
|
port = Int & >= 1 & <= 65535;
|
||||||
|
enabled = Bool default true;
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
api = {
|
||||||
|
port = 8080;
|
||||||
|
};
|
||||||
|
worker = {
|
||||||
|
port = 8081;
|
||||||
|
enabled = false;
|
||||||
|
};
|
||||||
|
} as {...Service}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
OpenConfig = {
|
||||||
|
enabled = Bool default true;
|
||||||
|
...Unknown
|
||||||
|
};
|
||||||
|
|
||||||
|
config = {
|
||||||
|
enabled = false;
|
||||||
|
plugin = {
|
||||||
|
name = "cache";
|
||||||
|
};
|
||||||
|
} as OpenConfig;
|
||||||
@@ -29,6 +29,7 @@
|
|||||||
|
|
||||||
apps.default = mkApp "decodal" "Run Decodal";
|
apps.default = mkApp "decodal" "Run Decodal";
|
||||||
apps.decodal = mkApp "decodal" "Run Decodal";
|
apps.decodal = mkApp "decodal" "Run Decodal";
|
||||||
|
apps.decodal-lsp = mkApp "decodal-lsp" "Run the Decodal language server";
|
||||||
|
|
||||||
checks.default = decodal;
|
checks.default = decodal;
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -24,7 +24,7 @@ let
|
|||||||
in
|
in
|
||||||
rustPlatform.buildRustPackage {
|
rustPlatform.buildRustPackage {
|
||||||
pname = "decodal";
|
pname = "decodal";
|
||||||
version = "0.1.1";
|
version = "0.1.2";
|
||||||
|
|
||||||
src = lib.cleanSourceWith {
|
src = lib.cleanSourceWith {
|
||||||
src = srcRoot;
|
src = srcRoot;
|
||||||
@@ -38,6 +38,8 @@ rustPlatform.buildRustPackage {
|
|||||||
cargoBuildFlags = [
|
cargoBuildFlags = [
|
||||||
"-p"
|
"-p"
|
||||||
"decodal-cli"
|
"decodal-cli"
|
||||||
|
"-p"
|
||||||
|
"decodal-lsp"
|
||||||
];
|
];
|
||||||
|
|
||||||
doInstallCheck = true;
|
doInstallCheck = true;
|
||||||
@@ -45,6 +47,7 @@ rustPlatform.buildRustPackage {
|
|||||||
runHook preInstallCheck
|
runHook preInstallCheck
|
||||||
|
|
||||||
test -x "$out/bin/decodal"
|
test -x "$out/bin/decodal"
|
||||||
|
test -x "$out/bin/decodal-lsp"
|
||||||
"$out/bin/decodal" >/dev/null
|
"$out/bin/decodal" >/dev/null
|
||||||
|
|
||||||
runHook postInstallCheck
|
runHook postInstallCheck
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Decodal contributors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
CodeMirror 6 language support for Decodal.
|
CodeMirror 6 language support for Decodal.
|
||||||
|
|
||||||
This package provides the Lezer parser metadata, highlighting tags, indentation rules, and block folding used by the Decodal Web playground.
|
This package provides the Lezer parser metadata, highlighting tags, indentation rules, block folding, and formatter command used by the Decodal Web playground.
|
||||||
It does not include the Decodal runtime or WebAssembly evaluator.
|
It does not include the Decodal runtime evaluator.
|
||||||
Use `decodal-wasm` when you want to evaluate Decodal in a browser.
|
Use `decodal-wasm` when you want to evaluate Decodal in a browser.
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
@@ -52,3 +52,23 @@ decodal({ highlight: false })
|
|||||||
```
|
```
|
||||||
|
|
||||||
Use this when you want the language support without the bundled highlight style.
|
Use this when you want the language support without the bundled highlight style.
|
||||||
|
|
||||||
|
## Formatting
|
||||||
|
|
||||||
|
The formatter is exposed as a separate subpath so plain syntax highlighting does not force callers to initialize WebAssembly.
|
||||||
|
|
||||||
|
```js
|
||||||
|
import {
|
||||||
|
formatDecodal,
|
||||||
|
formatDecodalCommand,
|
||||||
|
initDecodalFormatter,
|
||||||
|
} from 'decodal-codemirror/format';
|
||||||
|
import formatterWasm from 'decodal-codemirror/wasm/decodal_language_tools_bg.wasm?url';
|
||||||
|
|
||||||
|
await initDecodalFormatter(formatterWasm);
|
||||||
|
|
||||||
|
const result = formatDecodal('Server={port=Int default 8080;};');
|
||||||
|
if (result.ok) console.log(result.source);
|
||||||
|
|
||||||
|
formatDecodalCommand(view);
|
||||||
|
```
|
||||||
|
|||||||
@@ -1,23 +1,37 @@
|
|||||||
{
|
{
|
||||||
"name": "@hare/decodal-codemirror",
|
"name": "@hare/decodal-codemirror",
|
||||||
"version": "0.1.2",
|
"version": "0.3.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"exports": "./src/mod.ts",
|
"exports": {
|
||||||
|
".": "./src/mod.ts",
|
||||||
|
"./format": "./src/format.ts"
|
||||||
|
},
|
||||||
"imports": {
|
"imports": {
|
||||||
"@codemirror/language": "npm:@codemirror/language@^6.12.4",
|
"@codemirror/language": "npm:@codemirror/language@^6.12.4",
|
||||||
|
"@codemirror/view": "npm:@codemirror/view@^6.43.6",
|
||||||
"@lezer/highlight": "npm:@lezer/highlight@^1.2.3",
|
"@lezer/highlight": "npm:@lezer/highlight@^1.2.3",
|
||||||
"@lezer/lr": "npm:@lezer/lr@^1.4.10"
|
"@lezer/lr": "npm:@lezer/lr@^1.4.10"
|
||||||
},
|
},
|
||||||
"publish": {
|
"publish": {
|
||||||
"include": [
|
"include": [
|
||||||
"README.md",
|
"README.md",
|
||||||
|
"LICENSE-MIT",
|
||||||
|
"LICENSE-APACHE",
|
||||||
"src/mod.ts",
|
"src/mod.ts",
|
||||||
|
"src/format.ts",
|
||||||
"src/decodal.js",
|
"src/decodal.js",
|
||||||
"src/decodal.d.ts",
|
"src/decodal.d.ts",
|
||||||
"src/decodal.js.d.ts",
|
"src/decodal.js.d.ts",
|
||||||
|
"src/format.js",
|
||||||
|
"src/format.d.ts",
|
||||||
|
"src/format.js.d.ts",
|
||||||
"src/decodal-parser-jsr.js",
|
"src/decodal-parser-jsr.js",
|
||||||
"src/decodal-parser.js",
|
"src/decodal-parser.js",
|
||||||
"src/decodal-parser.terms.js",
|
"src/decodal-parser.terms.js",
|
||||||
|
"wasm/decodal_language_tools.js",
|
||||||
|
"wasm/decodal_language_tools.d.ts",
|
||||||
|
"wasm/decodal_language_tools_bg.wasm",
|
||||||
|
"wasm/decodal_language_tools_bg.wasm.d.ts",
|
||||||
"package.json"
|
"package.json"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-3
@@ -1,15 +1,23 @@
|
|||||||
{
|
{
|
||||||
"name": "decodal-codemirror",
|
"name": "decodal-codemirror",
|
||||||
"version": "0.1.2",
|
"version": "0.3.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "decodal-codemirror",
|
"name": "decodal-codemirror",
|
||||||
"version": "0.1.2",
|
"version": "0.3.0",
|
||||||
"license": "MIT OR Apache-2.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"dependencies": {
|
"devDependencies": {
|
||||||
"@codemirror/language": "^6.12.4",
|
"@codemirror/language": "^6.12.4",
|
||||||
|
"@codemirror/state": "^6.7.1",
|
||||||
|
"@codemirror/view": "^6.43.6",
|
||||||
|
"@lezer/highlight": "^1.2.3",
|
||||||
|
"@lezer/lr": "^1.4.10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@codemirror/language": "^6.12.4",
|
||||||
|
"@codemirror/view": "^6.43.6",
|
||||||
"@lezer/highlight": "^1.2.3",
|
"@lezer/highlight": "^1.2.3",
|
||||||
"@lezer/lr": "^1.4.10"
|
"@lezer/lr": "^1.4.10"
|
||||||
}
|
}
|
||||||
@@ -18,6 +26,7 @@
|
|||||||
"version": "6.12.4",
|
"version": "6.12.4",
|
||||||
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.4.tgz",
|
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.4.tgz",
|
||||||
"integrity": "sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==",
|
"integrity": "sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/state": "^6.0.0",
|
"@codemirror/state": "^6.0.0",
|
||||||
@@ -32,6 +41,7 @@
|
|||||||
"version": "6.7.1",
|
"version": "6.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.7.1.tgz",
|
||||||
"integrity": "sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A==",
|
"integrity": "sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@marijn/find-cluster-break": "^1.0.0"
|
"@marijn/find-cluster-break": "^1.0.0"
|
||||||
@@ -41,6 +51,7 @@
|
|||||||
"version": "6.43.6",
|
"version": "6.43.6",
|
||||||
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.6.tgz",
|
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.6.tgz",
|
||||||
"integrity": "sha512-EVunGSYN1wz1p75WY1s3Xg7t3i8Yol0kGZGizNdX9BUFgMFILYVe8/u6EVpo7Ff5PwbZuILb4QAq7IZoKzIEQA==",
|
"integrity": "sha512-EVunGSYN1wz1p75WY1s3Xg7t3i8Yol0kGZGizNdX9BUFgMFILYVe8/u6EVpo7Ff5PwbZuILb4QAq7IZoKzIEQA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/state": "^6.7.0",
|
"@codemirror/state": "^6.7.0",
|
||||||
@@ -53,12 +64,14 @@
|
|||||||
"version": "1.5.2",
|
"version": "1.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz",
|
||||||
"integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==",
|
"integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/@lezer/highlight": {
|
"node_modules/@lezer/highlight": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
|
||||||
"integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
|
"integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lezer/common": "^1.3.0"
|
"@lezer/common": "^1.3.0"
|
||||||
@@ -68,6 +81,7 @@
|
|||||||
"version": "1.4.10",
|
"version": "1.4.10",
|
||||||
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz",
|
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz",
|
||||||
"integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==",
|
"integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@lezer/common": "^1.0.0"
|
"@lezer/common": "^1.0.0"
|
||||||
@@ -77,24 +91,28 @@
|
|||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.3.tgz",
|
||||||
"integrity": "sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==",
|
"integrity": "sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/crelt": {
|
"node_modules/crelt": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.7.tgz",
|
||||||
"integrity": "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==",
|
"integrity": "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/style-mod": {
|
"node_modules/style-mod": {
|
||||||
"version": "4.1.3",
|
"version": "4.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
|
||||||
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
|
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/w3c-keyname": {
|
"node_modules/w3c-keyname": {
|
||||||
"version": "2.2.8",
|
"version": "2.2.8",
|
||||||
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
|
||||||
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
|
||||||
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "decodal-codemirror",
|
"name": "decodal-codemirror",
|
||||||
"version": "0.1.2",
|
"version": "0.3.0",
|
||||||
"description": "CodeMirror 6 language support for Decodal.",
|
"description": "CodeMirror 6 language support for Decodal.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"license": "MIT OR Apache-2.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
@@ -9,26 +9,48 @@
|
|||||||
"url": "git+https://gitea.hareworks.net/Hare/Decodal.git",
|
"url": "git+https://gitea.hareworks.net/Hare/Decodal.git",
|
||||||
"directory": "packages/decodal-codemirror"
|
"directory": "packages/decodal-codemirror"
|
||||||
},
|
},
|
||||||
|
"scripts": {
|
||||||
|
"test": "node --test src/*.test.mjs"
|
||||||
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"types": "./src/decodal.d.ts",
|
"types": "./src/decodal.d.ts",
|
||||||
"import": "./src/decodal.js"
|
"import": "./src/decodal.js"
|
||||||
},
|
},
|
||||||
|
"./format": {
|
||||||
|
"types": "./src/format.d.ts",
|
||||||
|
"import": "./src/format.js"
|
||||||
|
},
|
||||||
"./parser": "./src/decodal-parser.js",
|
"./parser": "./src/decodal-parser.js",
|
||||||
"./terms": "./src/decodal-parser.terms.js"
|
"./terms": "./src/decodal-parser.terms.js",
|
||||||
|
"./wasm/decodal_language_tools_bg.wasm": "./wasm/decodal_language_tools_bg.wasm"
|
||||||
},
|
},
|
||||||
"types": "./src/decodal.d.ts",
|
"types": "./src/decodal.d.ts",
|
||||||
"files": [
|
"files": [
|
||||||
"README.md",
|
"README.md",
|
||||||
"src/"
|
"LICENSE-MIT",
|
||||||
|
"LICENSE-APACHE",
|
||||||
|
"src/",
|
||||||
|
"wasm/decodal_language_tools.js",
|
||||||
|
"wasm/decodal_language_tools.d.ts",
|
||||||
|
"wasm/decodal_language_tools_bg.wasm",
|
||||||
|
"wasm/decodal_language_tools_bg.wasm.d.ts"
|
||||||
],
|
],
|
||||||
"keywords": [
|
"keywords": [
|
||||||
"decodal",
|
"decodal",
|
||||||
"codemirror",
|
"codemirror",
|
||||||
"lezer"
|
"lezer"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"peerDependencies": {
|
||||||
"@codemirror/language": "^6.12.4",
|
"@codemirror/language": "^6.12.4",
|
||||||
|
"@codemirror/view": "^6.43.6",
|
||||||
|
"@lezer/highlight": "^1.2.3",
|
||||||
|
"@lezer/lr": "^1.4.10"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@codemirror/language": "^6.12.4",
|
||||||
|
"@codemirror/state": "^6.7.1",
|
||||||
|
"@codemirror/view": "^6.43.6",
|
||||||
"@lezer/highlight": "^1.2.3",
|
"@lezer/highlight": "^1.2.3",
|
||||||
"@lezer/lr": "^1.4.10"
|
"@lezer/lr": "^1.4.10"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||||
import {LRParser} from "npm:@lezer/lr@^1.4.10"
|
import {LRParser} from "npm:@lezer/lr@^1.4.10"
|
||||||
|
const spec_Identifier = {__proto__:null,as:184}
|
||||||
export const parser = LRParser.deserialize({
|
export const parser = LRParser.deserialize({
|
||||||
version: 14,
|
version: 14,
|
||||||
states: "7vQYQPOOO#nQPO'#CaOOQO'#Cr'#CrO$wQPO'#CyO&UQPO'#DOO&^QPO'#DSO&eQPO'#DWO&mQPO'#CqO$wQPO'#DcO&wQPO'#DhOOQO'#Cq'#CqOOQO'#Cp'#CpO&|QPO'#CoO$wQPO'#CoOOQO'#Cn'#CnO)fQPO'#CmO.XQPO'#ClO0]QPO'#CkOOQO'#Cj'#CjO2TQPO'#CiO3xQPO'#ChO5jQPO'#CgO7UQPO'#CfOOQO'#Ce'#CeO7`QPO'#C`O7eQPO'#C_OOQO'#Dz'#DzQYQPOOO8xQPO'#D{O8}QPO,58{OOQO,59e,59eO9VQPO'#CaOOQO,59j,59jO9_QPO,59jOOQO,59n,59nO9gQPO,59nO9oQPO'#EOO9tQPO'#DYO9|QPO,59rO:RQPO'#CqO:`QPO'#D^O:hQQO,59vO:mQPO,59vO:rQPO,59]O:wQPO,59}OOQO,5:S,5:SO:|QPO'#DjOOQO,59[,59[O;TQPO,59[OOQO,59Z,59ZO$wQPO,59YO$wQPO,59XO$wQPO,59WOOQO'#Dr'#DrO$wQPO,59VO$wQPO,59UO$wQPO,59TO$wQPO,59SO$wQPO,59RO$wQPO,59QO$wQPO,58zOOQO,58y,58yOOQO-E7x-E7xOOQO,5:g,5:gOOQO-E7y-E7yO;YQPO1G/UO;bQPO1G/UOOQO1G/U1G/UO;jQPO1G/YO;qQPO1G/YOOQO1G/Y1G/YOOQO,5:j,5:jOOQO-E7|-E7|O$wQPO1G/^O$wQPO,59yO;yQPO,59xO<RQPO,59xO$wQPO1G/bO<ZQQO1G/bOOQO1G.w1G.wO<`QPO1G/iO<jQPO'#DkOOQO,5:U,5:UO<rQPO,5:UOOQO1G.v1G.vOOQO1G.t1G.tO<wQPO1G.sO@_QPO1G.rOAyQPO1G.qOOQO1G.p1G.pOCqQPO1G.oOEfQPO1G.nOFxQPO1G.mOOQO1G.l1G.lOOQO1G.f1G.fOOQO1G.s1G.sOOQO1G.r1G.rOOQO1G.q1G.qOOQO1G.o1G.oOOQO1G.n1G.nOOQO1G.m1G.mOOQO,5:h,5:hOOQO7+$p7+$pOHfQPO7+$pOOQO-E7z-E7zOOQO,5:i,5:iOOQO7+$t7+$tOHnQPO7+$tOOQO-E7{-E7{OOQO7+$x7+$xOOQO1G/e1G/eOHuQPO'#D_OOQO,5:k,5:kOHzQPO1G/dOOQO-E7}-E7}OOQO7+$|7+$|O$wQPO7+$|OOQO'#Df'#DfOISQPO'#DeOOQO7+%T7+%TOIXQPO7+%TOIaQPO,5:VOIhQPO,5:VOOQO1G/p1G/pOOQO<<H[<<H[PIpQPO'#D|OOQO<<H`<<H`P$wQPO'#D}PIuQPO'#EPOOQO<<Hh<<HhO$wQPO,5:POIzQPO<<HoOJUQPO<<HoOOQO<<Ho<<HoOJ^QPO1G/qOOQO1G/k1G/kOOQO,5:l,5:lOOQOAN>ZAN>ZOJeQPOAN>ZOOQO-E8O-E8OOOQOG23uG23uP<cQPO'#EQO$wQPO,59XO$wQPO,59WO$wQPO,59VO$wQPO,59TO$wQPO,59SO$wQPO,59ROJoQPO1G.rOKVQPO'#ClOKjQPO'#CkOL^QPO'#ChOM^QPO'#CgONaQPO'#CfO$wQPO,59WO$wQPO,59UO$wQPO,59QO! jQPO1G.qO!!QQPO'#CkO!#_QPO'#CiO$wQPO,59VO$wQPO,59UO$wQPO,59TO$wQPO,59SO$wQPO,59RO$wQPO,59QO$wQPO,58zO!$[QPO1G.oO!$rQPO1G.nO!%YQPO1G.mO!%zQPO'#CiO!&rQPO'#ChO!'gQPO'#CgO!(XQPO'#CfO!(vQPO'#C`",
|
states: ":|QYQPOOO#qQPO'#CaOOQO'#Cs'#CsO$zQPO'#CzO&XQQO'#DTO&dQQO'#D[O&nQPO'#D]O&vQPO'#CrO$zQPO'#DhO'QQPO'#DmOOQO'#Cr'#CrOOQO'#Cq'#CqO'VQPO'#CpO$zQPO'#CpOOQO'#Co'#CoO)rQPO'#CnO.kQPO'#CmO0rQPO'#ClOOQO'#Ck'#CkO2mQPO'#CjO4eQPO'#CiO6YQPO'#ChO7wQPO'#CgOOQO'#Cf'#CfO8RQPO'#CeO9iQPO'#C`O9nQPO'#C_OOQO'#EQ'#EQQYQPOOO;RQPO'#ERO;WQPO,58{OOQO,59f,59fO;`QPO'#CaO$zQPO,59kOOQO,59o,59oO;hQPO,59oO$zQPO,59rOOQO,59v,59vO;pQPO,59vO;xQPO'#EUO;}QPO'#D_O<VQPO,59wO<[QPO'#CrO<iQPO'#DcO<qQQO,59{O<vQPO,59{O<{QPO,59^O=QQPO,5:SOOQO,5:X,5:XO=VQPO'#DoOOQO,59],59]O=^QPO,59]OOQO,59[,59[O$zQPO,59ZO$zQPO,59YO$zQPO,59XOOQO'#Dw'#DwO$zQPO,59WO$zQPO,59VO$zQPO,59UO$zQPO,59TO$zQPO,59SO$zQPO,59ROOQO'#EP'#EPO$zQPO,59QO$zQPO,58zOOQO,58y,58yOOQO-E8O-E8OOOQO,5:m,5:mOOQO-E8P-E8PO=cQPO1G/VO=hQQO1G/ZO=sQPO1G/ZOOQO1G/Z1G/ZO={QPO1G/^O>TQPO1G/bO>[QPO1G/bOOQO1G/b1G/bOOQO,5:p,5:pOOQO-E8S-E8SO$zQPO1G/cO$zQPO,5:OO>dQPO,59}O>lQPO,59}O$zQPO1G/gO>tQQO1G/gOOQO1G.x1G.xO>yQPO1G/nO?TQPO'#DpOOQO,5:Z,5:ZO?]QPO,5:ZOOQO1G.w1G.wOOQO1G.u1G.uO?bQPO1G.tOCOQPO1G.sODmQPO1G.rOOQO1G.q1G.qOFhQPO1G.pOH`QPO1G.oOJTQPO1G.nOOQO1G.m1G.mOOQO1G.l1G.lOOQO1G.f1G.fOOQO1G.t1G.tOOQO1G.s1G.sOOQO1G.r1G.rOOQO1G.p1G.pOOQO1G.o1G.oOOQO1G.n1G.nOOQO7+$q7+$qOOQO,5:n,5:nO$zQPO'#DVOOQO7+$u7+$uOKgQPO7+$uOKoQQO7+$uOOQO-E8Q-E8QOOQO7+$x7+$xOKzQPO7+$xOOQO,5:o,5:oOOQO7+$|7+$|OLPQPO7+$|OOQO-E8R-E8ROOQO7+$}7+$}OOQO1G/j1G/jOLWQPO'#DdOOQO,5:q,5:qOL]QPO1G/iOOQO-E8T-E8TOOQO7+%R7+%RO$zQPO7+%ROOQO'#Dk'#DkOLeQPO'#DjOOQO7+%Y7+%YOLjQPO7+%YOLrQPO,5:[OLyQPO,5:[OOQO1G/u1G/uOOQO,59q,59qOOQO<<Ha<<HaOMRQPO<<HaOMWQPO<<HaPM`QPO'#ESOOQO<<Hd<<HdOOQO<<Hh<<HhP$zQPO'#ETPMeQPO'#EVOOQO<<Hm<<HmO$zQPO,5:UOMjQPO<<HtOMtQPO<<HtOOQO<<Ht<<HtOM|QPO1G/vOOQOAN={AN={ONTQPOAN={OOQO1G/p1G/pOOQO,5:r,5:rOOQOAN>`AN>`ONYQPOAN>`OOQO-E8U-E8UOOQOG23gG23gOOQOG23zG23zP>|QPO'#EWO$zQPO,59YO$zQPO,59XO$zQPO,59WO$zQPO,59UO$zQPO,59TO$zQPO,59SONdQPO1G.sONzQPO'#CmO! _QPO'#ClO!!RQPO'#CiO!#RQPO'#ChO!$UQPO'#CgO$zQPO,59XO$zQPO,59VO$zQPO,59RO$zQPO,59QO!%_QPO1G.rO!%uQPO'#ClO!'VQPO'#CjO!(SQPO'#CeO$zQPO,59WO$zQPO,59VO$zQPO,59UO$zQPO,59TO$zQPO,59SO$zQPO,59RO$zQPO,59QO$zQPO,58zO!)]QPO1G.pO!)sQPO1G.oO!*ZQPO1G.nO!*qQPO'#CjO!+lQPO'#CiO!,dQPO'#ChO!-XQPO'#CgO!-yQPO'#CeO!.eQPO'#C`",
|
||||||
stateData: "!)Y~O!wOSPOS~OUPOgQOhQOiQOjQOkQOlQOnROoROpROqROsSOwTO{UO!PVO!WWO!]XO!`]O!a]O~OneXoeXpeXqeX!PeX!aeX!beX!ceX!deX!eeX!geX!heX!ieX!jeX!keX!leX!meX~OVlOUeXWTXgeXheXieXjeXkeXleXseXteXweX{eX!WeX!]eX!`eX!ueX~P!gOUYOgQOhQOiQOjQOkQOlQOnROoROpROqROsSOwTO{UO!PVO!WWO!]XO!`]O!a]O~OUoOupO~OyrO~P$wOUoO}|P~OUwO!TyO~P]Og}O~OV!QO!P!OOUcXgcXhcXicXjcXkcXlcXncXocXpcXqcXscXtcXwcX{cX!WcX!]cX!`cX!acX!bcX!ccX!dcX!ecX!gcX!hcX!icX!jcX!kcX!lcX!mcX!ucXxcXycX!TcXucX!ScX~O!b!SO!c!SOUaXgaXhaXiaXjaXkaXlaXnaXoaXpaXqaXsaXtaXwaX{aX!PaX!WaX!]aX!`aX!aaX!daX!eaX!gaX!haX!iaX!jaX!kaX!laX!maX!uaXVaXxaXyaX!TaXuaX!SaX~OU`Xg`Xh`Xi`Xj`Xk`Xl`Xn`Xo`Xp`Xq`Xs`Xt`Xw`X{`X!P`X!W`X!]`X!``X!e`X!g`X!h`X!i`X!j`X!k`X!l`X!m`X!u`Xx`Xy`X!T`Xu`X!S`X~O!a!TO!d!TO~P,OOn!VOo!VOp!VOq!VO!g!VO!h!VOU_Xg_Xh_Xi_Xj_Xk_Xl_Xs_Xt_Xw_X{_X!P_X!W_X!]_X!`_X!a_X!i_X!j_X!k_X!l_X!m_X!u_X~O!e!UO~P.cOU]Xg]Xh]Xi]Xj]Xk]Xl]Xn]Xo]Xp]Xq]Xs]Xt]Xw]X{]X!P]X!W]X!]]X!`]X!a]X!j]X!k]X!l]X!m]X!u]X~O!i!XO~P0dOU[Xg[Xh[Xi[Xj[Xk[Xl[Xn[Xo[Xp[Xq[Xs[Xt[Xw[X{[X!P[X!W[X!][X!`[X!a[X!k[X!l[X!m[X!u[X~O!j!YO~P2[OUZXgZXhZXiZXjZXkZXlZXnZXoZXpZXqZXsZXtZXwZX{ZX!PZX!WZX!]ZX!`ZX!aZX!lZX!mZX!uZX~O!k!ZO~P4POUYXgYXhYXiYXjYXkYXlYXnYXoYXpYXqYXsYXtYXwYX{YX!PYX!WYX!]YX!`YX!aYX!uYX~O!l![O!m!]O~P5qOW!^O~Ot!_OURXgRXhRXiRXjRXkRXlRXnRXoRXpRXqRXsRXwRX{RX!PRX!WRX!]RX!`RX!aRX!uRX~OU!aO~OVlOWTa~OVlOWTX~Ot!cOu!eO~Ox!fOy!hO~Ot!iO~OUoO}|X~O}!kO~O!S!lOVeX!TeX~P!gOx!mO!T!QX~O!U!oO~O!T!pO~O!T!qO~Os!rO~O!T!tO~P$wOU!vO~OUoOu#YO~Ot#ZOu#YO~Oy#^O~P$wOx#_Oy#^O~OU#cO!T!Qa~Ox#eO!T!Qa~O!U#hO~Ou#kO!Z#iO~P$wOx#mO!T!_X~O!T#oO~O!b!SO!c!SOUaigaihaiiaijaikailainaioaipaiqaisaitaiwai{ai!Pai!Wai!]ai!`ai!aai!dai!eai!gai!hai!iai!jai!kai!lai!mai!uaixaiyai!Taiuai!Sai~O!d!TOn`io`ip`iq`is`it`i!e`i!g`i!h`i!i`i!j`i!k`i!l`i!m`i~OU`ig`ih`ii`ij`ik`il`iw`i{`i!P`i!W`i!]`i!``i!a`i!u`i~P?^Os_it_i!i_i!j_i!k_i!l_i!m_i~O!e!UOU_ig_ih_ii_ij_ik_il_in_io_ip_iq_iw_i{_i!P_i!W_i!]_i!`_i!a_i!u_i~PAbOs]it]i!j]i!k]i!l]i!m]i~O!i!XOU]ig]ih]ii]ij]ik]il]in]io]ip]iq]iw]i{]i!P]i!W]i!]]i!`]i!a]i!u]i~PC]Os[it[i!k[i!l[i!m[i~O!j!YOU[ig[ih[ii[ij[ik[il[in[io[ip[iq[iw[i{[i!P[i!W[i!][i!`[i!a[i!u[i~PETO!k!ZOUZigZihZiiZijZikZilZinZioZipZiqZisZitZiwZi{Zi!PZi!WZi!]Zi!`Zi!aZi!lZi!mZi!uZi~OUoOu#pO~Oy#rO~P$wO!S!lO~OU#cO!T!Qi~O!S#vO~Ot#wOu#yO~O!T!_a~P$wOx#zO!T!_a~OUoO~OU#cO~Ou#}O!Z#iO~P$wOt$OOu#}O~O!T!_i~P$wOu$QO!Z#iO~P$wO!a!TOx`iy`i!T`iu`i!S`i~P?^O!a$SO!d$SOV`X!b`X!c`X~P,OO!e$TOV_X!b_X!c_X!d_Xx_Xy_X!T_Xu_X!S_X~P.cO!j$VOV[X!b[X!c[X!d[X!e[X!g[X!h[X!i[Xx[Xy[X!T[Xu[X!S[X~P2[O!k$WOVZX!bZX!cZX!dZX!eZX!gZX!hZX!iZX!jZXxZXyZX!TZXuZX!SZX~P4PO!l$XO!m$bOVYX!bYX!cYX!dYX!eYX!gYX!hYX!iYX!jYX!kYXxYXyYX!TYXuYX!SYX~P5qO!e$`Ox_iy_i!T_iu_i!S_i~PAbOn!VOo!VOp!VOq!VO!e$`O!g!VO!h!VOx_Xy_X!i_X!j_X!k_X!l_X!m_X!T_Xs_Xt_Xu_X!S_X~O!i$aOV]X!b]X!c]X!d]X!e]X!g]X!h]Xx]Xy]X!T]Xu]X!S]X~P0dO!i$gOx]iy]i!T]iu]i!S]i~PC]O!j$hOx[iy[i!T[iu[i!S[i~PETO!k$iOxZiyZi!lZi!mZi!TZisZitZiuZi!SZi~O!i$gOx]Xy]X!j]X!k]X!l]X!m]X!T]Xs]Xt]Xu]X!S]X~O!j$hOx[Xy[X!k[X!l[X!m[X!T[Xs[Xt[Xu[X!S[X~O!k$iOxZXyZX!lZX!mZX!TZXsZXtZXuZX!SZX~O!l$jO!m$kOxYXyYX!TYXsYXtYXuYX!SYX~OW$lO~O{}!W!]!mjkih!ZUj~",
|
stateData: "!.w~O!}OSPOS~OUPOhQOiQOjQOkQOlQOmQOoROpROqROrROtSO{TO!QUO!UVO!]WO!bXO!e]O!f]O~OofXpfXqfXrfX!UfX!ffX!gfX!hfX!ifX!jfX!lfX!mfX!nfX!ofX!pfX!qfX!rfX#OfX~OVmOUfXWTXhfXifXjfXkfXlfXmfXtfXxfX{fX!QfX!]fX!bfX!efX!{fX~P!gOUYOhQOiQOjQOkQOlQOmQOoROpROqROrROtSO{TO!QUO!UVO!]WO!bXO!e]O!f]O~OUpOuqOvrO~OutO}uO~P$zOUpO!S!RP~OUzO!Y|O~P]Oh!QO~OV!TO!U!ROUdXhdXidXjdXkdXldXmdXodXpdXqdXrdXtdXxdX{dX!QdX!]dX!bdX!edX!fdX!gdX!hdX!idX!jdX!ldX!mdX!ndX!odX!pdX!qdX!rdX!{dX#OdX|dX}dX!YdXvdX!XdX~O!g!VO!h!VOUbXhbXibXjbXkbXlbXmbXobXpbXqbXrbXtbXxbX{bX!QbX!UbX!]bX!bbX!ebX!fbX!ibX!jbX!lbX!mbX!nbX!obX!pbX!qbX!rbX!{bX#ObXVbX|bX}bX!YbXvbX!XbX~OUaXhaXiaXjaXkaXlaXmaXoaXpaXqaXraXtaXxaX{aX!QaX!UaX!]aX!baX!eaX!jaX!laX!maX!naX!oaX!paX!qaX!raX!{aX#OaX|aX}aX!YaXvaX!XaX~O!f!WO!i!WO~P,_Oo!YOp!YOq!YOr!YO!l!YO!m!YOU`Xh`Xi`Xj`Xk`Xl`Xm`Xt`Xx`X{`X!Q`X!U`X!]`X!b`X!e`X!f`X!n`X!o`X!p`X!q`X!r`X!{`X#O`X~O!j!XO~P.uOU^Xh^Xi^Xj^Xk^Xl^Xm^Xo^Xp^Xq^Xr^Xt^Xx^X{^X!Q^X!U^X!]^X!b^X!e^X!f^X!o^X!p^X!q^X!r^X!{^X#O^X~O!n![O~P0yOU]Xh]Xi]Xj]Xk]Xl]Xm]Xo]Xp]Xq]Xr]Xt]Xx]X{]X!Q]X!U]X!]]X!b]X!e]X!f]X!p]X!q]X!r]X!{]X#O]X~O!o!]O~P2tOU[Xh[Xi[Xj[Xk[Xl[Xm[Xo[Xp[Xq[Xr[Xt[Xx[X{[X!Q[X!U[X!][X!b[X!e[X!f[X!q[X!r[X!{[X#O[X~O!p!^O~P4lOUZXhZXiZXjZXkZXlZXmZXoZXpZXqZXrZXtZXxZX{ZX!QZX!UZX!]ZX!bZX!eZX!fZX!{ZX#OZX~O!q!_O!r!`O~P6aO#O!aOUXXhXXiXXjXXkXXlXXmXXoXXpXXqXXrXXtXXxXX{XX!QXX!UXX!]XX!bXX!eXX!fXX!{XX~OW!cO~Ox!dOURXhRXiRXjRXkRXlRXmRXoRXpRXqRXrRXtRX{RX!QRX!URX!]RX!bRX!eRX!fRX!{RX~OU!fO~OVmOWTa~OVmOWTX~Ov!kOx!iO~O|!mO}!oO~Ox!pO~OUpO!S!RX~O!S!rO~O!X!sOVfX!YfX~P!gO|!tO!Y!VX~O!Z!vO~O!Y!wO~O!Y!xO~Ot!yO~O!Y!{O~P$zOU!}O~Ov#aO~OUpOu#cOv#dO~Ov#dOx#fO~O|#iO}#hO~O}#kO~P$zO|#lO}#kO~OU#pO!Y!Va~O|#rO!Y!Va~O!Z#uO~Ov#xO!`#vO~P$zO|#zO!Y!dX~O!Y#|O~O!g!VO!h!VOUbihbiibijbikbilbimbiobipbiqbirbitbixbi{bi!Qbi!Ubi!]bi!bbi!ebi!fbi!ibi!jbi!lbi!mbi!nbi!obi!pbi!qbi!rbi!{bi#Obi|bi}bi!Ybivbi!Xbi~O!i!WOoaipaiqairaitaixai!jai!lai!mai!nai!oai!pai!qai!rai#Oai~OUaihaiiaijaikailaimai{ai!Qai!Uai!]ai!bai!eai!fai!{ai~PAzOt`ix`i!n`i!o`i!p`i!q`i!r`i#O`i~O!j!XOU`ih`ii`ij`ik`il`im`io`ip`iq`ir`i{`i!Q`i!U`i!]`i!b`i!e`i!f`i!{`i~PDROt^ix^i!o^i!p^i!q^i!r^i#O^i~O!n![OU^ih^ii^ij^ik^il^im^io^ip^iq^ir^i{^i!Q^i!U^i!]^i!b^i!e^i!f^i!{^i~PFPOt]ix]i!p]i!q]i!r]i#O]i~O!o!]OU]ih]ii]ij]ik]il]im]io]ip]iq]ir]i{]i!Q]i!U]i!]]i!b]i!e]i!f]i!{]i~PGzOt[ix[i!q[i!r[i#O[i~O!p!^OU[ih[ii[ij[ik[il[im[io[ip[iq[ir[i{[i!Q[i!U[i!][i!b[i!e[i!f[i!{[i~PIrOv$OOx$PO~OUpOu#cOv$OO~O}$SO~O}$TO~P$zO!X!sO~OU#pO!Y!Vi~O!X$XO~Ov$[Ox$YO~O!Y!da~P$zO|$]O!Y!da~Ov$^O~Ov$^Ox$_O~OUpO~OU#pO~Ov$bO!`#vO~P$zOv$bOx$cO~O!Y!di~P$zOv$eO~Ov$fO!`#vO~P$zO!f!WO|ai}ai!Yaivai!Xai~PAzO!f$hO!i$hOVaX!gaX!haX~P,_O!j$iOV`X!g`X!h`X!i`X|`X}`X!Y`Xv`X!X`X~P.uO!o$kOV]X!g]X!h]X!i]X!j]X!l]X!m]X!n]X|]X}]X!Y]Xv]X!X]X~P2tO!p$lOV[X!g[X!h[X!i[X!j[X!l[X!m[X!n[X!o[X|[X}[X!Y[Xv[X!X[X~P4lO!q$mO!r$vOVZX!gZX!hZX!iZX!jZX!lZX!mZX!nZX!oZX!pZX|ZX}ZX!YZXvZX!XZX~P6aO!j$tO|`i}`i!Y`iv`i!X`i~PDROo!YOp!YOq!YOr!YO!j$tO!l!YO!m!YO|`X}`X!n`X!o`X!p`X!q`X!r`X#O`X!Y`Xt`Xv`Xx`X!X`X~O!n$uOV^X!g^X!h^X!i^X!j^X!l^X!m^X|^X}^X!Y^Xv^X!X^X~P0yOVXX!gXX!hXX!iXX!jXX!lXX!mXX!nXX!oXX!pXX!qXX!rXX|XX}XX!YXXvXX!XXX~P8RO!n$}O|^i}^i!Y^iv^i!X^i~PFPO!o%OO|]i}]i!Y]iv]i!X]i~PGzO!p%PO|[i}[i!Y[iv[i!X[i~PIrO!n$}O|^X}^X!o^X!p^X!q^X!r^X#O^X!Y^Xt^Xv^Xx^X!X^X~O!o%OO|]X}]X!p]X!q]X!r]X#O]X!Y]Xt]Xv]Xx]X!X]X~O!p%PO|[X}[X!q[X!r[X#O[X!Y[Xt[Xv[Xx[X!X[X~O!q%QO!r%RO|ZX}ZX#OZX!YZXtZXvZXxZX!XZX~O#O!aO|XX}XX!YXXtXXvXXxXX!XXX~OW%TO~O!Q!S!]!b!rklji!`Uk~",
|
||||||
goto: "3l!uPPP!v!z#[PPP#h$m%]%}&x'v(w)s*z,S-Y.b/f0jPPPPPP0jPPPP0jPPP0jPPP0jP1nP0jP1q1tPPP0jP1|2UP0jP2[2_PPPPPP2bPPPPPPP2k2q2x3O3Y3`3fTjOkSiOkQqSStUuV#X!c#Z#qShOk]$tSUu!c#Z#qSiOkQnRQsTQ{VQ|WQ!s!OS#Q!^$lY#]!f#_#m#s#zQ#a!kQ#b!lQ#g!oW#i!r#w$O$RQ#u#hR#{#v!OgORTVWk!O!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$lV#P!]$b$kWfOk!]!^Y$_R!k!o#h$bs$sTVW!O!f!l!r#_#m#s#v#w#z$O$R$k$lWeOk!]!^Q#O![Q#W$XY$^R!k!o#h$bQ$o$js$rTVW!O!f!l!r#_#m#s#v#w#z$O$R$k$lYdOk![!]!^Q!}!ZQ#V$W[$]R!k!o#h$X$bQ$n$iu$qTVW!O!f!l!r#_#m#s#v#w#z$O$R$j$k$l[cOk!Z![!]!^Q!|!YQ#U$V^$eR!k!o#h$W$X$bQ$m$hw$pTVW!O!f!l!r#_#m#s#v#w#z$O$R$i$j$k$l!hbORTVWk!O!Y!Z![!]!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$V$W$X$b$h$i$j$k$lV!{!X$a$g`aOk!X!Y!Z![!]!^Q!z!WQ#T$Ub$[R!k!o#h$V$W$X$a$bQ$c$f{$dTVW!O!f!l!r#_#m#s#v#w#z$O$R$g$h$i$j$k$l!``OTVWk!O!W!X!Y!Z![!]!^!f!l!r#_#m#s#v#w#z$O$R$f$g$h$i$j$k$lQ!y!UQ#S$TQ$Y$`e$ZR!k!o#h$U$V$W$X$a$b!z_ORTVWk!O!U!W!X!Y!Z![!]!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$T$U$V$W$X$`$a$b$f$g$h$i$j$k$lQ!x!TR#R$S#O^ORTVWk!O!T!U!W!X!Y!Z![!]!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$S$T$U$V$W$X$`$a$b$f$g$h$i$j$k$lQ!R]R!w!S#T[ORTVW]k!O!S!T!U!W!X!Y!Z![!]!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$S$T$U$V$W$X$`$a$b$f$g$h$i$j$k$l#TZORTVW]k!O!S!T!U!W!X!Y!Z![!]!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$S$T$U$V$W$X$`$a$b$f$g$h$i$j$k$l#TYORTVW]k!O!S!T!U!W!X!Y!Z![!]!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$S$T$U$V$W$X$`$a$b$f$g$h$i$j$k$lRvURzVQxVV#d!m#e#tQ#l!rV#|#w$O$RX#j!r#w$O$RR!P[R!u!OQ!WaQ$U$[R$f$dQkOR!`kSmPoR!bmQ!dqR#[!dQ!gsS#`!g#nR#n!sQuUR!juQ!nxR#f!nQ#x#lR$P#x",
|
goto: "6p!{PPP!|#Q#bPPP#n$|%n&f'^(_)c*j+l,y.X/e0s1}3XPPPPPP3XPPPP3XPPP3XP4c3XPPP3X3XP4iP3XP4l4oPPP3XP4w5PP3XP5V5YPPPPPP5]PPPPPPP5f5o5u5|6S6^6d6jTkOlSjOlQsSSwUxV#b!i#f$RSiOl]%^SUx!i#f$RSjOlQoRQvTQ!OVQ!PWQ!hqQ!ltQ!z!RS#Y!c%TY#j!m#l#z$U$]Q#n!rQ#o!sQ#t!vW#v!y$Y$c$gQ#}#cQ$W#uR$`$XUhOl!cW${R!r!v#uw%]TVWqt!R!m!s!y#c#l#z$U$X$Y$]$c$g%T!UgORTVWlqt!R!c!m!r!s!v!y#c#l#u#z$U$X$Y$]$c$g%TU#W!`$v%RV#X!b$w%SYfOl!`!b!c[$sR!r!v#u$v$w{%[TVWqt!R!m!s!y#c#l#z$U$X$Y$]$c$g%R%S%TYeOl!`!b!cQ#V!_Q#`$m[$rR!r!v#u$v$wQ%W%Q{%ZTVWqt!R!m!s!y#c#l#z$U$X$Y$]$c$g%R%S%T[dOl!_!`!b!cQ#U!^Q#_$l^$qR!r!v#u$m$v$wQ%V%P}%YTVWqt!R!m!s!y#c#l#z$U$X$Y$]$c$g%Q%R%S%T^cOl!^!_!`!b!cQ#T!]Q#^$k`$zR!r!v#u$l$m$v$wQ%U%O!P%XTVWqt!R!m!s!y#c#l#z$U$X$Y$]$c$g%P%Q%R%S%T!tbORTVWlqt!R!]!^!_!`!b!c!m!r!s!v!y#c#l#u#z$U$X$Y$]$c$g$k$l$m$v$w%O%P%Q%R%S%TV#S![$u$}baOl![!]!^!_!`!b!cQ#R!ZQ#]$jd$pR!r!v#u$k$l$m$u$v$wQ$x$|!T$yTVWqt!R!m!s!y#c#l#z$U$X$Y$]$c$g$}%O%P%Q%R%S%T!j`OTVWlqt!R!Z![!]!^!_!`!b!c!m!s!y#c#l#z$U$X$Y$]$c$g$|$}%O%P%Q%R%S%TQ#Q!XQ#[$iQ$n$tg$oR!r!v#u$j$k$l$m$u$v$w#W_ORTVWlqt!R!X!Z![!]!^!_!`!b!c!m!r!s!v!y#c#l#u#z$U$X$Y$]$c$g$i$j$k$l$m$t$u$v$w$|$}%O%P%Q%R%S%TQ#P!WR#Z$h#[^ORTVWlqt!R!W!X!Z![!]!^!_!`!b!c!m!r!s!v!y#c#l#u#z$U$X$Y$]$c$g$h$i$j$k$l$m$t$u$v$w$|$}%O%P%Q%R%S%TQ!U]R#O!V#a[ORTVW]lqt!R!V!W!X!Z![!]!^!_!`!b!c!m!r!s!v!y#c#l#u#z$U$X$Y$]$c$g$h$i$j$k$l$m$t$u$v$w$|$}%O%P%Q%R%S%T#aZORTVW]lqt!R!V!W!X!Z![!]!^!_!`!b!c!m!r!s!v!y#c#l#u#z$U$X$Y$]$c$g$h$i$j$k$l$m$t$u$v$w$|$}%O%P%Q%R%S%T#aYORTVW]lqt!R!V!W!X!Z![!]!^!_!`!b!c!m!r!s!v!y#c#l#u#z$U$X$Y$]$c$g$h$i$j$k$l$m$t$u$v$w$|$}%O%P%Q%R%S%TQ#e!iR$Q#fRyUR}VQ{VV#q!t#r$VQ#y!yV$a$Y$c$gX#w!y$Y$c$gR!S[R!|!RQ!ZaQ$j$pR$|$yQ!bhQ$w${R%S%]QlOR!elSnPpR!gnQ!jsR#g!jQ!nvS#m!n#{R#{!zQxUR!qxQ!u{R#s!uQ$Z#yR$d$Z",
|
||||||
nodeNames: "⚠ Comment Source Statement FieldDefinition FieldPath Identifier Dot Equal Expression DefaultExpression PatchExpression ComposeExpression LogicalOrExpression LogicalAndExpression ComparisonExpression ConcatExpression AdditiveExpression MultiplicativeExpression UnaryExpression PostfixExpression PrimaryExpression Literal String Integer Float True False Regex ComparisonConstraint Lt Lte Gt Gte Object LBrace Semicolon RBrace Array LBracket Comma RBracket LetExpression Let FieldDefinitionList In FunctionExpression LParen ParameterList Parameter Colon RParen Arrow MatchExpression Match MatchArm Pattern Underscore ImportExpression Import CallSuffix ArgumentList Bang Minus Star Slash Plus PlusPlus CompareOperator EqualEqual BangEqual AmpAmp PipePipe Amp SlashSlash Default",
|
nodeNames: "⚠ Comment Source Statement FieldDefinition FieldPath Identifier Dot Equal Expression AsExpression DefaultExpression PatchExpression ComposeExpression LogicalOrExpression LogicalAndExpression ComparisonExpression ConcatExpression AdditiveExpression MultiplicativeExpression UnaryExpression PostfixExpression PrimaryExpression Literal String Integer Float True False Regex ComparisonConstraint Lt Lte Gt Gte MapConstraint LBrace Ellipsis RBrace Object Semicolon ObjectRest ArrayConstraint LBracket Comma RBracket Array LetExpression Let FieldDefinitionList In FunctionExpression LParen ParameterList Parameter Colon RParen Arrow MatchExpression Match MatchArm Pattern Underscore ImportExpression Import CallSuffix ArgumentList Bang Minus Star Slash Plus PlusPlus CompareOperator EqualEqual BangEqual AmpAmp PipePipe Amp SlashSlash Default As",
|
||||||
maxTerm: 85,
|
maxTerm: 92,
|
||||||
skippedNodes: [0,1],
|
skippedNodes: [0,1],
|
||||||
repeatNodeCount: 7,
|
repeatNodeCount: 7,
|
||||||
tokenData: "=T~R!PX^$Upq$Uqr$yrs%Wst&zvw'cxy'pyz'uz{'z{|(P|}(^}!O(c!O!P(h!P!Q(m!Q![+T![!]+n!]!^+s!^!_+x!_!`,V!`!a,l!c!},y!}#O-[#P#Q-a#R#S-f#T#W,y#W#X-y#X#Y,y#Y#Z1Z#Z#],y#]#^3k#^#`,y#`#a6}#a#b8b#b#h,y#h#i:r#i#o,y#o#p<n#p#q<s#q#r=O#y#z$U$f$g$U#BY#BZ$U$IS$I_$U$I|$JO$U$JT$JU$U$KV$KW$U&FU&FV$U~$ZY!w~X^$Upq$U#y#z$U$f$g$U#BY#BZ$U$IS$I_$U$I|$JO$U$JT$JU$U$KV$KW$U&FU&FV$U~%OP!`~!_!`%R~%WO!h~~%ZWOY%WZr%Wrs%ss#O%W#O#P%x#P;'S%W;'S;=`&t<%lO%W~%xOg~~%{RO;'S%W;'S;=`&U;=`O%W~&XXOY%WZr%Wrs%ss#O%W#O#P%x#P;'S%W;'S;=`&t;=`<%l%W<%lO%W~&wP;=`<%l%W~'PSP~OY&zZ;'S&z;'S;=`']<%lO&z~'`P;=`<%l&z~'hP!k~vw'k~'pO!i~~'uO!P~~'zO!T~~(PO!b~~(UP!d~{|(X~(^O!e~~(cOx~~(hO!a~~(mOV~~(rW!c~OY)[Z!P)[!P!Q+O!Q#O)[#O#P)|#P;'S)[;'S;=`*x<%lO)[~)_WOY)[Z!P)[!P!Q)w!Q#O)[#O#P)|#P;'S)[;'S;=`*x<%lO)[~)|Ol~~*PRO;'S)[;'S;=`*Y;=`O)[~*]XOY)[Z!P)[!P!Q)w!Q#O)[#O#P)|#P;'S)[;'S;=`*x;=`<%l)[<%lO)[~*{P;=`<%l)[~+TO!l~~+YQh~!O!P+`!Q![+T~+cP!Q![+f~+kPi~!Q![+f~+sO!S~~+xOt~~+}Pn~!_!`,Q~,VOo~~,[QWP!_!`,b!`!a,g~,gO!g~Q,lO!UQ~,qPp~!_!`,t~,yOq~~-OSU~!Q![,y!c!},y#R#S,y#T#o,y~-aOw~~-fOy~~-mS!Z~U~!Q![,y!c!},y#R#S,y#T#o,y~.OUU~!Q![,y!c!},y#R#S,y#T#X,y#X#Y.b#Y#o,y~.gUU~!Q![,y!c!},y#R#S,y#T#Y,y#Y#Z.y#Z#o,y~/OTU~!Q![,y!c!},y#R#S,y#T#U/_#U#o,y~/dUU~!Q![,y!c!},y#R#S,y#T#i,y#i#j/v#j#o,y~/{UU~!Q![,y!c!},y#R#S,y#T#`,y#`#a0_#a#o,y~0dUU~!Q![,y!c!},y#R#S,y#T#h,y#h#i0v#i#o,y~0}S!m~U~!Q![,y!c!},y#R#S,y#T#o,y~1`TU~!Q![,y!c!},y#R#S,y#T#U1o#U#o,y~1tUU~!Q![,y!c!},y#R#S,y#T#`,y#`#a2W#a#o,y~2]UU~!Q![,y!c!},y#R#S,y#T#g,y#g#h2o#h#o,y~2tUU~!Q![,y!c!},y#R#S,y#T#X,y#X#Y3W#Y#o,y~3_Sk~U~!Q![,y!c!},y#R#S,y#T#o,y~3pVU~!Q![,y!c!},y#R#S,y#T#a,y#a#b4V#b#c6j#c#o,y~4[UU~!Q![,y!c!},y#R#S,y#T#d,y#d#e4n#e#o,y~4sUU~!Q![,y!c!},y#R#S,y#T#c,y#c#d5V#d#o,y~5[UU~!Q![,y!c!},y#R#S,y#T#f,y#f#g5n#g#o,y~5sUU~!Q![,y!c!},y#R#S,y#T#h,y#h#i6V#i#o,y~6^S!]~U~!Q![,y!c!},y#R#S,y#T#o,y~6qS}~U~!Q![,y!c!},y#R#S,y#T#o,y~7SUU~!Q![,y!c!},y#R#S,y#T#X,y#X#Y7f#Y#o,y~7kUU~!Q![,y!c!},y#R#S,y#T#h,y#h#i7}#i#o,y~8US{~U~!Q![,y!c!},y#R#S,y#T#o,y~8gTU~!Q![,y!c!},y#R#S,y#T#U8v#U#o,y~8{UU~!Q![,y!c!},y#R#S,y#T#h,y#h#i9_#i#o,y~9dUU~!Q![,y!c!},y#R#S,y#T#V,y#V#W9v#W#o,y~9{UU~!Q![,y!c!},y#R#S,y#T#[,y#[#]:_#]#o,y~:fS!W~U~!Q![,y!c!},y#R#S,y#T#o,y~:wUU~!Q![,y!c!},y#R#S,y#T#f,y#f#g;Z#g#o,y~;`UU~!Q![,y!c!},y#R#S,y#T#i,y#i#j;r#j#o,y~;wUU~!Q![,y!c!},y#R#S,y#T#X,y#X#Y<Z#Y#o,y~<bSj~U~!Q![,y!c!},y#R#S,y#T#o,y~<sOs~~<vP#p#q<y~=OO!j~~=TOu~",
|
tokenData: "=c~R!PX^$Upq$Uqr$yrs%Wst&zvw'cxy'pyz'uz{'z{|(P|}(^}!O(c!O!P(h!P!Q({!Q![+c![!]+|!]!^,R!^!_,W!_!`,e!`!a,z!c!}-X!}#O-j#P#Q-o#R#S-t#T#W-X#W#X.X#X#Y-X#Y#Z1i#Z#]-X#]#^3y#^#`-X#`#a7]#a#b8p#b#h-X#h#i;Q#i#o-X#o#p<|#p#q=R#q#r=^#y#z$U$f$g$U#BY#BZ$U$IS$I_$U$I|$JO$U$JT$JU$U$KV$KW$U&FU&FV$U~$ZY!}~X^$Upq$U#y#z$U$f$g$U#BY#BZ$U$IS$I_$U$I|$JO$U$JT$JU$U$KV$KW$U&FU&FV$U~%OP!e~!_!`%R~%WO!m~~%ZWOY%WZr%Wrs%ss#O%W#O#P%x#P;'S%W;'S;=`&t<%lO%W~%xOh~~%{RO;'S%W;'S;=`&U;=`O%W~&XXOY%WZr%Wrs%ss#O%W#O#P%x#P;'S%W;'S;=`&t;=`<%l%W<%lO%W~&wP;=`<%l%W~'PSP~OY&zZ;'S&z;'S;=`']<%lO&z~'`P;=`<%l&z~'hP!p~vw'k~'pO!n~~'uO!U~~'zO!Y~~(PO!g~~(UP!i~{|(X~(^O!j~~(cO|~~(hO!f~R(mPVP!O!P(pQ(sP!O!P(vQ({OuQ~)QW!h~OY)jZ!P)j!P!Q+^!Q#O)j#O#P*[#P;'S)j;'S;=`+W<%lO)j~)mWOY)jZ!P)j!P!Q*V!Q#O)j#O#P*[#P;'S)j;'S;=`+W<%lO)j~*[Om~~*_RO;'S)j;'S;=`*h;=`O)j~*kXOY)jZ!P)j!P!Q*V!Q#O)j#O#P*[#P;'S)j;'S;=`+W;=`<%l)j<%lO)j~+ZP;=`<%l)j~+cO!q~~+hQi~!O!P+n!Q![+c~+qP!Q![+t~+yPj~!Q![+t~,RO!X~~,WOx~~,]Po~!_!`,`~,eOp~~,jQWP!_!`,p!`!a,u~,uO!l~Q,zO!ZQ~-PPq~!_!`-S~-XOr~~-^SU~!Q![-X!c!}-X#R#S-X#T#o-X~-oO{~~-tO}~~-{S!`~U~!Q![-X!c!}-X#R#S-X#T#o-X~.^UU~!Q![-X!c!}-X#R#S-X#T#X-X#X#Y.p#Y#o-X~.uUU~!Q![-X!c!}-X#R#S-X#T#Y-X#Y#Z/X#Z#o-X~/^TU~!Q![-X!c!}-X#R#S-X#T#U/m#U#o-X~/rUU~!Q![-X!c!}-X#R#S-X#T#i-X#i#j0U#j#o-X~0ZUU~!Q![-X!c!}-X#R#S-X#T#`-X#`#a0m#a#o-X~0rUU~!Q![-X!c!}-X#R#S-X#T#h-X#h#i1U#i#o-X~1]S!r~U~!Q![-X!c!}-X#R#S-X#T#o-X~1nTU~!Q![-X!c!}-X#R#S-X#T#U1}#U#o-X~2SUU~!Q![-X!c!}-X#R#S-X#T#`-X#`#a2f#a#o-X~2kUU~!Q![-X!c!}-X#R#S-X#T#g-X#g#h2}#h#o-X~3SUU~!Q![-X!c!}-X#R#S-X#T#X-X#X#Y3f#Y#o-X~3mSl~U~!Q![-X!c!}-X#R#S-X#T#o-X~4OVU~!Q![-X!c!}-X#R#S-X#T#a-X#a#b4e#b#c6x#c#o-X~4jUU~!Q![-X!c!}-X#R#S-X#T#d-X#d#e4|#e#o-X~5RUU~!Q![-X!c!}-X#R#S-X#T#c-X#c#d5e#d#o-X~5jUU~!Q![-X!c!}-X#R#S-X#T#f-X#f#g5|#g#o-X~6RUU~!Q![-X!c!}-X#R#S-X#T#h-X#h#i6e#i#o-X~6lS!b~U~!Q![-X!c!}-X#R#S-X#T#o-X~7PS!S~U~!Q![-X!c!}-X#R#S-X#T#o-X~7bUU~!Q![-X!c!}-X#R#S-X#T#X-X#X#Y7t#Y#o-X~7yUU~!Q![-X!c!}-X#R#S-X#T#h-X#h#i8]#i#o-X~8dS!Q~U~!Q![-X!c!}-X#R#S-X#T#o-X~8uTU~!Q![-X!c!}-X#R#S-X#T#U9U#U#o-X~9ZUU~!Q![-X!c!}-X#R#S-X#T#h-X#h#i9m#i#o-X~9rUU~!Q![-X!c!}-X#R#S-X#T#V-X#V#W:U#W#o-X~:ZUU~!Q![-X!c!}-X#R#S-X#T#[-X#[#]:m#]#o-X~:tS!]~U~!Q![-X!c!}-X#R#S-X#T#o-X~;VUU~!Q![-X!c!}-X#R#S-X#T#f-X#f#g;i#g#o-X~;nUU~!Q![-X!c!}-X#R#S-X#T#i-X#i#j<Q#j#o-X~<VUU~!Q![-X!c!}-X#R#S-X#T#X-X#X#Y<i#Y#o-X~<pSk~U~!Q![-X!c!}-X#R#S-X#T#o-X~=ROt~~=UP#p#q=X~=^O!o~~=cOv~",
|
||||||
tokenizers: [0, 1],
|
tokenizers: [0, 1],
|
||||||
topRules: {"Source":[0,2]},
|
topRules: {"Source":[0,2]},
|
||||||
tokenPrec: 2481
|
specialized: [{term: 6, get: (value) => spec_Identifier[value] || -1}],
|
||||||
|
tokenPrec: 2740
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||||
import {LRParser} from "@lezer/lr"
|
import {LRParser} from "@lezer/lr"
|
||||||
|
const spec_Identifier = {__proto__:null,as:184}
|
||||||
export const parser = LRParser.deserialize({
|
export const parser = LRParser.deserialize({
|
||||||
version: 14,
|
version: 14,
|
||||||
states: "7vQYQPOOO#nQPO'#CaOOQO'#Cr'#CrO$wQPO'#CyO&UQPO'#DOO&^QPO'#DSO&eQPO'#DWO&mQPO'#CqO$wQPO'#DcO&wQPO'#DhOOQO'#Cq'#CqOOQO'#Cp'#CpO&|QPO'#CoO$wQPO'#CoOOQO'#Cn'#CnO)fQPO'#CmO.XQPO'#ClO0]QPO'#CkOOQO'#Cj'#CjO2TQPO'#CiO3xQPO'#ChO5jQPO'#CgO7UQPO'#CfOOQO'#Ce'#CeO7`QPO'#C`O7eQPO'#C_OOQO'#Dz'#DzQYQPOOO8xQPO'#D{O8}QPO,58{OOQO,59e,59eO9VQPO'#CaOOQO,59j,59jO9_QPO,59jOOQO,59n,59nO9gQPO,59nO9oQPO'#EOO9tQPO'#DYO9|QPO,59rO:RQPO'#CqO:`QPO'#D^O:hQQO,59vO:mQPO,59vO:rQPO,59]O:wQPO,59}OOQO,5:S,5:SO:|QPO'#DjOOQO,59[,59[O;TQPO,59[OOQO,59Z,59ZO$wQPO,59YO$wQPO,59XO$wQPO,59WOOQO'#Dr'#DrO$wQPO,59VO$wQPO,59UO$wQPO,59TO$wQPO,59SO$wQPO,59RO$wQPO,59QO$wQPO,58zOOQO,58y,58yOOQO-E7x-E7xOOQO,5:g,5:gOOQO-E7y-E7yO;YQPO1G/UO;bQPO1G/UOOQO1G/U1G/UO;jQPO1G/YO;qQPO1G/YOOQO1G/Y1G/YOOQO,5:j,5:jOOQO-E7|-E7|O$wQPO1G/^O$wQPO,59yO;yQPO,59xO<RQPO,59xO$wQPO1G/bO<ZQQO1G/bOOQO1G.w1G.wO<`QPO1G/iO<jQPO'#DkOOQO,5:U,5:UO<rQPO,5:UOOQO1G.v1G.vOOQO1G.t1G.tO<wQPO1G.sO@_QPO1G.rOAyQPO1G.qOOQO1G.p1G.pOCqQPO1G.oOEfQPO1G.nOFxQPO1G.mOOQO1G.l1G.lOOQO1G.f1G.fOOQO1G.s1G.sOOQO1G.r1G.rOOQO1G.q1G.qOOQO1G.o1G.oOOQO1G.n1G.nOOQO1G.m1G.mOOQO,5:h,5:hOOQO7+$p7+$pOHfQPO7+$pOOQO-E7z-E7zOOQO,5:i,5:iOOQO7+$t7+$tOHnQPO7+$tOOQO-E7{-E7{OOQO7+$x7+$xOOQO1G/e1G/eOHuQPO'#D_OOQO,5:k,5:kOHzQPO1G/dOOQO-E7}-E7}OOQO7+$|7+$|O$wQPO7+$|OOQO'#Df'#DfOISQPO'#DeOOQO7+%T7+%TOIXQPO7+%TOIaQPO,5:VOIhQPO,5:VOOQO1G/p1G/pOOQO<<H[<<H[PIpQPO'#D|OOQO<<H`<<H`P$wQPO'#D}PIuQPO'#EPOOQO<<Hh<<HhO$wQPO,5:POIzQPO<<HoOJUQPO<<HoOOQO<<Ho<<HoOJ^QPO1G/qOOQO1G/k1G/kOOQO,5:l,5:lOOQOAN>ZAN>ZOJeQPOAN>ZOOQO-E8O-E8OOOQOG23uG23uP<cQPO'#EQO$wQPO,59XO$wQPO,59WO$wQPO,59VO$wQPO,59TO$wQPO,59SO$wQPO,59ROJoQPO1G.rOKVQPO'#ClOKjQPO'#CkOL^QPO'#ChOM^QPO'#CgONaQPO'#CfO$wQPO,59WO$wQPO,59UO$wQPO,59QO! jQPO1G.qO!!QQPO'#CkO!#_QPO'#CiO$wQPO,59VO$wQPO,59UO$wQPO,59TO$wQPO,59SO$wQPO,59RO$wQPO,59QO$wQPO,58zO!$[QPO1G.oO!$rQPO1G.nO!%YQPO1G.mO!%zQPO'#CiO!&rQPO'#ChO!'gQPO'#CgO!(XQPO'#CfO!(vQPO'#C`",
|
states: ":|QYQPOOO#qQPO'#CaOOQO'#Cs'#CsO$zQPO'#CzO&XQQO'#DTO&dQQO'#D[O&nQPO'#D]O&vQPO'#CrO$zQPO'#DhO'QQPO'#DmOOQO'#Cr'#CrOOQO'#Cq'#CqO'VQPO'#CpO$zQPO'#CpOOQO'#Co'#CoO)rQPO'#CnO.kQPO'#CmO0rQPO'#ClOOQO'#Ck'#CkO2mQPO'#CjO4eQPO'#CiO6YQPO'#ChO7wQPO'#CgOOQO'#Cf'#CfO8RQPO'#CeO9iQPO'#C`O9nQPO'#C_OOQO'#EQ'#EQQYQPOOO;RQPO'#ERO;WQPO,58{OOQO,59f,59fO;`QPO'#CaO$zQPO,59kOOQO,59o,59oO;hQPO,59oO$zQPO,59rOOQO,59v,59vO;pQPO,59vO;xQPO'#EUO;}QPO'#D_O<VQPO,59wO<[QPO'#CrO<iQPO'#DcO<qQQO,59{O<vQPO,59{O<{QPO,59^O=QQPO,5:SOOQO,5:X,5:XO=VQPO'#DoOOQO,59],59]O=^QPO,59]OOQO,59[,59[O$zQPO,59ZO$zQPO,59YO$zQPO,59XOOQO'#Dw'#DwO$zQPO,59WO$zQPO,59VO$zQPO,59UO$zQPO,59TO$zQPO,59SO$zQPO,59ROOQO'#EP'#EPO$zQPO,59QO$zQPO,58zOOQO,58y,58yOOQO-E8O-E8OOOQO,5:m,5:mOOQO-E8P-E8PO=cQPO1G/VO=hQQO1G/ZO=sQPO1G/ZOOQO1G/Z1G/ZO={QPO1G/^O>TQPO1G/bO>[QPO1G/bOOQO1G/b1G/bOOQO,5:p,5:pOOQO-E8S-E8SO$zQPO1G/cO$zQPO,5:OO>dQPO,59}O>lQPO,59}O$zQPO1G/gO>tQQO1G/gOOQO1G.x1G.xO>yQPO1G/nO?TQPO'#DpOOQO,5:Z,5:ZO?]QPO,5:ZOOQO1G.w1G.wOOQO1G.u1G.uO?bQPO1G.tOCOQPO1G.sODmQPO1G.rOOQO1G.q1G.qOFhQPO1G.pOH`QPO1G.oOJTQPO1G.nOOQO1G.m1G.mOOQO1G.l1G.lOOQO1G.f1G.fOOQO1G.t1G.tOOQO1G.s1G.sOOQO1G.r1G.rOOQO1G.p1G.pOOQO1G.o1G.oOOQO1G.n1G.nOOQO7+$q7+$qOOQO,5:n,5:nO$zQPO'#DVOOQO7+$u7+$uOKgQPO7+$uOKoQQO7+$uOOQO-E8Q-E8QOOQO7+$x7+$xOKzQPO7+$xOOQO,5:o,5:oOOQO7+$|7+$|OLPQPO7+$|OOQO-E8R-E8ROOQO7+$}7+$}OOQO1G/j1G/jOLWQPO'#DdOOQO,5:q,5:qOL]QPO1G/iOOQO-E8T-E8TOOQO7+%R7+%RO$zQPO7+%ROOQO'#Dk'#DkOLeQPO'#DjOOQO7+%Y7+%YOLjQPO7+%YOLrQPO,5:[OLyQPO,5:[OOQO1G/u1G/uOOQO,59q,59qOOQO<<Ha<<HaOMRQPO<<HaOMWQPO<<HaPM`QPO'#ESOOQO<<Hd<<HdOOQO<<Hh<<HhP$zQPO'#ETPMeQPO'#EVOOQO<<Hm<<HmO$zQPO,5:UOMjQPO<<HtOMtQPO<<HtOOQO<<Ht<<HtOM|QPO1G/vOOQOAN={AN={ONTQPOAN={OOQO1G/p1G/pOOQO,5:r,5:rOOQOAN>`AN>`ONYQPOAN>`OOQO-E8U-E8UOOQOG23gG23gOOQOG23zG23zP>|QPO'#EWO$zQPO,59YO$zQPO,59XO$zQPO,59WO$zQPO,59UO$zQPO,59TO$zQPO,59SONdQPO1G.sONzQPO'#CmO! _QPO'#ClO!!RQPO'#CiO!#RQPO'#ChO!$UQPO'#CgO$zQPO,59XO$zQPO,59VO$zQPO,59RO$zQPO,59QO!%_QPO1G.rO!%uQPO'#ClO!'VQPO'#CjO!(SQPO'#CeO$zQPO,59WO$zQPO,59VO$zQPO,59UO$zQPO,59TO$zQPO,59SO$zQPO,59RO$zQPO,59QO$zQPO,58zO!)]QPO1G.pO!)sQPO1G.oO!*ZQPO1G.nO!*qQPO'#CjO!+lQPO'#CiO!,dQPO'#ChO!-XQPO'#CgO!-yQPO'#CeO!.eQPO'#C`",
|
||||||
stateData: "!)Y~O!wOSPOS~OUPOgQOhQOiQOjQOkQOlQOnROoROpROqROsSOwTO{UO!PVO!WWO!]XO!`]O!a]O~OneXoeXpeXqeX!PeX!aeX!beX!ceX!deX!eeX!geX!heX!ieX!jeX!keX!leX!meX~OVlOUeXWTXgeXheXieXjeXkeXleXseXteXweX{eX!WeX!]eX!`eX!ueX~P!gOUYOgQOhQOiQOjQOkQOlQOnROoROpROqROsSOwTO{UO!PVO!WWO!]XO!`]O!a]O~OUoOupO~OyrO~P$wOUoO}|P~OUwO!TyO~P]Og}O~OV!QO!P!OOUcXgcXhcXicXjcXkcXlcXncXocXpcXqcXscXtcXwcX{cX!WcX!]cX!`cX!acX!bcX!ccX!dcX!ecX!gcX!hcX!icX!jcX!kcX!lcX!mcX!ucXxcXycX!TcXucX!ScX~O!b!SO!c!SOUaXgaXhaXiaXjaXkaXlaXnaXoaXpaXqaXsaXtaXwaX{aX!PaX!WaX!]aX!`aX!aaX!daX!eaX!gaX!haX!iaX!jaX!kaX!laX!maX!uaXVaXxaXyaX!TaXuaX!SaX~OU`Xg`Xh`Xi`Xj`Xk`Xl`Xn`Xo`Xp`Xq`Xs`Xt`Xw`X{`X!P`X!W`X!]`X!``X!e`X!g`X!h`X!i`X!j`X!k`X!l`X!m`X!u`Xx`Xy`X!T`Xu`X!S`X~O!a!TO!d!TO~P,OOn!VOo!VOp!VOq!VO!g!VO!h!VOU_Xg_Xh_Xi_Xj_Xk_Xl_Xs_Xt_Xw_X{_X!P_X!W_X!]_X!`_X!a_X!i_X!j_X!k_X!l_X!m_X!u_X~O!e!UO~P.cOU]Xg]Xh]Xi]Xj]Xk]Xl]Xn]Xo]Xp]Xq]Xs]Xt]Xw]X{]X!P]X!W]X!]]X!`]X!a]X!j]X!k]X!l]X!m]X!u]X~O!i!XO~P0dOU[Xg[Xh[Xi[Xj[Xk[Xl[Xn[Xo[Xp[Xq[Xs[Xt[Xw[X{[X!P[X!W[X!][X!`[X!a[X!k[X!l[X!m[X!u[X~O!j!YO~P2[OUZXgZXhZXiZXjZXkZXlZXnZXoZXpZXqZXsZXtZXwZX{ZX!PZX!WZX!]ZX!`ZX!aZX!lZX!mZX!uZX~O!k!ZO~P4POUYXgYXhYXiYXjYXkYXlYXnYXoYXpYXqYXsYXtYXwYX{YX!PYX!WYX!]YX!`YX!aYX!uYX~O!l![O!m!]O~P5qOW!^O~Ot!_OURXgRXhRXiRXjRXkRXlRXnRXoRXpRXqRXsRXwRX{RX!PRX!WRX!]RX!`RX!aRX!uRX~OU!aO~OVlOWTa~OVlOWTX~Ot!cOu!eO~Ox!fOy!hO~Ot!iO~OUoO}|X~O}!kO~O!S!lOVeX!TeX~P!gOx!mO!T!QX~O!U!oO~O!T!pO~O!T!qO~Os!rO~O!T!tO~P$wOU!vO~OUoOu#YO~Ot#ZOu#YO~Oy#^O~P$wOx#_Oy#^O~OU#cO!T!Qa~Ox#eO!T!Qa~O!U#hO~Ou#kO!Z#iO~P$wOx#mO!T!_X~O!T#oO~O!b!SO!c!SOUaigaihaiiaijaikailainaioaipaiqaisaitaiwai{ai!Pai!Wai!]ai!`ai!aai!dai!eai!gai!hai!iai!jai!kai!lai!mai!uaixaiyai!Taiuai!Sai~O!d!TOn`io`ip`iq`is`it`i!e`i!g`i!h`i!i`i!j`i!k`i!l`i!m`i~OU`ig`ih`ii`ij`ik`il`iw`i{`i!P`i!W`i!]`i!``i!a`i!u`i~P?^Os_it_i!i_i!j_i!k_i!l_i!m_i~O!e!UOU_ig_ih_ii_ij_ik_il_in_io_ip_iq_iw_i{_i!P_i!W_i!]_i!`_i!a_i!u_i~PAbOs]it]i!j]i!k]i!l]i!m]i~O!i!XOU]ig]ih]ii]ij]ik]il]in]io]ip]iq]iw]i{]i!P]i!W]i!]]i!`]i!a]i!u]i~PC]Os[it[i!k[i!l[i!m[i~O!j!YOU[ig[ih[ii[ij[ik[il[in[io[ip[iq[iw[i{[i!P[i!W[i!][i!`[i!a[i!u[i~PETO!k!ZOUZigZihZiiZijZikZilZinZioZipZiqZisZitZiwZi{Zi!PZi!WZi!]Zi!`Zi!aZi!lZi!mZi!uZi~OUoOu#pO~Oy#rO~P$wO!S!lO~OU#cO!T!Qi~O!S#vO~Ot#wOu#yO~O!T!_a~P$wOx#zO!T!_a~OUoO~OU#cO~Ou#}O!Z#iO~P$wOt$OOu#}O~O!T!_i~P$wOu$QO!Z#iO~P$wO!a!TOx`iy`i!T`iu`i!S`i~P?^O!a$SO!d$SOV`X!b`X!c`X~P,OO!e$TOV_X!b_X!c_X!d_Xx_Xy_X!T_Xu_X!S_X~P.cO!j$VOV[X!b[X!c[X!d[X!e[X!g[X!h[X!i[Xx[Xy[X!T[Xu[X!S[X~P2[O!k$WOVZX!bZX!cZX!dZX!eZX!gZX!hZX!iZX!jZXxZXyZX!TZXuZX!SZX~P4PO!l$XO!m$bOVYX!bYX!cYX!dYX!eYX!gYX!hYX!iYX!jYX!kYXxYXyYX!TYXuYX!SYX~P5qO!e$`Ox_iy_i!T_iu_i!S_i~PAbOn!VOo!VOp!VOq!VO!e$`O!g!VO!h!VOx_Xy_X!i_X!j_X!k_X!l_X!m_X!T_Xs_Xt_Xu_X!S_X~O!i$aOV]X!b]X!c]X!d]X!e]X!g]X!h]Xx]Xy]X!T]Xu]X!S]X~P0dO!i$gOx]iy]i!T]iu]i!S]i~PC]O!j$hOx[iy[i!T[iu[i!S[i~PETO!k$iOxZiyZi!lZi!mZi!TZisZitZiuZi!SZi~O!i$gOx]Xy]X!j]X!k]X!l]X!m]X!T]Xs]Xt]Xu]X!S]X~O!j$hOx[Xy[X!k[X!l[X!m[X!T[Xs[Xt[Xu[X!S[X~O!k$iOxZXyZX!lZX!mZX!TZXsZXtZXuZX!SZX~O!l$jO!m$kOxYXyYX!TYXsYXtYXuYX!SYX~OW$lO~O{}!W!]!mjkih!ZUj~",
|
stateData: "!.w~O!}OSPOS~OUPOhQOiQOjQOkQOlQOmQOoROpROqROrROtSO{TO!QUO!UVO!]WO!bXO!e]O!f]O~OofXpfXqfXrfX!UfX!ffX!gfX!hfX!ifX!jfX!lfX!mfX!nfX!ofX!pfX!qfX!rfX#OfX~OVmOUfXWTXhfXifXjfXkfXlfXmfXtfXxfX{fX!QfX!]fX!bfX!efX!{fX~P!gOUYOhQOiQOjQOkQOlQOmQOoROpROqROrROtSO{TO!QUO!UVO!]WO!bXO!e]O!f]O~OUpOuqOvrO~OutO}uO~P$zOUpO!S!RP~OUzO!Y|O~P]Oh!QO~OV!TO!U!ROUdXhdXidXjdXkdXldXmdXodXpdXqdXrdXtdXxdX{dX!QdX!]dX!bdX!edX!fdX!gdX!hdX!idX!jdX!ldX!mdX!ndX!odX!pdX!qdX!rdX!{dX#OdX|dX}dX!YdXvdX!XdX~O!g!VO!h!VOUbXhbXibXjbXkbXlbXmbXobXpbXqbXrbXtbXxbX{bX!QbX!UbX!]bX!bbX!ebX!fbX!ibX!jbX!lbX!mbX!nbX!obX!pbX!qbX!rbX!{bX#ObXVbX|bX}bX!YbXvbX!XbX~OUaXhaXiaXjaXkaXlaXmaXoaXpaXqaXraXtaXxaX{aX!QaX!UaX!]aX!baX!eaX!jaX!laX!maX!naX!oaX!paX!qaX!raX!{aX#OaX|aX}aX!YaXvaX!XaX~O!f!WO!i!WO~P,_Oo!YOp!YOq!YOr!YO!l!YO!m!YOU`Xh`Xi`Xj`Xk`Xl`Xm`Xt`Xx`X{`X!Q`X!U`X!]`X!b`X!e`X!f`X!n`X!o`X!p`X!q`X!r`X!{`X#O`X~O!j!XO~P.uOU^Xh^Xi^Xj^Xk^Xl^Xm^Xo^Xp^Xq^Xr^Xt^Xx^X{^X!Q^X!U^X!]^X!b^X!e^X!f^X!o^X!p^X!q^X!r^X!{^X#O^X~O!n![O~P0yOU]Xh]Xi]Xj]Xk]Xl]Xm]Xo]Xp]Xq]Xr]Xt]Xx]X{]X!Q]X!U]X!]]X!b]X!e]X!f]X!p]X!q]X!r]X!{]X#O]X~O!o!]O~P2tOU[Xh[Xi[Xj[Xk[Xl[Xm[Xo[Xp[Xq[Xr[Xt[Xx[X{[X!Q[X!U[X!][X!b[X!e[X!f[X!q[X!r[X!{[X#O[X~O!p!^O~P4lOUZXhZXiZXjZXkZXlZXmZXoZXpZXqZXrZXtZXxZX{ZX!QZX!UZX!]ZX!bZX!eZX!fZX!{ZX#OZX~O!q!_O!r!`O~P6aO#O!aOUXXhXXiXXjXXkXXlXXmXXoXXpXXqXXrXXtXXxXX{XX!QXX!UXX!]XX!bXX!eXX!fXX!{XX~OW!cO~Ox!dOURXhRXiRXjRXkRXlRXmRXoRXpRXqRXrRXtRX{RX!QRX!URX!]RX!bRX!eRX!fRX!{RX~OU!fO~OVmOWTa~OVmOWTX~Ov!kOx!iO~O|!mO}!oO~Ox!pO~OUpO!S!RX~O!S!rO~O!X!sOVfX!YfX~P!gO|!tO!Y!VX~O!Z!vO~O!Y!wO~O!Y!xO~Ot!yO~O!Y!{O~P$zOU!}O~Ov#aO~OUpOu#cOv#dO~Ov#dOx#fO~O|#iO}#hO~O}#kO~P$zO|#lO}#kO~OU#pO!Y!Va~O|#rO!Y!Va~O!Z#uO~Ov#xO!`#vO~P$zO|#zO!Y!dX~O!Y#|O~O!g!VO!h!VOUbihbiibijbikbilbimbiobipbiqbirbitbixbi{bi!Qbi!Ubi!]bi!bbi!ebi!fbi!ibi!jbi!lbi!mbi!nbi!obi!pbi!qbi!rbi!{bi#Obi|bi}bi!Ybivbi!Xbi~O!i!WOoaipaiqairaitaixai!jai!lai!mai!nai!oai!pai!qai!rai#Oai~OUaihaiiaijaikailaimai{ai!Qai!Uai!]ai!bai!eai!fai!{ai~PAzOt`ix`i!n`i!o`i!p`i!q`i!r`i#O`i~O!j!XOU`ih`ii`ij`ik`il`im`io`ip`iq`ir`i{`i!Q`i!U`i!]`i!b`i!e`i!f`i!{`i~PDROt^ix^i!o^i!p^i!q^i!r^i#O^i~O!n![OU^ih^ii^ij^ik^il^im^io^ip^iq^ir^i{^i!Q^i!U^i!]^i!b^i!e^i!f^i!{^i~PFPOt]ix]i!p]i!q]i!r]i#O]i~O!o!]OU]ih]ii]ij]ik]il]im]io]ip]iq]ir]i{]i!Q]i!U]i!]]i!b]i!e]i!f]i!{]i~PGzOt[ix[i!q[i!r[i#O[i~O!p!^OU[ih[ii[ij[ik[il[im[io[ip[iq[ir[i{[i!Q[i!U[i!][i!b[i!e[i!f[i!{[i~PIrOv$OOx$PO~OUpOu#cOv$OO~O}$SO~O}$TO~P$zO!X!sO~OU#pO!Y!Vi~O!X$XO~Ov$[Ox$YO~O!Y!da~P$zO|$]O!Y!da~Ov$^O~Ov$^Ox$_O~OUpO~OU#pO~Ov$bO!`#vO~P$zOv$bOx$cO~O!Y!di~P$zOv$eO~Ov$fO!`#vO~P$zO!f!WO|ai}ai!Yaivai!Xai~PAzO!f$hO!i$hOVaX!gaX!haX~P,_O!j$iOV`X!g`X!h`X!i`X|`X}`X!Y`Xv`X!X`X~P.uO!o$kOV]X!g]X!h]X!i]X!j]X!l]X!m]X!n]X|]X}]X!Y]Xv]X!X]X~P2tO!p$lOV[X!g[X!h[X!i[X!j[X!l[X!m[X!n[X!o[X|[X}[X!Y[Xv[X!X[X~P4lO!q$mO!r$vOVZX!gZX!hZX!iZX!jZX!lZX!mZX!nZX!oZX!pZX|ZX}ZX!YZXvZX!XZX~P6aO!j$tO|`i}`i!Y`iv`i!X`i~PDROo!YOp!YOq!YOr!YO!j$tO!l!YO!m!YO|`X}`X!n`X!o`X!p`X!q`X!r`X#O`X!Y`Xt`Xv`Xx`X!X`X~O!n$uOV^X!g^X!h^X!i^X!j^X!l^X!m^X|^X}^X!Y^Xv^X!X^X~P0yOVXX!gXX!hXX!iXX!jXX!lXX!mXX!nXX!oXX!pXX!qXX!rXX|XX}XX!YXXvXX!XXX~P8RO!n$}O|^i}^i!Y^iv^i!X^i~PFPO!o%OO|]i}]i!Y]iv]i!X]i~PGzO!p%PO|[i}[i!Y[iv[i!X[i~PIrO!n$}O|^X}^X!o^X!p^X!q^X!r^X#O^X!Y^Xt^Xv^Xx^X!X^X~O!o%OO|]X}]X!p]X!q]X!r]X#O]X!Y]Xt]Xv]Xx]X!X]X~O!p%PO|[X}[X!q[X!r[X#O[X!Y[Xt[Xv[Xx[X!X[X~O!q%QO!r%RO|ZX}ZX#OZX!YZXtZXvZXxZX!XZX~O#O!aO|XX}XX!YXXtXXvXXxXX!XXX~OW%TO~O!Q!S!]!b!rklji!`Uk~",
|
||||||
goto: "3l!uPPP!v!z#[PPP#h$m%]%}&x'v(w)s*z,S-Y.b/f0jPPPPPP0jPPPP0jPPP0jPPP0jP1nP0jP1q1tPPP0jP1|2UP0jP2[2_PPPPPP2bPPPPPPP2k2q2x3O3Y3`3fTjOkSiOkQqSStUuV#X!c#Z#qShOk]$tSUu!c#Z#qSiOkQnRQsTQ{VQ|WQ!s!OS#Q!^$lY#]!f#_#m#s#zQ#a!kQ#b!lQ#g!oW#i!r#w$O$RQ#u#hR#{#v!OgORTVWk!O!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$lV#P!]$b$kWfOk!]!^Y$_R!k!o#h$bs$sTVW!O!f!l!r#_#m#s#v#w#z$O$R$k$lWeOk!]!^Q#O![Q#W$XY$^R!k!o#h$bQ$o$js$rTVW!O!f!l!r#_#m#s#v#w#z$O$R$k$lYdOk![!]!^Q!}!ZQ#V$W[$]R!k!o#h$X$bQ$n$iu$qTVW!O!f!l!r#_#m#s#v#w#z$O$R$j$k$l[cOk!Z![!]!^Q!|!YQ#U$V^$eR!k!o#h$W$X$bQ$m$hw$pTVW!O!f!l!r#_#m#s#v#w#z$O$R$i$j$k$l!hbORTVWk!O!Y!Z![!]!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$V$W$X$b$h$i$j$k$lV!{!X$a$g`aOk!X!Y!Z![!]!^Q!z!WQ#T$Ub$[R!k!o#h$V$W$X$a$bQ$c$f{$dTVW!O!f!l!r#_#m#s#v#w#z$O$R$g$h$i$j$k$l!``OTVWk!O!W!X!Y!Z![!]!^!f!l!r#_#m#s#v#w#z$O$R$f$g$h$i$j$k$lQ!y!UQ#S$TQ$Y$`e$ZR!k!o#h$U$V$W$X$a$b!z_ORTVWk!O!U!W!X!Y!Z![!]!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$T$U$V$W$X$`$a$b$f$g$h$i$j$k$lQ!x!TR#R$S#O^ORTVWk!O!T!U!W!X!Y!Z![!]!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$S$T$U$V$W$X$`$a$b$f$g$h$i$j$k$lQ!R]R!w!S#T[ORTVW]k!O!S!T!U!W!X!Y!Z![!]!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$S$T$U$V$W$X$`$a$b$f$g$h$i$j$k$l#TZORTVW]k!O!S!T!U!W!X!Y!Z![!]!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$S$T$U$V$W$X$`$a$b$f$g$h$i$j$k$l#TYORTVW]k!O!S!T!U!W!X!Y!Z![!]!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$S$T$U$V$W$X$`$a$b$f$g$h$i$j$k$lRvURzVQxVV#d!m#e#tQ#l!rV#|#w$O$RX#j!r#w$O$RR!P[R!u!OQ!WaQ$U$[R$f$dQkOR!`kSmPoR!bmQ!dqR#[!dQ!gsS#`!g#nR#n!sQuUR!juQ!nxR#f!nQ#x#lR$P#x",
|
goto: "6p!{PPP!|#Q#bPPP#n$|%n&f'^(_)c*j+l,y.X/e0s1}3XPPPPPP3XPPPP3XPPP3XP4c3XPPP3X3XP4iP3XP4l4oPPP3XP4w5PP3XP5V5YPPPPPP5]PPPPPPP5f5o5u5|6S6^6d6jTkOlSjOlQsSSwUxV#b!i#f$RSiOl]%^SUx!i#f$RSjOlQoRQvTQ!OVQ!PWQ!hqQ!ltQ!z!RS#Y!c%TY#j!m#l#z$U$]Q#n!rQ#o!sQ#t!vW#v!y$Y$c$gQ#}#cQ$W#uR$`$XUhOl!cW${R!r!v#uw%]TVWqt!R!m!s!y#c#l#z$U$X$Y$]$c$g%T!UgORTVWlqt!R!c!m!r!s!v!y#c#l#u#z$U$X$Y$]$c$g%TU#W!`$v%RV#X!b$w%SYfOl!`!b!c[$sR!r!v#u$v$w{%[TVWqt!R!m!s!y#c#l#z$U$X$Y$]$c$g%R%S%TYeOl!`!b!cQ#V!_Q#`$m[$rR!r!v#u$v$wQ%W%Q{%ZTVWqt!R!m!s!y#c#l#z$U$X$Y$]$c$g%R%S%T[dOl!_!`!b!cQ#U!^Q#_$l^$qR!r!v#u$m$v$wQ%V%P}%YTVWqt!R!m!s!y#c#l#z$U$X$Y$]$c$g%Q%R%S%T^cOl!^!_!`!b!cQ#T!]Q#^$k`$zR!r!v#u$l$m$v$wQ%U%O!P%XTVWqt!R!m!s!y#c#l#z$U$X$Y$]$c$g%P%Q%R%S%T!tbORTVWlqt!R!]!^!_!`!b!c!m!r!s!v!y#c#l#u#z$U$X$Y$]$c$g$k$l$m$v$w%O%P%Q%R%S%TV#S![$u$}baOl![!]!^!_!`!b!cQ#R!ZQ#]$jd$pR!r!v#u$k$l$m$u$v$wQ$x$|!T$yTVWqt!R!m!s!y#c#l#z$U$X$Y$]$c$g$}%O%P%Q%R%S%T!j`OTVWlqt!R!Z![!]!^!_!`!b!c!m!s!y#c#l#z$U$X$Y$]$c$g$|$}%O%P%Q%R%S%TQ#Q!XQ#[$iQ$n$tg$oR!r!v#u$j$k$l$m$u$v$w#W_ORTVWlqt!R!X!Z![!]!^!_!`!b!c!m!r!s!v!y#c#l#u#z$U$X$Y$]$c$g$i$j$k$l$m$t$u$v$w$|$}%O%P%Q%R%S%TQ#P!WR#Z$h#[^ORTVWlqt!R!W!X!Z![!]!^!_!`!b!c!m!r!s!v!y#c#l#u#z$U$X$Y$]$c$g$h$i$j$k$l$m$t$u$v$w$|$}%O%P%Q%R%S%TQ!U]R#O!V#a[ORTVW]lqt!R!V!W!X!Z![!]!^!_!`!b!c!m!r!s!v!y#c#l#u#z$U$X$Y$]$c$g$h$i$j$k$l$m$t$u$v$w$|$}%O%P%Q%R%S%T#aZORTVW]lqt!R!V!W!X!Z![!]!^!_!`!b!c!m!r!s!v!y#c#l#u#z$U$X$Y$]$c$g$h$i$j$k$l$m$t$u$v$w$|$}%O%P%Q%R%S%T#aYORTVW]lqt!R!V!W!X!Z![!]!^!_!`!b!c!m!r!s!v!y#c#l#u#z$U$X$Y$]$c$g$h$i$j$k$l$m$t$u$v$w$|$}%O%P%Q%R%S%TQ#e!iR$Q#fRyUR}VQ{VV#q!t#r$VQ#y!yV$a$Y$c$gX#w!y$Y$c$gR!S[R!|!RQ!ZaQ$j$pR$|$yQ!bhQ$w${R%S%]QlOR!elSnPpR!gnQ!jsR#g!jQ!nvS#m!n#{R#{!zQxUR!qxQ!u{R#s!uQ$Z#yR$d$Z",
|
||||||
nodeNames: "⚠ Comment Source Statement FieldDefinition FieldPath Identifier Dot Equal Expression DefaultExpression PatchExpression ComposeExpression LogicalOrExpression LogicalAndExpression ComparisonExpression ConcatExpression AdditiveExpression MultiplicativeExpression UnaryExpression PostfixExpression PrimaryExpression Literal String Integer Float True False Regex ComparisonConstraint Lt Lte Gt Gte Object LBrace Semicolon RBrace Array LBracket Comma RBracket LetExpression Let FieldDefinitionList In FunctionExpression LParen ParameterList Parameter Colon RParen Arrow MatchExpression Match MatchArm Pattern Underscore ImportExpression Import CallSuffix ArgumentList Bang Minus Star Slash Plus PlusPlus CompareOperator EqualEqual BangEqual AmpAmp PipePipe Amp SlashSlash Default",
|
nodeNames: "⚠ Comment Source Statement FieldDefinition FieldPath Identifier Dot Equal Expression AsExpression DefaultExpression PatchExpression ComposeExpression LogicalOrExpression LogicalAndExpression ComparisonExpression ConcatExpression AdditiveExpression MultiplicativeExpression UnaryExpression PostfixExpression PrimaryExpression Literal String Integer Float True False Regex ComparisonConstraint Lt Lte Gt Gte MapConstraint LBrace Ellipsis RBrace Object Semicolon ObjectRest ArrayConstraint LBracket Comma RBracket Array LetExpression Let FieldDefinitionList In FunctionExpression LParen ParameterList Parameter Colon RParen Arrow MatchExpression Match MatchArm Pattern Underscore ImportExpression Import CallSuffix ArgumentList Bang Minus Star Slash Plus PlusPlus CompareOperator EqualEqual BangEqual AmpAmp PipePipe Amp SlashSlash Default As",
|
||||||
maxTerm: 85,
|
maxTerm: 92,
|
||||||
skippedNodes: [0,1],
|
skippedNodes: [0,1],
|
||||||
repeatNodeCount: 7,
|
repeatNodeCount: 7,
|
||||||
tokenData: "=T~R!PX^$Upq$Uqr$yrs%Wst&zvw'cxy'pyz'uz{'z{|(P|}(^}!O(c!O!P(h!P!Q(m!Q![+T![!]+n!]!^+s!^!_+x!_!`,V!`!a,l!c!},y!}#O-[#P#Q-a#R#S-f#T#W,y#W#X-y#X#Y,y#Y#Z1Z#Z#],y#]#^3k#^#`,y#`#a6}#a#b8b#b#h,y#h#i:r#i#o,y#o#p<n#p#q<s#q#r=O#y#z$U$f$g$U#BY#BZ$U$IS$I_$U$I|$JO$U$JT$JU$U$KV$KW$U&FU&FV$U~$ZY!w~X^$Upq$U#y#z$U$f$g$U#BY#BZ$U$IS$I_$U$I|$JO$U$JT$JU$U$KV$KW$U&FU&FV$U~%OP!`~!_!`%R~%WO!h~~%ZWOY%WZr%Wrs%ss#O%W#O#P%x#P;'S%W;'S;=`&t<%lO%W~%xOg~~%{RO;'S%W;'S;=`&U;=`O%W~&XXOY%WZr%Wrs%ss#O%W#O#P%x#P;'S%W;'S;=`&t;=`<%l%W<%lO%W~&wP;=`<%l%W~'PSP~OY&zZ;'S&z;'S;=`']<%lO&z~'`P;=`<%l&z~'hP!k~vw'k~'pO!i~~'uO!P~~'zO!T~~(PO!b~~(UP!d~{|(X~(^O!e~~(cOx~~(hO!a~~(mOV~~(rW!c~OY)[Z!P)[!P!Q+O!Q#O)[#O#P)|#P;'S)[;'S;=`*x<%lO)[~)_WOY)[Z!P)[!P!Q)w!Q#O)[#O#P)|#P;'S)[;'S;=`*x<%lO)[~)|Ol~~*PRO;'S)[;'S;=`*Y;=`O)[~*]XOY)[Z!P)[!P!Q)w!Q#O)[#O#P)|#P;'S)[;'S;=`*x;=`<%l)[<%lO)[~*{P;=`<%l)[~+TO!l~~+YQh~!O!P+`!Q![+T~+cP!Q![+f~+kPi~!Q![+f~+sO!S~~+xOt~~+}Pn~!_!`,Q~,VOo~~,[QWP!_!`,b!`!a,g~,gO!g~Q,lO!UQ~,qPp~!_!`,t~,yOq~~-OSU~!Q![,y!c!},y#R#S,y#T#o,y~-aOw~~-fOy~~-mS!Z~U~!Q![,y!c!},y#R#S,y#T#o,y~.OUU~!Q![,y!c!},y#R#S,y#T#X,y#X#Y.b#Y#o,y~.gUU~!Q![,y!c!},y#R#S,y#T#Y,y#Y#Z.y#Z#o,y~/OTU~!Q![,y!c!},y#R#S,y#T#U/_#U#o,y~/dUU~!Q![,y!c!},y#R#S,y#T#i,y#i#j/v#j#o,y~/{UU~!Q![,y!c!},y#R#S,y#T#`,y#`#a0_#a#o,y~0dUU~!Q![,y!c!},y#R#S,y#T#h,y#h#i0v#i#o,y~0}S!m~U~!Q![,y!c!},y#R#S,y#T#o,y~1`TU~!Q![,y!c!},y#R#S,y#T#U1o#U#o,y~1tUU~!Q![,y!c!},y#R#S,y#T#`,y#`#a2W#a#o,y~2]UU~!Q![,y!c!},y#R#S,y#T#g,y#g#h2o#h#o,y~2tUU~!Q![,y!c!},y#R#S,y#T#X,y#X#Y3W#Y#o,y~3_Sk~U~!Q![,y!c!},y#R#S,y#T#o,y~3pVU~!Q![,y!c!},y#R#S,y#T#a,y#a#b4V#b#c6j#c#o,y~4[UU~!Q![,y!c!},y#R#S,y#T#d,y#d#e4n#e#o,y~4sUU~!Q![,y!c!},y#R#S,y#T#c,y#c#d5V#d#o,y~5[UU~!Q![,y!c!},y#R#S,y#T#f,y#f#g5n#g#o,y~5sUU~!Q![,y!c!},y#R#S,y#T#h,y#h#i6V#i#o,y~6^S!]~U~!Q![,y!c!},y#R#S,y#T#o,y~6qS}~U~!Q![,y!c!},y#R#S,y#T#o,y~7SUU~!Q![,y!c!},y#R#S,y#T#X,y#X#Y7f#Y#o,y~7kUU~!Q![,y!c!},y#R#S,y#T#h,y#h#i7}#i#o,y~8US{~U~!Q![,y!c!},y#R#S,y#T#o,y~8gTU~!Q![,y!c!},y#R#S,y#T#U8v#U#o,y~8{UU~!Q![,y!c!},y#R#S,y#T#h,y#h#i9_#i#o,y~9dUU~!Q![,y!c!},y#R#S,y#T#V,y#V#W9v#W#o,y~9{UU~!Q![,y!c!},y#R#S,y#T#[,y#[#]:_#]#o,y~:fS!W~U~!Q![,y!c!},y#R#S,y#T#o,y~:wUU~!Q![,y!c!},y#R#S,y#T#f,y#f#g;Z#g#o,y~;`UU~!Q![,y!c!},y#R#S,y#T#i,y#i#j;r#j#o,y~;wUU~!Q![,y!c!},y#R#S,y#T#X,y#X#Y<Z#Y#o,y~<bSj~U~!Q![,y!c!},y#R#S,y#T#o,y~<sOs~~<vP#p#q<y~=OO!j~~=TOu~",
|
tokenData: "=c~R!PX^$Upq$Uqr$yrs%Wst&zvw'cxy'pyz'uz{'z{|(P|}(^}!O(c!O!P(h!P!Q({!Q![+c![!]+|!]!^,R!^!_,W!_!`,e!`!a,z!c!}-X!}#O-j#P#Q-o#R#S-t#T#W-X#W#X.X#X#Y-X#Y#Z1i#Z#]-X#]#^3y#^#`-X#`#a7]#a#b8p#b#h-X#h#i;Q#i#o-X#o#p<|#p#q=R#q#r=^#y#z$U$f$g$U#BY#BZ$U$IS$I_$U$I|$JO$U$JT$JU$U$KV$KW$U&FU&FV$U~$ZY!}~X^$Upq$U#y#z$U$f$g$U#BY#BZ$U$IS$I_$U$I|$JO$U$JT$JU$U$KV$KW$U&FU&FV$U~%OP!e~!_!`%R~%WO!m~~%ZWOY%WZr%Wrs%ss#O%W#O#P%x#P;'S%W;'S;=`&t<%lO%W~%xOh~~%{RO;'S%W;'S;=`&U;=`O%W~&XXOY%WZr%Wrs%ss#O%W#O#P%x#P;'S%W;'S;=`&t;=`<%l%W<%lO%W~&wP;=`<%l%W~'PSP~OY&zZ;'S&z;'S;=`']<%lO&z~'`P;=`<%l&z~'hP!p~vw'k~'pO!n~~'uO!U~~'zO!Y~~(PO!g~~(UP!i~{|(X~(^O!j~~(cO|~~(hO!f~R(mPVP!O!P(pQ(sP!O!P(vQ({OuQ~)QW!h~OY)jZ!P)j!P!Q+^!Q#O)j#O#P*[#P;'S)j;'S;=`+W<%lO)j~)mWOY)jZ!P)j!P!Q*V!Q#O)j#O#P*[#P;'S)j;'S;=`+W<%lO)j~*[Om~~*_RO;'S)j;'S;=`*h;=`O)j~*kXOY)jZ!P)j!P!Q*V!Q#O)j#O#P*[#P;'S)j;'S;=`+W;=`<%l)j<%lO)j~+ZP;=`<%l)j~+cO!q~~+hQi~!O!P+n!Q![+c~+qP!Q![+t~+yPj~!Q![+t~,RO!X~~,WOx~~,]Po~!_!`,`~,eOp~~,jQWP!_!`,p!`!a,u~,uO!l~Q,zO!ZQ~-PPq~!_!`-S~-XOr~~-^SU~!Q![-X!c!}-X#R#S-X#T#o-X~-oO{~~-tO}~~-{S!`~U~!Q![-X!c!}-X#R#S-X#T#o-X~.^UU~!Q![-X!c!}-X#R#S-X#T#X-X#X#Y.p#Y#o-X~.uUU~!Q![-X!c!}-X#R#S-X#T#Y-X#Y#Z/X#Z#o-X~/^TU~!Q![-X!c!}-X#R#S-X#T#U/m#U#o-X~/rUU~!Q![-X!c!}-X#R#S-X#T#i-X#i#j0U#j#o-X~0ZUU~!Q![-X!c!}-X#R#S-X#T#`-X#`#a0m#a#o-X~0rUU~!Q![-X!c!}-X#R#S-X#T#h-X#h#i1U#i#o-X~1]S!r~U~!Q![-X!c!}-X#R#S-X#T#o-X~1nTU~!Q![-X!c!}-X#R#S-X#T#U1}#U#o-X~2SUU~!Q![-X!c!}-X#R#S-X#T#`-X#`#a2f#a#o-X~2kUU~!Q![-X!c!}-X#R#S-X#T#g-X#g#h2}#h#o-X~3SUU~!Q![-X!c!}-X#R#S-X#T#X-X#X#Y3f#Y#o-X~3mSl~U~!Q![-X!c!}-X#R#S-X#T#o-X~4OVU~!Q![-X!c!}-X#R#S-X#T#a-X#a#b4e#b#c6x#c#o-X~4jUU~!Q![-X!c!}-X#R#S-X#T#d-X#d#e4|#e#o-X~5RUU~!Q![-X!c!}-X#R#S-X#T#c-X#c#d5e#d#o-X~5jUU~!Q![-X!c!}-X#R#S-X#T#f-X#f#g5|#g#o-X~6RUU~!Q![-X!c!}-X#R#S-X#T#h-X#h#i6e#i#o-X~6lS!b~U~!Q![-X!c!}-X#R#S-X#T#o-X~7PS!S~U~!Q![-X!c!}-X#R#S-X#T#o-X~7bUU~!Q![-X!c!}-X#R#S-X#T#X-X#X#Y7t#Y#o-X~7yUU~!Q![-X!c!}-X#R#S-X#T#h-X#h#i8]#i#o-X~8dS!Q~U~!Q![-X!c!}-X#R#S-X#T#o-X~8uTU~!Q![-X!c!}-X#R#S-X#T#U9U#U#o-X~9ZUU~!Q![-X!c!}-X#R#S-X#T#h-X#h#i9m#i#o-X~9rUU~!Q![-X!c!}-X#R#S-X#T#V-X#V#W:U#W#o-X~:ZUU~!Q![-X!c!}-X#R#S-X#T#[-X#[#]:m#]#o-X~:tS!]~U~!Q![-X!c!}-X#R#S-X#T#o-X~;VUU~!Q![-X!c!}-X#R#S-X#T#f-X#f#g;i#g#o-X~;nUU~!Q![-X!c!}-X#R#S-X#T#i-X#i#j<Q#j#o-X~<VUU~!Q![-X!c!}-X#R#S-X#T#X-X#X#Y<i#Y#o-X~<pSk~U~!Q![-X!c!}-X#R#S-X#T#o-X~=ROt~~=UP#p#q=X~=^O!o~~=cOv~",
|
||||||
tokenizers: [0, 1],
|
tokenizers: [0, 1],
|
||||||
topRules: {"Source":[0,2]},
|
topRules: {"Source":[0,2]},
|
||||||
tokenPrec: 2481
|
specialized: [{term: 6, get: (value) => spec_Identifier[value] || -1}],
|
||||||
|
tokenPrec: 2740
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -9,69 +9,75 @@ export const
|
|||||||
Dot = 7,
|
Dot = 7,
|
||||||
Equal = 8,
|
Equal = 8,
|
||||||
Expression = 9,
|
Expression = 9,
|
||||||
DefaultExpression = 10,
|
AsExpression = 10,
|
||||||
PatchExpression = 11,
|
DefaultExpression = 11,
|
||||||
ComposeExpression = 12,
|
PatchExpression = 12,
|
||||||
LogicalOrExpression = 13,
|
ComposeExpression = 13,
|
||||||
LogicalAndExpression = 14,
|
LogicalOrExpression = 14,
|
||||||
ComparisonExpression = 15,
|
LogicalAndExpression = 15,
|
||||||
ConcatExpression = 16,
|
ComparisonExpression = 16,
|
||||||
AdditiveExpression = 17,
|
ConcatExpression = 17,
|
||||||
MultiplicativeExpression = 18,
|
AdditiveExpression = 18,
|
||||||
UnaryExpression = 19,
|
MultiplicativeExpression = 19,
|
||||||
PostfixExpression = 20,
|
UnaryExpression = 20,
|
||||||
PrimaryExpression = 21,
|
PostfixExpression = 21,
|
||||||
Literal = 22,
|
PrimaryExpression = 22,
|
||||||
String = 23,
|
Literal = 23,
|
||||||
Integer = 24,
|
String = 24,
|
||||||
Float = 25,
|
Integer = 25,
|
||||||
True = 26,
|
Float = 26,
|
||||||
False = 27,
|
True = 27,
|
||||||
Regex = 28,
|
False = 28,
|
||||||
ComparisonConstraint = 29,
|
Regex = 29,
|
||||||
Lt = 30,
|
ComparisonConstraint = 30,
|
||||||
Lte = 31,
|
Lt = 31,
|
||||||
Gt = 32,
|
Lte = 32,
|
||||||
Gte = 33,
|
Gt = 33,
|
||||||
Object = 34,
|
Gte = 34,
|
||||||
LBrace = 35,
|
MapConstraint = 35,
|
||||||
Semicolon = 36,
|
LBrace = 36,
|
||||||
RBrace = 37,
|
Ellipsis = 37,
|
||||||
Array = 38,
|
RBrace = 38,
|
||||||
LBracket = 39,
|
Object = 39,
|
||||||
Comma = 40,
|
Semicolon = 40,
|
||||||
RBracket = 41,
|
ObjectRest = 41,
|
||||||
LetExpression = 42,
|
ArrayConstraint = 42,
|
||||||
Let = 43,
|
LBracket = 43,
|
||||||
FieldDefinitionList = 44,
|
Comma = 44,
|
||||||
In = 45,
|
RBracket = 45,
|
||||||
FunctionExpression = 46,
|
Array = 46,
|
||||||
LParen = 47,
|
LetExpression = 47,
|
||||||
ParameterList = 48,
|
Let = 48,
|
||||||
Parameter = 49,
|
FieldDefinitionList = 49,
|
||||||
Colon = 50,
|
In = 50,
|
||||||
RParen = 51,
|
FunctionExpression = 51,
|
||||||
Arrow = 52,
|
LParen = 52,
|
||||||
MatchExpression = 53,
|
ParameterList = 53,
|
||||||
Match = 54,
|
Parameter = 54,
|
||||||
MatchArm = 55,
|
Colon = 55,
|
||||||
Pattern = 56,
|
RParen = 56,
|
||||||
Underscore = 57,
|
Arrow = 57,
|
||||||
ImportExpression = 58,
|
MatchExpression = 58,
|
||||||
Import = 59,
|
Match = 59,
|
||||||
CallSuffix = 60,
|
MatchArm = 60,
|
||||||
ArgumentList = 61,
|
Pattern = 61,
|
||||||
Bang = 62,
|
Underscore = 62,
|
||||||
Minus = 63,
|
ImportExpression = 63,
|
||||||
Star = 64,
|
Import = 64,
|
||||||
Slash = 65,
|
CallSuffix = 65,
|
||||||
Plus = 66,
|
ArgumentList = 66,
|
||||||
PlusPlus = 67,
|
Bang = 67,
|
||||||
CompareOperator = 68,
|
Minus = 68,
|
||||||
EqualEqual = 69,
|
Star = 69,
|
||||||
BangEqual = 70,
|
Slash = 70,
|
||||||
AmpAmp = 71,
|
Plus = 71,
|
||||||
PipePipe = 72,
|
PlusPlus = 72,
|
||||||
Amp = 73,
|
CompareOperator = 73,
|
||||||
SlashSlash = 74,
|
EqualEqual = 74,
|
||||||
Default = 75
|
BangEqual = 75,
|
||||||
|
AmpAmp = 76,
|
||||||
|
PipePipe = 77,
|
||||||
|
Amp = 78,
|
||||||
|
SlashSlash = 79,
|
||||||
|
Default = 80,
|
||||||
|
As = 81
|
||||||
|
|||||||
@@ -1,32 +1,36 @@
|
|||||||
import { LRLanguage, LanguageSupport, HighlightStyle, syntaxHighlighting, foldNodeProp, indentNodeProp } from '@codemirror/language';
|
import { LRLanguage, LanguageSupport, HighlightStyle, syntaxHighlighting, delimitedIndent, foldNodeProp, indentNodeProp } from '@codemirror/language';
|
||||||
import { styleTags, tags as t } from '@lezer/highlight';
|
import { styleTags, tags as t } from '@lezer/highlight';
|
||||||
import { parser } from './decodal-parser.js';
|
import { parser } from './decodal-parser.js';
|
||||||
|
|
||||||
const parserWithMetadata = parser.configure({
|
const parserWithMetadata = parser.configure({
|
||||||
props: [
|
props: [
|
||||||
styleTags({
|
styleTags({
|
||||||
'Let In Match Import Default': t.keyword,
|
'Let In Match Import Default As': t.keyword,
|
||||||
'True False': t.bool,
|
'True False': t.bool,
|
||||||
Identifier: t.variableName,
|
Identifier: t.variableName,
|
||||||
String: t.string,
|
String: t.string,
|
||||||
Regex: t.regexp,
|
Regex: t.regexp,
|
||||||
'Integer Float': t.number,
|
'Integer Float': t.number,
|
||||||
Comment: t.lineComment,
|
Comment: t.lineComment,
|
||||||
'Plus Minus Star Slash PlusPlus Equal EqualEqual Bang BangEqual Amp AmpAmp PipePipe SlashSlash Gt Gte Lt Lte Arrow': t.operator,
|
'Plus Minus Star Slash PlusPlus Ellipsis Equal EqualEqual Bang BangEqual Amp AmpAmp PipePipe SlashSlash Gt Gte Lt Lte Arrow': t.operator,
|
||||||
'LBrace RBrace': t.brace,
|
'LBrace RBrace': t.brace,
|
||||||
'LBracket RBracket': t.squareBracket,
|
'LBracket RBracket': t.squareBracket,
|
||||||
'LParen RParen': t.paren,
|
'LParen RParen': t.paren,
|
||||||
'Semicolon Comma Dot Colon': t.punctuation,
|
'Semicolon Comma Dot Colon': t.punctuation,
|
||||||
}),
|
}),
|
||||||
indentNodeProp.add({
|
indentNodeProp.add({
|
||||||
Object: context => context.column(context.node.from) + context.unit,
|
Object: delimitedIndent({ closing: '}', align: false }),
|
||||||
Array: context => context.column(context.node.from) + context.unit,
|
MapConstraint: delimitedIndent({ closing: '}', align: false }),
|
||||||
MatchExpression: context => context.column(context.node.from) + context.unit,
|
Array: delimitedIndent({ closing: ']', align: false }),
|
||||||
|
ArrayConstraint: delimitedIndent({ closing: ']', align: false }),
|
||||||
|
MatchExpression: delimitedIndent({ closing: '}', align: false }),
|
||||||
LetExpression: context => context.column(context.node.from) + context.unit,
|
LetExpression: context => context.column(context.node.from) + context.unit,
|
||||||
}),
|
}),
|
||||||
foldNodeProp.add({
|
foldNodeProp.add({
|
||||||
Object: foldDelimited('{', '}'),
|
Object: foldDelimited('{', '}'),
|
||||||
|
MapConstraint: foldDelimited('{', '}'),
|
||||||
Array: foldDelimited('[', ']'),
|
Array: foldDelimited('[', ']'),
|
||||||
|
ArrayConstraint: foldDelimited('[', ']'),
|
||||||
MatchExpression: foldDelimited('{', '}'),
|
MatchExpression: foldDelimited('{', '}'),
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
|
||||||
|
import { getIndentation, indentUnit } from '@codemirror/language';
|
||||||
|
import { EditorState } from '@codemirror/state';
|
||||||
|
|
||||||
|
import { decodal } from './decodal.js';
|
||||||
|
import { parser } from './decodal-parser.js';
|
||||||
|
|
||||||
|
function parseErrors(source) {
|
||||||
|
const errors = [];
|
||||||
|
const cursor = parser.parse(source).cursor();
|
||||||
|
do {
|
||||||
|
if (cursor.type.isError) errors.push({ from: cursor.from, to: cursor.to });
|
||||||
|
} while (cursor.next());
|
||||||
|
return errors;
|
||||||
|
}
|
||||||
|
|
||||||
|
function indentationAt(doc, lineNumber) {
|
||||||
|
const state = EditorState.create({
|
||||||
|
doc,
|
||||||
|
extensions: [decodal(), indentUnit.of(' ')],
|
||||||
|
});
|
||||||
|
return getIndentation(state, state.doc.line(lineNumber).from);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('indents an inline object from its containing line', () => {
|
||||||
|
const doc = `hoge = fuga & piyo & {
|
||||||
|
|
||||||
|
}`;
|
||||||
|
|
||||||
|
assert.equal(indentationAt(doc, 2), 2);
|
||||||
|
assert.equal(indentationAt(doc, 3), 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('preserves the surrounding indentation for a nested inline object', () => {
|
||||||
|
const doc = `root = {
|
||||||
|
hoge = fuga & piyo & {
|
||||||
|
|
||||||
|
};
|
||||||
|
};`;
|
||||||
|
|
||||||
|
assert.equal(indentationAt(doc, 3), 4);
|
||||||
|
assert.equal(indentationAt(doc, 4), 2);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('parses map constraints and range refinement', () => {
|
||||||
|
const source = `services = {
|
||||||
|
api = { port = 8080; };
|
||||||
|
} as {...{ port = Int; }};`;
|
||||||
|
|
||||||
|
assert.deepEqual(parseErrors(source), []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('parses unknown and object rest constraints', () => {
|
||||||
|
const source = `value = {
|
||||||
|
hoge = Int;
|
||||||
|
fuga = String;
|
||||||
|
...Unknown
|
||||||
|
};`;
|
||||||
|
|
||||||
|
assert.deepEqual(parseErrors(source), []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('only treats the exact as keyword as an operator', () => {
|
||||||
|
assert.deepEqual(parseErrors('asset = 1; value = asset as Int;'), []);
|
||||||
|
});
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
import type { EditorView } from '@codemirror/view';
|
||||||
|
|
||||||
|
export interface FormatSuccess {
|
||||||
|
ok: true;
|
||||||
|
source: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FormatFailure {
|
||||||
|
ok: false;
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FormatResult = FormatSuccess | FormatFailure;
|
||||||
|
|
||||||
|
export declare function initDecodalFormatter(moduleOrPath?: RequestInfo | URL | Response | BufferSource | WebAssembly.Module): Promise<void>;
|
||||||
|
export declare function formatDecodal(source: string): FormatResult;
|
||||||
|
export declare function formatDecodalCommand(view: EditorView): boolean;
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import initLanguageTools, { formatSource } from '../wasm/decodal_language_tools.js';
|
||||||
|
|
||||||
|
let initialized = false;
|
||||||
|
|
||||||
|
export async function initDecodalFormatter(moduleOrPath) {
|
||||||
|
await initLanguageTools(moduleOrPath);
|
||||||
|
initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDecodal(source) {
|
||||||
|
if (!initialized) {
|
||||||
|
return { ok: false, error: 'Decodal formatter is not initialized' };
|
||||||
|
}
|
||||||
|
return JSON.parse(formatSource(source));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatDecodalCommand(view) {
|
||||||
|
const result = formatDecodal(view.state.doc.toString());
|
||||||
|
if (!result.ok) return false;
|
||||||
|
view.dispatch({
|
||||||
|
changes: {
|
||||||
|
from: 0,
|
||||||
|
to: view.state.doc.length,
|
||||||
|
insert: result.source,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
+17
@@ -0,0 +1,17 @@
|
|||||||
|
import type { EditorView } from '@codemirror/view';
|
||||||
|
|
||||||
|
export interface FormatSuccess {
|
||||||
|
ok: true;
|
||||||
|
source: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FormatFailure {
|
||||||
|
ok: false;
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FormatResult = FormatSuccess | FormatFailure;
|
||||||
|
|
||||||
|
export declare function initDecodalFormatter(moduleOrPath?: RequestInfo | URL | Response | BufferSource | WebAssembly.Module): Promise<void>;
|
||||||
|
export declare function formatDecodal(source: string): FormatResult;
|
||||||
|
export declare function formatDecodalCommand(view: EditorView): boolean;
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
|
||||||
|
import { formatDecodal, initDecodalFormatter } from './format.js';
|
||||||
|
|
||||||
|
const wasmPath = new URL('../wasm/decodal_language_tools_bg.wasm', import.meta.url);
|
||||||
|
|
||||||
|
test('WASM uses the canonical formatter and is idempotent', async () => {
|
||||||
|
await initDecodalFormatter({ module_or_path: await readFile(wasmPath) });
|
||||||
|
const source = 'Server={\n# host\nhost=String default "localhost"; # trailing\n\nport=Int&>0;\n};';
|
||||||
|
const expected = 'Server = {\n # host\n host = String default "localhost"; # trailing\n\n port = Int & > 0;\n};\n';
|
||||||
|
|
||||||
|
assert.deepEqual(formatDecodal(source), { ok: true, source: expected });
|
||||||
|
assert.deepEqual(formatDecodal(expected), { ok: true, source: expected });
|
||||||
|
});
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
/**
|
||||||
|
* Decodal source formatting helpers for CodeMirror integrations.
|
||||||
|
*
|
||||||
|
* The formatter is backed by WebAssembly generated from the canonical Rust
|
||||||
|
* `decodal-language-tools` formatter. Call {@link initDecodalFormatter} once before
|
||||||
|
* using {@link formatDecodal} or {@link formatDecodalCommand}.
|
||||||
|
*
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
import type { EditorView } from 'npm:@codemirror/view@^6.43.6';
|
||||||
|
import initLanguageTools, { formatSource } from '../wasm/decodal_language_tools.js';
|
||||||
|
|
||||||
|
let initialized = false;
|
||||||
|
|
||||||
|
/** Successful formatter result. */
|
||||||
|
export interface FormatSuccess {
|
||||||
|
/** Indicates that formatting succeeded. */
|
||||||
|
ok: true;
|
||||||
|
/** The formatted Decodal source text. */
|
||||||
|
source: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Failed formatter result. */
|
||||||
|
export interface FormatFailure {
|
||||||
|
/** Indicates that formatting failed. */
|
||||||
|
ok: false;
|
||||||
|
/** Human-readable formatter error. */
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Result returned by {@link formatDecodal}. */
|
||||||
|
export type FormatResult = FormatSuccess | FormatFailure;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the bundled Decodal formatter WebAssembly module.
|
||||||
|
*
|
||||||
|
* Bundlers such as Vite should pass the package wasm asset URL explicitly:
|
||||||
|
*
|
||||||
|
* ```ts
|
||||||
|
* import wasmUrl from 'decodal-codemirror/wasm/decodal_language_tools_bg.wasm?url';
|
||||||
|
* await initDecodalFormatter(wasmUrl);
|
||||||
|
* ```
|
||||||
|
*
|
||||||
|
* @param moduleOrPath - Optional WebAssembly module, bytes, response, URL, or
|
||||||
|
* request accepted by wasm-bindgen's generated initializer.
|
||||||
|
*/
|
||||||
|
export async function initDecodalFormatter(
|
||||||
|
moduleOrPath?: RequestInfo | URL | Response | BufferSource | WebAssembly.Module,
|
||||||
|
): Promise<void> {
|
||||||
|
await initLanguageTools(moduleOrPath);
|
||||||
|
initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a complete Decodal source document.
|
||||||
|
*
|
||||||
|
* @param source - Source text to format.
|
||||||
|
* @returns Either formatted source text or an error message.
|
||||||
|
*/
|
||||||
|
export function formatDecodal(source: string): FormatResult {
|
||||||
|
if (!initialized) {
|
||||||
|
return { ok: false, error: 'Decodal formatter is not initialized' };
|
||||||
|
}
|
||||||
|
return JSON.parse(formatSource(source)) as FormatResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* CodeMirror command that formats the whole document in-place.
|
||||||
|
*
|
||||||
|
* @param view - The editor view whose document should be formatted.
|
||||||
|
* @returns `true` when the document was formatted, otherwise `false`.
|
||||||
|
*/
|
||||||
|
export function formatDecodalCommand(view: EditorView): boolean {
|
||||||
|
const result = formatDecodal(view.state.doc.toString());
|
||||||
|
if (!result.ok) return false;
|
||||||
|
view.dispatch({
|
||||||
|
changes: {
|
||||||
|
from: 0,
|
||||||
|
to: view.state.doc.length,
|
||||||
|
insert: result.source,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -1,32 +1,46 @@
|
|||||||
import { HighlightStyle, LRLanguage, LanguageSupport, foldNodeProp, indentNodeProp, syntaxHighlighting } from 'npm:@codemirror/language@^6.12.4';
|
/**
|
||||||
|
* CodeMirror 6 language support for Decodal.
|
||||||
|
*
|
||||||
|
* This module exports the Decodal language extension, parser metadata, and the
|
||||||
|
* bundled highlight style used by the official Decodal playground. Add
|
||||||
|
* {@link decodal} to a CodeMirror extension set to enable highlighting,
|
||||||
|
* indentation, and folding for Decodal documents.
|
||||||
|
*
|
||||||
|
* @module
|
||||||
|
*/
|
||||||
|
import { HighlightStyle, LRLanguage, LanguageSupport, delimitedIndent, foldNodeProp, indentNodeProp, syntaxHighlighting } from 'npm:@codemirror/language@^6.12.4';
|
||||||
import { styleTags, tags as t } from 'npm:@lezer/highlight@^1.2.3';
|
import { styleTags, tags as t } from 'npm:@lezer/highlight@^1.2.3';
|
||||||
import { parser } from './decodal-parser-jsr.js';
|
import { parser } from './decodal-parser-jsr.js';
|
||||||
|
|
||||||
const parserWithMetadata = parser.configure({
|
const parserWithMetadata = parser.configure({
|
||||||
props: [
|
props: [
|
||||||
styleTags({
|
styleTags({
|
||||||
'Let In Match Import Default': t.keyword,
|
'Let In Match Import Default As': t.keyword,
|
||||||
'True False': t.bool,
|
'True False': t.bool,
|
||||||
Identifier: t.variableName,
|
Identifier: t.variableName,
|
||||||
String: t.string,
|
String: t.string,
|
||||||
Regex: t.regexp,
|
Regex: t.regexp,
|
||||||
'Integer Float': t.number,
|
'Integer Float': t.number,
|
||||||
Comment: t.lineComment,
|
Comment: t.lineComment,
|
||||||
'Plus Minus Star Slash PlusPlus Equal EqualEqual Bang BangEqual Amp AmpAmp PipePipe SlashSlash Gt Gte Lt Lte Arrow': t.operator,
|
'Plus Minus Star Slash PlusPlus Ellipsis Equal EqualEqual Bang BangEqual Amp AmpAmp PipePipe SlashSlash Gt Gte Lt Lte Arrow': t.operator,
|
||||||
'LBrace RBrace': t.brace,
|
'LBrace RBrace': t.brace,
|
||||||
'LBracket RBracket': t.squareBracket,
|
'LBracket RBracket': t.squareBracket,
|
||||||
'LParen RParen': t.paren,
|
'LParen RParen': t.paren,
|
||||||
'Semicolon Comma Dot Colon': t.punctuation,
|
'Semicolon Comma Dot Colon': t.punctuation,
|
||||||
}),
|
}),
|
||||||
indentNodeProp.add({
|
indentNodeProp.add({
|
||||||
Object: context => context.column(context.node.from) + context.unit,
|
Object: delimitedIndent({ closing: '}', align: false }),
|
||||||
Array: context => context.column(context.node.from) + context.unit,
|
MapConstraint: delimitedIndent({ closing: '}', align: false }),
|
||||||
MatchExpression: context => context.column(context.node.from) + context.unit,
|
Array: delimitedIndent({ closing: ']', align: false }),
|
||||||
|
ArrayConstraint: delimitedIndent({ closing: ']', align: false }),
|
||||||
|
MatchExpression: delimitedIndent({ closing: '}', align: false }),
|
||||||
LetExpression: context => context.column(context.node.from) + context.unit,
|
LetExpression: context => context.column(context.node.from) + context.unit,
|
||||||
}),
|
}),
|
||||||
foldNodeProp.add({
|
foldNodeProp.add({
|
||||||
Object: foldDelimited('{', '}'),
|
Object: foldDelimited('{', '}'),
|
||||||
|
MapConstraint: foldDelimited('{', '}'),
|
||||||
Array: foldDelimited('[', ']'),
|
Array: foldDelimited('[', ']'),
|
||||||
|
ArrayConstraint: foldDelimited('[', ']'),
|
||||||
MatchExpression: foldDelimited('{', '}'),
|
MatchExpression: foldDelimited('{', '}'),
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
@@ -44,6 +58,7 @@ function foldDelimited(open: string, close: string) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Decodal LR language definition for CodeMirror 6. */
|
||||||
export const decodalLanguage: LRLanguage = LRLanguage.define({
|
export const decodalLanguage: LRLanguage = LRLanguage.define({
|
||||||
parser: parserWithMetadata,
|
parser: parserWithMetadata,
|
||||||
languageData: {
|
languageData: {
|
||||||
@@ -52,6 +67,7 @@ export const decodalLanguage: LRLanguage = LRLanguage.define({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/** Default Decodal highlight style used by the playground. */
|
||||||
export const decodalHighlightStyle: HighlightStyle = HighlightStyle.define([
|
export const decodalHighlightStyle: HighlightStyle = HighlightStyle.define([
|
||||||
{ tag: t.keyword, color: '#93c5fd', fontWeight: '700' },
|
{ tag: t.keyword, color: '#93c5fd', fontWeight: '700' },
|
||||||
{ tag: t.variableName, color: 'inherit' },
|
{ tag: t.variableName, color: 'inherit' },
|
||||||
@@ -64,10 +80,19 @@ export const decodalHighlightStyle: HighlightStyle = HighlightStyle.define([
|
|||||||
{ tag: [t.brace, t.squareBracket, t.paren, t.punctuation], color: '#f9a8d4' },
|
{ tag: [t.brace, t.squareBracket, t.paren, t.punctuation], color: '#f9a8d4' },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
/** Options for {@link decodal}. */
|
||||||
export interface DecodalOptions {
|
export interface DecodalOptions {
|
||||||
|
/** Include the bundled {@link decodalHighlightStyle}. Defaults to `true`. */
|
||||||
highlight?: boolean;
|
highlight?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create CodeMirror language support for Decodal.
|
||||||
|
*
|
||||||
|
* @param options - Optional language support options.
|
||||||
|
* @returns A CodeMirror extension bundle containing the Decodal language and,
|
||||||
|
* unless disabled, the default highlight style.
|
||||||
|
*/
|
||||||
export function decodal(options: DecodalOptions = {}): LanguageSupport {
|
export function decodal(options: DecodalOptions = {}): LanguageSupport {
|
||||||
const { highlight = true } = options;
|
const { highlight = true } = options;
|
||||||
return new LanguageSupport(
|
return new LanguageSupport(
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
# wasm-pack output is committed as part of decodal-codemirror.
|
||||||
+2
-5
@@ -1,16 +1,13 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
export function evaluate(source: string): string;
|
export function formatSource(source: string): string;
|
||||||
|
|
||||||
export function evaluateProject(entry: string, files_json: string): string;
|
|
||||||
|
|
||||||
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
||||||
|
|
||||||
export interface InitOutput {
|
export interface InitOutput {
|
||||||
readonly memory: WebAssembly.Memory;
|
readonly memory: WebAssembly.Memory;
|
||||||
readonly evaluate: (a: number, b: number) => [number, number];
|
readonly formatSource: (a: number, b: number) => [number, number];
|
||||||
readonly evaluateProject: (a: number, b: number, c: number, d: number) => [number, number];
|
|
||||||
readonly __wbindgen_externrefs: WebAssembly.Table;
|
readonly __wbindgen_externrefs: WebAssembly.Table;
|
||||||
readonly __wbindgen_malloc: (a: number, b: number) => number;
|
readonly __wbindgen_malloc: (a: number, b: number) => number;
|
||||||
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
||||||
+5
-27
@@ -1,16 +1,16 @@
|
|||||||
/* @ts-self-types="./decodal_wasm.d.ts" */
|
/* @ts-self-types="./decodal_language_tools.d.ts" */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {string} source
|
* @param {string} source
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
export function evaluate(source) {
|
export function formatSource(source) {
|
||||||
let deferred2_0;
|
let deferred2_0;
|
||||||
let deferred2_1;
|
let deferred2_1;
|
||||||
try {
|
try {
|
||||||
const ptr0 = passStringToWasm0(source, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
const ptr0 = passStringToWasm0(source, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
const len0 = WASM_VECTOR_LEN;
|
const len0 = WASM_VECTOR_LEN;
|
||||||
const ret = wasm.evaluate(ptr0, len0);
|
const ret = wasm.formatSource(ptr0, len0);
|
||||||
deferred2_0 = ret[0];
|
deferred2_0 = ret[0];
|
||||||
deferred2_1 = ret[1];
|
deferred2_1 = ret[1];
|
||||||
return getStringFromWasm0(ret[0], ret[1]);
|
return getStringFromWasm0(ret[0], ret[1]);
|
||||||
@@ -18,28 +18,6 @@ export function evaluate(source) {
|
|||||||
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
|
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param {string} entry
|
|
||||||
* @param {string} files_json
|
|
||||||
* @returns {string}
|
|
||||||
*/
|
|
||||||
export function evaluateProject(entry, files_json) {
|
|
||||||
let deferred3_0;
|
|
||||||
let deferred3_1;
|
|
||||||
try {
|
|
||||||
const ptr0 = passStringToWasm0(entry, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
||||||
const len0 = WASM_VECTOR_LEN;
|
|
||||||
const ptr1 = passStringToWasm0(files_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
|
||||||
const len1 = WASM_VECTOR_LEN;
|
|
||||||
const ret = wasm.evaluateProject(ptr0, len0, ptr1, len1);
|
|
||||||
deferred3_0 = ret[0];
|
|
||||||
deferred3_1 = ret[1];
|
|
||||||
return getStringFromWasm0(ret[0], ret[1]);
|
|
||||||
} finally {
|
|
||||||
wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
function __wbg_get_imports() {
|
function __wbg_get_imports() {
|
||||||
const import0 = {
|
const import0 = {
|
||||||
__proto__: null,
|
__proto__: null,
|
||||||
@@ -55,7 +33,7 @@ function __wbg_get_imports() {
|
|||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
__proto__: null,
|
__proto__: null,
|
||||||
"./decodal_wasm_bg.js": import0,
|
"./decodal_language_tools_bg.js": import0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -215,7 +193,7 @@ async function __wbg_init(module_or_path) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (module_or_path === undefined) {
|
if (module_or_path === undefined) {
|
||||||
module_or_path = new URL('decodal_wasm_bg.wasm', import.meta.url);
|
module_or_path = new URL('decodal_language_tools_bg.wasm', import.meta.url);
|
||||||
}
|
}
|
||||||
const imports = __wbg_get_imports();
|
const imports = __wbg_get_imports();
|
||||||
|
|
||||||
Binary file not shown.
+1
-2
@@ -1,8 +1,7 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
export const memory: WebAssembly.Memory;
|
export const memory: WebAssembly.Memory;
|
||||||
export const evaluate: (a: number, b: number) => [number, number];
|
export const formatSource: (a: number, b: number) => [number, number];
|
||||||
export const evaluateProject: (a: number, b: number, c: number, d: number) => [number, number];
|
|
||||||
export const __wbindgen_externrefs: WebAssembly.Table;
|
export const __wbindgen_externrefs: WebAssembly.Table;
|
||||||
export const __wbindgen_malloc: (a: number, b: number) => number;
|
export const __wbindgen_malloc: (a: number, b: number) => number;
|
||||||
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
# wasm-pack output is committed for the browser runtime package.
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Decodal contributors
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user