Compare commits
11
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8198b615a8 | ||
|
|
62aa81b91c | ||
|
|
9c46635ee0 | ||
|
|
515adc2533 | ||
|
|
d9cf24d0eb | ||
|
|
d28edcf041 | ||
|
|
e862e53f3e | ||
|
|
610358110f | ||
|
|
bdaccd9803 | ||
|
|
08603dc4b5 | ||
|
|
165daada23 |
Generated
+162
-39
@@ -11,31 +11,42 @@ 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"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cc"
|
|
||||||
version = "1.2.66"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f5d6cac793997bd970000024b2934968efe83b382de4fdcf4fcb46b6ee4ad996"
|
|
||||||
dependencies = [
|
|
||||||
"find-msvc-tools",
|
|
||||||
"shlex",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
version = "1.0.4"
|
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.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"decodal-derive",
|
"decodal-derive",
|
||||||
"regex",
|
"regex",
|
||||||
@@ -43,46 +54,93 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "decodal-cli"
|
name = "decodal-cli"
|
||||||
version = "0.1.2"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"decodal",
|
"decodal",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "decodal-derive"
|
name = "decodal-derive"
|
||||||
version = "0.1.2"
|
version = "0.1.3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"decodal",
|
"decodal",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.117",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "decodal-language-service"
|
||||||
|
version = "0.2.0"
|
||||||
|
dependencies = [
|
||||||
|
"decodal",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "decodal-language-tools"
|
name = "decodal-language-tools"
|
||||||
version = "0.1.2"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"decodal",
|
"decodal",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tree-sitter",
|
|
||||||
"tree-sitter-decodal",
|
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "decodal-lsp"
|
||||||
|
version = "0.2.0"
|
||||||
|
dependencies = [
|
||||||
|
"decodal",
|
||||||
|
"decodal-language-service",
|
||||||
|
"decodal-language-tools",
|
||||||
|
"lsp-server",
|
||||||
|
"lsp-types",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "decodal-wasm"
|
name = "decodal-wasm"
|
||||||
version = "0.1.2"
|
version = "0.2.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"decodal",
|
"decodal",
|
||||||
|
"decodal-language-service",
|
||||||
|
"js-sys",
|
||||||
|
"serde-wasm-bindgen",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "find-msvc-tools"
|
name = "fluent-uri"
|
||||||
version = "0.1.9"
|
version = "0.1.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
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"
|
||||||
@@ -90,6 +148,49 @@ 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"
|
||||||
@@ -102,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"
|
||||||
@@ -162,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]]
|
||||||
@@ -181,7 +300,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -198,10 +317,21 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "shlex"
|
name = "serde_repr"
|
||||||
version = "2.0.1"
|
version = "0.1.21"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
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"
|
||||||
@@ -215,21 +345,14 @@ dependencies = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tree-sitter"
|
name = "syn"
|
||||||
version = "0.22.6"
|
version = "3.0.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "df7cc499ceadd4dcdf7ec6d4cbc34ece92c3fa07821e287aedecd4416c516dca"
|
checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"proc-macro2",
|
||||||
"regex",
|
"quote",
|
||||||
]
|
"unicode-ident",
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tree-sitter-decodal"
|
|
||||||
version = "0.0.1"
|
|
||||||
dependencies = [
|
|
||||||
"cc",
|
|
||||||
"tree-sitter",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -270,7 +393,7 @@ dependencies = [
|
|||||||
"bumpalo",
|
"bumpalo",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn",
|
"syn 2.0.117",
|
||||||
"wasm-bindgen-shared",
|
"wasm-bindgen-shared",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
+4
-1
@@ -5,11 +5,14 @@ members = [
|
|||||||
"crates/decodal-wasm",
|
"crates/decodal-wasm",
|
||||||
"crates/decodal-derive",
|
"crates/decodal-derive",
|
||||||
"crates/decodal-language-tools",
|
"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.2.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,7 +4,8 @@ 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
|
||||||
- deterministic expression evaluation
|
- deterministic expression evaluation
|
||||||
@@ -13,7 +14,7 @@ It is designed around a lightweight Rust library:
|
|||||||
|
|
||||||
## 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]
|
||||||
@@ -64,6 +65,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.2.0"
|
||||||
|
decodal-language-service = "0.2.0"
|
||||||
|
decodal-lsp = "0.2.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 +93,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,61 @@
|
|||||||
|
# Releasing Decodal
|
||||||
|
|
||||||
|
Run all commands from the repository root. Publishing requires Cargo, npm, and JSR credentials; validation does not.
|
||||||
|
|
||||||
|
## Versions
|
||||||
|
|
||||||
|
- `decodal-derive`: `0.1.3` (already published)
|
||||||
|
- Other published Rust crates: `0.2.0`
|
||||||
|
- `decodal-wasm` npm/JSR package: `0.1.4`
|
||||||
|
- `decodal-codemirror`: `0.1.6`
|
||||||
|
|
||||||
|
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
|
||||||
|
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.2.0", path = "../decodal-core" }
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use decodal::{
|
use decodal::{
|
||||||
Data, Diagnostic, DiagnosticKind, Engine, LoadedSource, SourceId, SourceLoader, Span,
|
Data, Diagnostic, DiagnosticKind, Engine, ImportLoader, LoadedImport, LoadedSource, SourceId,
|
||||||
format_diagnostic_with,
|
Span, format_diagnostic_with,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn main() -> ExitCode {
|
fn main() -> ExitCode {
|
||||||
@@ -95,12 +95,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,7 +112,7 @@ impl SourceLoader for FsLoader {
|
|||||||
} else {
|
} else {
|
||||||
PathBuf::from(path)
|
PathBuf::from(path)
|
||||||
};
|
};
|
||||||
load_path(&path)
|
load_path(&path).map(LoadedImport::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.1.3", 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 }
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
use decodal::{
|
||||||
|
Data, Diagnostic, DiagnosticKind, Engine, HostValue, ImportLoader, LoadedImport, Span,
|
||||||
|
};
|
||||||
|
|
||||||
|
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<HostValue> {
|
||||||
|
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" => HostValue::bool(true),
|
||||||
|
"false" => HostValue::bool(false),
|
||||||
|
value => HostValue::string(value),
|
||||||
|
};
|
||||||
|
fields.push((name.trim(), value));
|
||||||
|
}
|
||||||
|
Ok(HostValue::object([
|
||||||
|
("frontmatter", HostValue::object(fields)),
|
||||||
|
("body", HostValue::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(())
|
||||||
|
}
|
||||||
@@ -70,6 +70,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>,
|
||||||
|
|||||||
@@ -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)]
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
+543
-42
@@ -6,7 +6,7 @@ use crate::{
|
|||||||
constraints::normalize_constraints,
|
constraints::normalize_constraints,
|
||||||
diagnostic::{Diagnostic, DiagnosticKind, Result},
|
diagnostic::{Diagnostic, DiagnosticKind, Result},
|
||||||
embedding::HostValue,
|
embedding::HostValue,
|
||||||
module::{EmptyLoader, LoadedSource, Module, SourceLoader},
|
module::{EmptyLoader, ImportLoader, LoadedImport, LoadedSource, LoadedValue, Module},
|
||||||
parse_source_with_source_id,
|
parse_source_with_source_id,
|
||||||
runtime::{
|
runtime::{
|
||||||
AbstractValue, Binding, ConcreteValue, Constraint, ConstraintEntry, Data, DataField, Env,
|
AbstractValue, Binding, ConcreteValue, Constraint, ConstraintEntry, Data, DataField, Env,
|
||||||
@@ -19,10 +19,50 @@ pub struct Engine<L = EmptyLoader> {
|
|||||||
loader: L,
|
loader: L,
|
||||||
prelude_env: EnvId,
|
prelude_env: EnvId,
|
||||||
modules: Vec<Module>,
|
modules: Vec<Module>,
|
||||||
|
imported_values: Vec<ImportedValue>,
|
||||||
thunks: Vec<Thunk>,
|
thunks: Vec<Thunk>,
|
||||||
|
thunk_import_origins: Vec<Option<ImportedValueOrigin>>,
|
||||||
envs: Vec<Env>,
|
envs: Vec<Env>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ImportedValue {
|
||||||
|
key: String,
|
||||||
|
root: ThunkId,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
struct ImportedValueOrigin {
|
||||||
|
key: String,
|
||||||
|
path: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImportedValueOrigin {
|
||||||
|
fn root(key: String) -> Self {
|
||||||
|
Self {
|
||||||
|
key,
|
||||||
|
path: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn field(&self, name: String) -> Self {
|
||||||
|
let mut path = self.path.clone();
|
||||||
|
path.push(name);
|
||||||
|
Self {
|
||||||
|
key: self.key.clone(),
|
||||||
|
path,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn index(&self, index: usize) -> Self {
|
||||||
|
let mut path = self.path.clone();
|
||||||
|
path.push(format!("[{index}]"));
|
||||||
|
Self {
|
||||||
|
key: self.key.clone(),
|
||||||
|
path,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Engine<EmptyLoader> {
|
impl Engine<EmptyLoader> {
|
||||||
pub fn from_parse(ast: Ast, root: ExprId) -> Self {
|
pub fn from_parse(ast: Ast, root: ExprId) -> Self {
|
||||||
let mut this = Self::new(EmptyLoader);
|
let mut this = Self::new(EmptyLoader);
|
||||||
@@ -38,13 +78,15 @@ impl Engine<EmptyLoader> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<L: SourceLoader> Engine<L> {
|
impl<L: ImportLoader> Engine<L> {
|
||||||
pub fn new(loader: L) -> Self {
|
pub fn new(loader: L) -> Self {
|
||||||
Self {
|
Self {
|
||||||
loader,
|
loader,
|
||||||
prelude_env: EnvId(0),
|
prelude_env: EnvId(0),
|
||||||
modules: Vec::new(),
|
modules: Vec::new(),
|
||||||
|
imported_values: Vec::new(),
|
||||||
thunks: Vec::new(),
|
thunks: Vec::new(),
|
||||||
|
thunk_import_origins: Vec::new(),
|
||||||
envs: vec![Env {
|
envs: vec![Env {
|
||||||
parent: None,
|
parent: None,
|
||||||
bindings: Vec::new(),
|
bindings: Vec::new(),
|
||||||
@@ -65,6 +107,42 @@ impl<L: SourceLoader> Engine<L> {
|
|||||||
thunk
|
thunk
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the values installed in the host-owned global environment.
|
||||||
|
///
|
||||||
|
/// Language tooling uses this after [`crate::HostEnvironment`] configures a
|
||||||
|
/// fresh engine, so editor-visible globals cannot drift from evaluation.
|
||||||
|
pub fn global_values(&mut self) -> Result<Vec<(String, RuntimeValue)>> {
|
||||||
|
let bindings = self.envs[self.prelude_env.0 as usize].bindings.clone();
|
||||||
|
bindings
|
||||||
|
.into_iter()
|
||||||
|
.map(|binding| Ok((binding.name, self.force(binding.value)?)))
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the immediately visible fields of an object runtime value.
|
||||||
|
pub fn value_fields(
|
||||||
|
&mut self,
|
||||||
|
value: &RuntimeValue,
|
||||||
|
) -> Result<Option<Vec<(String, RuntimeValue)>>> {
|
||||||
|
let object = match value {
|
||||||
|
RuntimeValue::Concrete(crate::runtime::ConcreteValue::Object(object)) => object,
|
||||||
|
RuntimeValue::Abstract(abstract_value) => {
|
||||||
|
let Some(default) = abstract_value.default else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
let default = self.force(default)?;
|
||||||
|
return self.value_fields(&default);
|
||||||
|
}
|
||||||
|
_ => return Ok(None),
|
||||||
|
};
|
||||||
|
let fields = object.fields.clone();
|
||||||
|
fields
|
||||||
|
.into_iter()
|
||||||
|
.map(|field| Ok((field.name, self.force(field.value)?)))
|
||||||
|
.collect::<Result<Vec<_>>>()
|
||||||
|
.map(Some)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn add_root_source(
|
pub fn add_root_source(
|
||||||
&mut self,
|
&mut self,
|
||||||
key: impl Into<String>,
|
key: impl Into<String>,
|
||||||
@@ -236,10 +314,38 @@ impl<L: SourceLoader> Engine<L> {
|
|||||||
.map(|index| ModuleId(index as u32))
|
.map(|index| ModuleId(index as u32))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_import(&mut self, current: ModuleId, specifier: &str) -> Result<ModuleId> {
|
fn eval_import(&mut self, current: ModuleId, specifier: &str) -> Result<RuntimeValue> {
|
||||||
let current_key = self.modules[current.0 as usize].key.clone();
|
let current_key = self.modules[current.0 as usize].key.clone();
|
||||||
let LoadedSource { key, name, source } = self.loader.load(Some(¤t_key), specifier)?;
|
let loaded = self.loader.load(Some(¤t_key), specifier)?;
|
||||||
self.add_source(key, name, &source)
|
match loaded {
|
||||||
|
LoadedImport::Source(LoadedSource { key, name, source }) => {
|
||||||
|
if let Some(value) = self.find_imported_value(&key) {
|
||||||
|
return self.force(value);
|
||||||
|
}
|
||||||
|
let module = self.add_source(key, name, &source)?;
|
||||||
|
self.eval_module(module)
|
||||||
|
}
|
||||||
|
LoadedImport::Value(LoadedValue { key, value }) => {
|
||||||
|
if let Some(module) = self.find_module(&key) {
|
||||||
|
return self.eval_module(module);
|
||||||
|
}
|
||||||
|
if let Some(value) = self.find_imported_value(&key) {
|
||||||
|
return self.force(value);
|
||||||
|
}
|
||||||
|
let origin = ImportedValueOrigin::root(key.clone());
|
||||||
|
let value = self.internalize_host_value_with_origin(value, Some(&origin))?;
|
||||||
|
let root = self.add_value_thunk_with_import_origin(value, origin);
|
||||||
|
self.imported_values.push(ImportedValue { key, root });
|
||||||
|
self.force(root)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn find_imported_value(&self, key: &str) -> Option<ThunkId> {
|
||||||
|
self.imported_values
|
||||||
|
.iter()
|
||||||
|
.find(|value| value.key == key)
|
||||||
|
.map(|value| value.root)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn eval_expr(&mut self, reference: ExprRef, env: EnvId) -> Result<RuntimeValue> {
|
fn eval_expr(&mut self, reference: ExprRef, env: EnvId) -> Result<RuntimeValue> {
|
||||||
@@ -303,10 +409,7 @@ impl<L: SourceLoader> Engine<L> {
|
|||||||
let_env,
|
let_env,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Expr::Import(specifier) => {
|
Expr::Import(specifier) => self.eval_import(reference.module, &specifier),
|
||||||
let module = self.load_import(reference.module, &specifier)?;
|
|
||||||
self.eval_module(module)
|
|
||||||
}
|
|
||||||
Expr::Path { base, field } => {
|
Expr::Path { base, field } => {
|
||||||
let base = self.eval_expr(
|
let base = self.eval_expr(
|
||||||
ExprRef {
|
ExprRef {
|
||||||
@@ -368,6 +471,13 @@ impl<L: SourceLoader> Engine<L> {
|
|||||||
},
|
},
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
Expr::Parenthesized { expr } => self.eval_expr(
|
||||||
|
ExprRef {
|
||||||
|
module: reference.module,
|
||||||
|
expr,
|
||||||
|
},
|
||||||
|
env,
|
||||||
|
),
|
||||||
Expr::Match { scrutinee, arms } => {
|
Expr::Match { scrutinee, arms } => {
|
||||||
let value = self.eval_expr(
|
let value = self.eval_expr(
|
||||||
ExprRef {
|
ExprRef {
|
||||||
@@ -724,6 +834,14 @@ impl<L: SourceLoader> Engine<L> {
|
|||||||
) -> Result<bool> {
|
) -> Result<bool> {
|
||||||
match self.expr(pattern).clone() {
|
match self.expr(pattern).clone() {
|
||||||
Expr::Wildcard => Ok(true),
|
Expr::Wildcard => Ok(true),
|
||||||
|
Expr::Parenthesized { expr } => self.matches_pattern(
|
||||||
|
value,
|
||||||
|
ExprRef {
|
||||||
|
module: pattern.module,
|
||||||
|
expr,
|
||||||
|
},
|
||||||
|
env,
|
||||||
|
),
|
||||||
Expr::ArrayConstraint { .. }
|
Expr::ArrayConstraint { .. }
|
||||||
| Expr::CompareConstraint { .. }
|
| Expr::CompareConstraint { .. }
|
||||||
| Expr::RegexConstraint(_)
|
| Expr::RegexConstraint(_)
|
||||||
@@ -805,17 +923,59 @@ impl<L: SourceLoader> Engine<L> {
|
|||||||
.iter()
|
.iter()
|
||||||
.position(|lhs_field| lhs_field.name == rhs_field.name)
|
.position(|lhs_field| lhs_field.name == rhs_field.name)
|
||||||
{
|
{
|
||||||
|
let lhs_thunk = lhs.fields[index].value;
|
||||||
let lhs_field_span = lhs.fields[index].span;
|
let lhs_field_span = lhs.fields[index].span;
|
||||||
let lhs_value = self.force(lhs.fields[index].value)?;
|
let lhs_origin = self.thunk_import_origin(lhs_thunk).cloned();
|
||||||
|
let rhs_origin = self.thunk_import_origin(rhs_field.value).cloned();
|
||||||
|
let lhs_value = self.force(lhs_thunk)?;
|
||||||
let rhs_value = self.force(rhs_field.value)?;
|
let rhs_value = self.force(rhs_field.value)?;
|
||||||
let value = self
|
let value = self
|
||||||
.compose_and(lhs_value, rhs_value, span)
|
.compose_and(lhs_value.clone(), rhs_value.clone(), span)
|
||||||
.map_err(|diag| {
|
.map_err(|diag| {
|
||||||
diag.with_label(span, format!("while composing field `{}`", rhs_field.name))
|
let mut diagnostic = diag.with_label(
|
||||||
.with_label(lhs_field_span, format!("left field `{}`", rhs_field.name))
|
span,
|
||||||
.with_label(rhs_field.span, format!("right field `{}`", rhs_field.name))
|
format!("while composing field `{}`", rhs_field.name),
|
||||||
|
);
|
||||||
|
if lhs_origin.is_some() {
|
||||||
|
diagnostic = annotate_imported_value(
|
||||||
|
diagnostic,
|
||||||
|
lhs_origin.as_ref(),
|
||||||
|
Some(&lhs_value),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
diagnostic = diagnostic.with_label(
|
||||||
|
lhs_field_span,
|
||||||
|
format!("left field `{}`", rhs_field.name),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if rhs_origin.is_some() {
|
||||||
|
diagnostic = annotate_imported_value(
|
||||||
|
diagnostic,
|
||||||
|
rhs_origin.as_ref(),
|
||||||
|
Some(&rhs_value),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
diagnostic = diagnostic.with_label(
|
||||||
|
rhs_field.span,
|
||||||
|
format!("right field `{}`", rhs_field.name),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
diagnostic
|
||||||
})?;
|
})?;
|
||||||
lhs.fields[index].value = self.add_value_thunk_with_span(value, rhs_field.span);
|
let result_span = match (lhs_origin.is_some(), rhs_origin.is_some()) {
|
||||||
|
(false, true) => lhs_field_span,
|
||||||
|
(true, false) => rhs_field.span,
|
||||||
|
_ => rhs_field.span,
|
||||||
|
};
|
||||||
|
let result_origin = rhs_origin.clone().or(lhs_origin.clone());
|
||||||
|
lhs.fields[index].value = self.add_value_thunk_with_span_and_import_origin(
|
||||||
|
value,
|
||||||
|
result_span,
|
||||||
|
result_origin,
|
||||||
|
);
|
||||||
|
if lhs_origin.is_some() && rhs_origin.is_none() {
|
||||||
|
lhs.fields[index].span = rhs_field.span;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
lhs.fields.push(rhs_field);
|
lhs.fields.push(rhs_field);
|
||||||
}
|
}
|
||||||
@@ -841,9 +1001,14 @@ impl<L: SourceLoader> Engine<L> {
|
|||||||
.position(|lhs_field| lhs_field.name == rhs_field.name)
|
.position(|lhs_field| lhs_field.name == rhs_field.name)
|
||||||
{
|
{
|
||||||
let lhs_value = self.force(lhs.fields[index].value)?;
|
let lhs_value = self.force(lhs.fields[index].value)?;
|
||||||
|
let rhs_origin = self.thunk_import_origin(rhs_field.value).cloned();
|
||||||
let rhs_value = self.force(rhs_field.value)?;
|
let rhs_value = self.force(rhs_field.value)?;
|
||||||
let value = self.patch(lhs_value, rhs_value)?;
|
let value = self.patch(lhs_value, rhs_value)?;
|
||||||
lhs.fields[index].value = self.add_value_thunk_with_span(value, rhs_field.span);
|
lhs.fields[index].value = self.add_value_thunk_with_span_and_import_origin(
|
||||||
|
value,
|
||||||
|
rhs_field.span,
|
||||||
|
rhs_origin,
|
||||||
|
);
|
||||||
lhs.fields[index].span = rhs_field.span;
|
lhs.fields[index].span = rhs_field.span;
|
||||||
} else {
|
} else {
|
||||||
lhs.fields.push(rhs_field);
|
lhs.fields.push(rhs_field);
|
||||||
@@ -893,38 +1058,68 @@ impl<L: SourceLoader> Engine<L> {
|
|||||||
constraint,
|
constraint,
|
||||||
span,
|
span,
|
||||||
value_label,
|
value_label,
|
||||||
"value does not satisfy primitive type constraint",
|
format!(
|
||||||
|
"expected {primitive:?}, found {}",
|
||||||
|
runtime_value_kind(&value)
|
||||||
|
),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Constraint::ArrayItems(item_constraint) => {
|
Constraint::ArrayItems(item_constraint) => {
|
||||||
|
let value_kind = runtime_value_kind(&value);
|
||||||
let RuntimeValue::Concrete(ConcreteValue::Array(items)) = value else {
|
let RuntimeValue::Concrete(ConcreteValue::Array(items)) = value else {
|
||||||
return Err(self.constraint_violation(
|
return Err(self.constraint_violation(
|
||||||
constraint,
|
constraint,
|
||||||
span,
|
span,
|
||||||
value_label,
|
value_label,
|
||||||
"value does not satisfy array element constraint",
|
format!("expected Array, found {value_kind}"),
|
||||||
));
|
));
|
||||||
};
|
};
|
||||||
let item_constraint_value = self.force(*item_constraint)?;
|
let item_constraint_value = self.force(*item_constraint)?;
|
||||||
let mut constrained_items = Vec::with_capacity(items.len());
|
let mut constrained_items = Vec::with_capacity(items.len());
|
||||||
for (index, item) in items.into_iter().enumerate() {
|
for (index, item) in items.into_iter().enumerate() {
|
||||||
let item_span = self.thunk_span(item);
|
let item_span = self.thunk_span(item);
|
||||||
|
let item_origin = self.thunk_import_origin(item).cloned();
|
||||||
let item_value = self.force(item)?;
|
let item_value = self.force(item)?;
|
||||||
|
let composition_span = if item_origin.is_some() {
|
||||||
|
constraint.span
|
||||||
|
} else {
|
||||||
|
item_span
|
||||||
|
};
|
||||||
let constrained = self
|
let constrained = self
|
||||||
.compose_and(item_constraint_value.clone(), item_value, item_span)
|
.compose_and(
|
||||||
|
item_constraint_value.clone(),
|
||||||
|
item_value.clone(),
|
||||||
|
composition_span,
|
||||||
|
)
|
||||||
.map_err(|diagnostic| {
|
.map_err(|diagnostic| {
|
||||||
diagnostic
|
let diagnostic = diagnostic.with_label(
|
||||||
.with_label(
|
constraint.span,
|
||||||
constraint.span,
|
"array element constraint declared here",
|
||||||
"array element constraint declared here",
|
);
|
||||||
|
if item_origin.is_some() {
|
||||||
|
annotate_imported_value(
|
||||||
|
diagnostic,
|
||||||
|
item_origin.as_ref(),
|
||||||
|
Some(&item_value),
|
||||||
)
|
)
|
||||||
.with_label(
|
} else {
|
||||||
|
diagnostic.with_label(
|
||||||
item_span,
|
item_span,
|
||||||
format!("array element [{index}] checked here"),
|
format!("array element [{index}] checked here"),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
})?;
|
})?;
|
||||||
constrained_items.push(self.add_value_thunk_with_span(constrained, item_span));
|
let constrained_span = if item_origin.is_some() {
|
||||||
|
constraint.span
|
||||||
|
} else {
|
||||||
|
item_span
|
||||||
|
};
|
||||||
|
constrained_items.push(self.add_value_thunk_with_span_and_import_origin(
|
||||||
|
constrained,
|
||||||
|
constrained_span,
|
||||||
|
item_origin,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
Ok(RuntimeValue::Concrete(ConcreteValue::Array(
|
Ok(RuntimeValue::Concrete(ConcreteValue::Array(
|
||||||
constrained_items,
|
constrained_items,
|
||||||
@@ -965,7 +1160,7 @@ impl<L: SourceLoader> Engine<L> {
|
|||||||
constraint: &ConstraintEntry,
|
constraint: &ConstraintEntry,
|
||||||
operation_span: Span,
|
operation_span: Span,
|
||||||
value_label: Option<(Span, &'static str)>,
|
value_label: Option<(Span, &'static str)>,
|
||||||
message: &'static str,
|
message: impl Into<String>,
|
||||||
) -> Diagnostic {
|
) -> Diagnostic {
|
||||||
let mut diagnostic = Diagnostic::new(
|
let mut diagnostic = Diagnostic::new(
|
||||||
DiagnosticKind::ConstraintViolation,
|
DiagnosticKind::ConstraintViolation,
|
||||||
@@ -1001,6 +1196,14 @@ impl<L: SourceLoader> Engine<L> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn internalize_host_value(&mut self, value: HostValue) -> Result<RuntimeValue> {
|
fn internalize_host_value(&mut self, value: HostValue) -> Result<RuntimeValue> {
|
||||||
|
self.internalize_host_value_with_origin(value, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn internalize_host_value_with_origin(
|
||||||
|
&mut self,
|
||||||
|
value: HostValue,
|
||||||
|
origin: Option<&ImportedValueOrigin>,
|
||||||
|
) -> Result<RuntimeValue> {
|
||||||
match value {
|
match value {
|
||||||
HostValue::String(value) => Ok(RuntimeValue::Concrete(ConcreteValue::String(value))),
|
HostValue::String(value) => Ok(RuntimeValue::Concrete(ConcreteValue::String(value))),
|
||||||
HostValue::Int(value) => Ok(RuntimeValue::Concrete(ConcreteValue::Int(value))),
|
HostValue::Int(value) => Ok(RuntimeValue::Concrete(ConcreteValue::Int(value))),
|
||||||
@@ -1008,9 +1211,12 @@ impl<L: SourceLoader> Engine<L> {
|
|||||||
HostValue::Bool(value) => Ok(RuntimeValue::Concrete(ConcreteValue::Bool(value))),
|
HostValue::Bool(value) => Ok(RuntimeValue::Concrete(ConcreteValue::Bool(value))),
|
||||||
HostValue::Array(items) => {
|
HostValue::Array(items) => {
|
||||||
let mut thunks = Vec::new();
|
let mut thunks = Vec::new();
|
||||||
for item in items {
|
for (index, item) in items.into_iter().enumerate() {
|
||||||
let value = self.internalize_host_value(item)?;
|
let item_origin = origin.map(|origin| origin.index(index));
|
||||||
thunks.push(self.add_value_thunk(value));
|
let value =
|
||||||
|
self.internalize_host_value_with_origin(item, item_origin.as_ref())?;
|
||||||
|
thunks
|
||||||
|
.push(self.add_value_thunk_with_optional_import_origin(value, item_origin));
|
||||||
}
|
}
|
||||||
Ok(RuntimeValue::Concrete(ConcreteValue::Array(thunks)))
|
Ok(RuntimeValue::Concrete(ConcreteValue::Array(thunks)))
|
||||||
}
|
}
|
||||||
@@ -1019,11 +1225,11 @@ impl<L: SourceLoader> Engine<L> {
|
|||||||
mut constraints,
|
mut constraints,
|
||||||
default,
|
default,
|
||||||
} => {
|
} => {
|
||||||
let item = self.internalize_host_value(*item)?;
|
let item = self.internalize_host_value_with_origin(*item, origin)?;
|
||||||
let item = self.add_value_thunk(item);
|
let item = self.add_value_thunk_with_optional_import_origin(item, origin.cloned());
|
||||||
let default = if let Some(default) = default {
|
let default = if let Some(default) = default {
|
||||||
let value = self.internalize_host_value(*default)?;
|
let value = self.internalize_host_value_with_origin(*default, origin)?;
|
||||||
Some(self.add_value_thunk(value))
|
Some(self.add_value_thunk_with_optional_import_origin(value, origin.cloned()))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
@@ -1044,19 +1250,27 @@ impl<L: SourceLoader> Engine<L> {
|
|||||||
HostValue::Object(fields) => {
|
HostValue::Object(fields) => {
|
||||||
let mut object = ObjectValue { fields: Vec::new() };
|
let mut object = ObjectValue { fields: Vec::new() };
|
||||||
for field in fields {
|
for field in fields {
|
||||||
|
let field_origin = origin.map(|origin| origin.field(field.name.clone()));
|
||||||
if object
|
if object
|
||||||
.fields
|
.fields
|
||||||
.iter()
|
.iter()
|
||||||
.any(|existing| existing.name == field.name)
|
.any(|existing| existing.name == field.name)
|
||||||
{
|
{
|
||||||
return Err(Diagnostic::new(
|
let diagnostic = Diagnostic::new(
|
||||||
DiagnosticKind::Conflict,
|
DiagnosticKind::Conflict,
|
||||||
Span::default(),
|
Span::default(),
|
||||||
format!("duplicate host object field `{}`", field.name),
|
format!("duplicate host object field `{}`", field.name),
|
||||||
|
);
|
||||||
|
return Err(annotate_imported_value(
|
||||||
|
diagnostic,
|
||||||
|
field_origin.as_ref(),
|
||||||
|
None,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let value = self.internalize_host_value(field.value)?;
|
let value = self
|
||||||
let value = self.add_value_thunk(value);
|
.internalize_host_value_with_origin(field.value, field_origin.as_ref())?;
|
||||||
|
let value =
|
||||||
|
self.add_value_thunk_with_optional_import_origin(value, field_origin);
|
||||||
object.fields.push(ObjectField {
|
object.fields.push(ObjectField {
|
||||||
name: field.name,
|
name: field.name,
|
||||||
value,
|
value,
|
||||||
@@ -1070,8 +1284,8 @@ impl<L: SourceLoader> Engine<L> {
|
|||||||
default,
|
default,
|
||||||
} => {
|
} => {
|
||||||
let default = if let Some(default) = default {
|
let default = if let Some(default) = default {
|
||||||
let value = self.internalize_host_value(*default)?;
|
let value = self.internalize_host_value_with_origin(*default, origin)?;
|
||||||
Some(self.add_value_thunk(value))
|
Some(self.add_value_thunk_with_optional_import_origin(value, origin.cloned()))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
@@ -1178,13 +1392,48 @@ impl<L: SourceLoader> Engine<L> {
|
|||||||
self.add_thunk(ThunkKind::Value(value), span)
|
self.add_thunk(ThunkKind::Value(value), span)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn add_value_thunk_with_import_origin(
|
||||||
|
&mut self,
|
||||||
|
value: RuntimeValue,
|
||||||
|
origin: ImportedValueOrigin,
|
||||||
|
) -> ThunkId {
|
||||||
|
self.add_value_thunk_with_optional_import_origin(value, Some(origin))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_value_thunk_with_optional_import_origin(
|
||||||
|
&mut self,
|
||||||
|
value: RuntimeValue,
|
||||||
|
origin: Option<ImportedValueOrigin>,
|
||||||
|
) -> ThunkId {
|
||||||
|
self.add_value_thunk_with_span_and_import_origin(value, Span::default(), origin)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_value_thunk_with_span_and_import_origin(
|
||||||
|
&mut self,
|
||||||
|
value: RuntimeValue,
|
||||||
|
span: Span,
|
||||||
|
origin: Option<ImportedValueOrigin>,
|
||||||
|
) -> ThunkId {
|
||||||
|
self.add_thunk_with_import_origin(ThunkKind::Value(value), span, origin)
|
||||||
|
}
|
||||||
|
|
||||||
fn add_thunk(&mut self, kind: ThunkKind, span: Span) -> ThunkId {
|
fn add_thunk(&mut self, kind: ThunkKind, span: Span) -> ThunkId {
|
||||||
|
self.add_thunk_with_import_origin(kind, span, None)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_thunk_with_import_origin(
|
||||||
|
&mut self,
|
||||||
|
kind: ThunkKind,
|
||||||
|
span: Span,
|
||||||
|
origin: Option<ImportedValueOrigin>,
|
||||||
|
) -> ThunkId {
|
||||||
let id = ThunkId(self.thunks.len() as u32);
|
let id = ThunkId(self.thunks.len() as u32);
|
||||||
self.thunks.push(Thunk {
|
self.thunks.push(Thunk {
|
||||||
kind,
|
kind,
|
||||||
state: ThunkState::Unevaluated,
|
state: ThunkState::Unevaluated,
|
||||||
span,
|
span,
|
||||||
});
|
});
|
||||||
|
self.thunk_import_origins.push(origin);
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1238,6 +1487,10 @@ impl<L: SourceLoader> Engine<L> {
|
|||||||
self.thunks[thunk.0 as usize].span
|
self.thunks[thunk.0 as usize].span
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn thunk_import_origin(&self, thunk: ThunkId) -> Option<&ImportedValueOrigin> {
|
||||||
|
self.thunk_import_origins[thunk.0 as usize].as_ref()
|
||||||
|
}
|
||||||
|
|
||||||
fn with_path_context(
|
fn with_path_context(
|
||||||
&self,
|
&self,
|
||||||
diagnostic: Diagnostic,
|
diagnostic: Diagnostic,
|
||||||
@@ -1296,6 +1549,9 @@ pub fn format_diagnostic_with<'a>(
|
|||||||
label.message,
|
label.message,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
for note in &diagnostic.notes {
|
||||||
|
out.push_str(&format!("\nnote: {note}"));
|
||||||
|
}
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1370,6 +1626,45 @@ fn format_path(path: &[String]) -> String {
|
|||||||
output
|
output
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn annotate_imported_value(
|
||||||
|
diagnostic: Diagnostic,
|
||||||
|
origin: Option<&ImportedValueOrigin>,
|
||||||
|
value: Option<&RuntimeValue>,
|
||||||
|
) -> Diagnostic {
|
||||||
|
let Some(origin) = origin else {
|
||||||
|
return diagnostic;
|
||||||
|
};
|
||||||
|
if diagnostic
|
||||||
|
.notes
|
||||||
|
.iter()
|
||||||
|
.any(|note| note.starts_with("imported `"))
|
||||||
|
{
|
||||||
|
return diagnostic;
|
||||||
|
}
|
||||||
|
let path = if origin.path.is_empty() {
|
||||||
|
String::from("<root>")
|
||||||
|
} else {
|
||||||
|
format_path(&origin.path)
|
||||||
|
};
|
||||||
|
let value = value
|
||||||
|
.map(|value| format!(" supplied a value of type {}", runtime_value_kind(value)))
|
||||||
|
.unwrap_or_default();
|
||||||
|
diagnostic.with_note(format!("imported `{}` at `{path}`{value}", origin.key))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn runtime_value_kind(value: &RuntimeValue) -> &'static str {
|
||||||
|
match value {
|
||||||
|
RuntimeValue::Concrete(ConcreteValue::String(_)) => "String",
|
||||||
|
RuntimeValue::Concrete(ConcreteValue::Int(_)) => "Int",
|
||||||
|
RuntimeValue::Concrete(ConcreteValue::Float(_)) => "Float",
|
||||||
|
RuntimeValue::Concrete(ConcreteValue::Bool(_)) => "Bool",
|
||||||
|
RuntimeValue::Concrete(ConcreteValue::Array(_)) => "Array",
|
||||||
|
RuntimeValue::Concrete(ConcreteValue::Object(_)) => "Object",
|
||||||
|
RuntimeValue::Concrete(ConcreteValue::Function(_)) => "Function",
|
||||||
|
RuntimeValue::Abstract(_) => "abstract",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "regex")]
|
#[cfg(feature = "regex")]
|
||||||
fn satisfies_regex(value: &RuntimeValue, pattern: &str, span: Span) -> Result<()> {
|
fn satisfies_regex(value: &RuntimeValue, pattern: &str, span: Span) -> Result<()> {
|
||||||
let RuntimeValue::Concrete(ConcreteValue::String(value)) = value else {
|
let RuntimeValue::Concrete(ConcreteValue::String(value)) = value else {
|
||||||
@@ -1763,6 +2058,13 @@ mod tests {
|
|||||||
assert_eq!(fields[4].value, Data::Int(-2));
|
assert_eq!(fields[4].value, Data::Int(-2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parenthesized_wildcard_remains_a_match_pattern() {
|
||||||
|
let data = eval_data("result = match 1 { (_): 2; };");
|
||||||
|
let Data::Object(fields) = data else { panic!() };
|
||||||
|
assert_eq!(fields[0].value, Data::Int(2));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn arithmetic_can_feed_constraints() {
|
fn arithmetic_can_feed_constraints() {
|
||||||
let data = eval_data("port = Int & > 4000 + 42 default 8080;");
|
let data = eval_data("port = Int & > 4000 + 42 default 8080;");
|
||||||
@@ -1964,8 +2266,8 @@ mod tests {
|
|||||||
sources: Vec<(String, String)>,
|
sources: Vec<(String, String)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SourceLoader for MapLoader {
|
impl ImportLoader for MapLoader {
|
||||||
fn load(&mut self, _current_key: Option<&str>, specifier: &str) -> Result<LoadedSource> {
|
fn load(&mut self, _current_key: Option<&str>, specifier: &str) -> Result<LoadedImport> {
|
||||||
let source = self
|
let source = self
|
||||||
.sources
|
.sources
|
||||||
.iter()
|
.iter()
|
||||||
@@ -1974,11 +2276,11 @@ mod tests {
|
|||||||
.ok_or_else(|| {
|
.ok_or_else(|| {
|
||||||
Diagnostic::new(DiagnosticKind::Import, Span::default(), "missing source")
|
Diagnostic::new(DiagnosticKind::Import, Span::default(), "missing source")
|
||||||
})?;
|
})?;
|
||||||
Ok(LoadedSource {
|
Ok(LoadedImport::Source(LoadedSource {
|
||||||
key: specifier.into(),
|
key: specifier.into(),
|
||||||
name: specifier.into(),
|
name: specifier.into(),
|
||||||
source,
|
source,
|
||||||
})
|
}))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2003,6 +2305,205 @@ mod tests {
|
|||||||
assert_eq!(fields[0].value, Data::Int(9000));
|
assert_eq!(fields[0].value, Data::Int(9000));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct MarkdownLoader {
|
||||||
|
source: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ImportLoader for MarkdownLoader {
|
||||||
|
fn load(&mut self, _current_key: Option<&str>, specifier: &str) -> Result<LoadedImport> {
|
||||||
|
if specifier != "./post.md" {
|
||||||
|
return Err(Diagnostic::new(
|
||||||
|
DiagnosticKind::Import,
|
||||||
|
Span::default(),
|
||||||
|
format!("unknown content import `{specifier}`"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let value = parse_test_markdown(&self.source)?;
|
||||||
|
Ok(LoadedImport::value("content/post.md", value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_test_markdown(source: &str) -> Result<HostValue> {
|
||||||
|
let source = source.strip_prefix("---\n").ok_or_else(|| {
|
||||||
|
Diagnostic::new(
|
||||||
|
DiagnosticKind::Import,
|
||||||
|
Span::default(),
|
||||||
|
"markdown frontmatter must start with `---`",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let (frontmatter, body) = source.split_once("\n---\n").ok_or_else(|| {
|
||||||
|
Diagnostic::new(
|
||||||
|
DiagnosticKind::Import,
|
||||||
|
Span::default(),
|
||||||
|
"markdown frontmatter is not terminated",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let mut fields = Vec::new();
|
||||||
|
for line in frontmatter.lines() {
|
||||||
|
let (name, value) = line.split_once(':').ok_or_else(|| {
|
||||||
|
Diagnostic::new(
|
||||||
|
DiagnosticKind::Import,
|
||||||
|
Span::default(),
|
||||||
|
"invalid markdown frontmatter field",
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
let value = match value.trim() {
|
||||||
|
"true" => HostValue::bool(true),
|
||||||
|
"false" => HostValue::bool(false),
|
||||||
|
value => HostValue::string(value.trim_matches('"')),
|
||||||
|
};
|
||||||
|
fields.push((name.trim(), value));
|
||||||
|
}
|
||||||
|
Ok(HostValue::object([
|
||||||
|
("frontmatter", HostValue::object(fields)),
|
||||||
|
("body", HostValue::string(body)),
|
||||||
|
]))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn imports_host_structured_content() {
|
||||||
|
let mut engine = Engine::new(MarkdownLoader {
|
||||||
|
source: String::from("---\ntitle: \"Hello\"\ndraft: false\n---\n# Hello\n\nBody text."),
|
||||||
|
});
|
||||||
|
let module = engine
|
||||||
|
.add_root_source(
|
||||||
|
"main.dcdl",
|
||||||
|
"main.dcdl",
|
||||||
|
r#"
|
||||||
|
let
|
||||||
|
Post = {
|
||||||
|
frontmatter = {
|
||||||
|
title = String;
|
||||||
|
draft = Bool;
|
||||||
|
};
|
||||||
|
body = String;
|
||||||
|
};
|
||||||
|
post = Post & import "./post.md";
|
||||||
|
in {
|
||||||
|
title = post.frontmatter.title;
|
||||||
|
draft = post.frontmatter.draft;
|
||||||
|
body = post.body;
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let value = engine.eval_module(module).unwrap();
|
||||||
|
let data = engine.materialize(&value).unwrap();
|
||||||
|
let Data::Object(fields) = data else { panic!() };
|
||||||
|
assert_eq!(fields[0].value, Data::String(String::from("Hello")));
|
||||||
|
assert_eq!(fields[1].value, Data::Bool(false));
|
||||||
|
assert_eq!(
|
||||||
|
fields[2].value,
|
||||||
|
Data::String(String::from("# Hello\n\nBody text."))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn structured_import_violation_reports_key_path_and_schema_span() {
|
||||||
|
let mut engine = Engine::new(MarkdownLoader {
|
||||||
|
source: String::from("---\ntitle: \"Hello\"\ndraft: maybe\n---\n# Hello"),
|
||||||
|
});
|
||||||
|
let module = engine
|
||||||
|
.add_root_source(
|
||||||
|
"main.dcdl",
|
||||||
|
"main.dcdl",
|
||||||
|
r#"
|
||||||
|
let
|
||||||
|
Post = {
|
||||||
|
frontmatter = {
|
||||||
|
title = String;
|
||||||
|
draft = Bool;
|
||||||
|
};
|
||||||
|
body = String;
|
||||||
|
};
|
||||||
|
in Post & import "./post.md"
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let error = engine.eval_module(module).unwrap_err();
|
||||||
|
|
||||||
|
assert_eq!(error.kind, DiagnosticKind::ConstraintViolation);
|
||||||
|
assert_eq!(error.message, "expected Bool, found String");
|
||||||
|
assert!(
|
||||||
|
error
|
||||||
|
.labels
|
||||||
|
.iter()
|
||||||
|
.all(|label| label.span != Span::default())
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
error
|
||||||
|
.labels
|
||||||
|
.iter()
|
||||||
|
.any(|label| label.message == "constraint declared here")
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
error.notes,
|
||||||
|
["imported `content/post.md` at `frontmatter.draft` supplied a value of type String"]
|
||||||
|
);
|
||||||
|
assert!(engine.format_diagnostic(&error).contains(
|
||||||
|
"note: imported `content/post.md` at `frontmatter.draft` supplied a value of type String"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
struct StructuredArrayLoader;
|
||||||
|
|
||||||
|
impl ImportLoader for StructuredArrayLoader {
|
||||||
|
fn load(&mut self, _current_key: Option<&str>, _specifier: &str) -> Result<LoadedImport> {
|
||||||
|
Ok(LoadedImport::value(
|
||||||
|
"content/navigation.json",
|
||||||
|
HostValue::object([(
|
||||||
|
"items",
|
||||||
|
HostValue::array([HostValue::string("home"), HostValue::int(2)]),
|
||||||
|
)]),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn structured_array_violation_reports_indexed_import_path() {
|
||||||
|
let mut engine = Engine::new(StructuredArrayLoader);
|
||||||
|
let module = engine
|
||||||
|
.add_root_source(
|
||||||
|
"main.dcdl",
|
||||||
|
"main.dcdl",
|
||||||
|
r#"{ items = [...String]; } & import "./navigation.json""#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let error = engine.eval_module(module).unwrap_err();
|
||||||
|
|
||||||
|
assert_eq!(error.message, "expected String, found Int");
|
||||||
|
assert_eq!(
|
||||||
|
error.notes,
|
||||||
|
["imported `content/navigation.json` at `items[1]` supplied a value of type Int"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct StructuredAliasLoader;
|
||||||
|
|
||||||
|
impl ImportLoader for StructuredAliasLoader {
|
||||||
|
fn load(&mut self, _current_key: Option<&str>, _specifier: &str) -> Result<LoadedImport> {
|
||||||
|
Ok(LoadedImport::value(
|
||||||
|
"shared-value",
|
||||||
|
HostValue::int_type().default_int(1)?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn structured_imports_share_cached_runtime_value_by_key() {
|
||||||
|
let mut engine = Engine::new(StructuredAliasLoader);
|
||||||
|
let module = engine
|
||||||
|
.add_root_source(
|
||||||
|
"main.dcdl",
|
||||||
|
"main.dcdl",
|
||||||
|
r#"(import "./first.data") & (import "./second.data")"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let value = engine.eval_module(module).unwrap();
|
||||||
|
let data = engine.materialize(&value).unwrap();
|
||||||
|
assert_eq!(data, Data::Int(1));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn top_level_fields_are_recursive_module_scope() {
|
fn top_level_fields_are_recursive_module_scope() {
|
||||||
let mut engine = Engine::new(EmptyLoader);
|
let mut engine = Engine::new(EmptyLoader);
|
||||||
|
|||||||
@@ -52,9 +52,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,
|
||||||
@@ -78,10 +94,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());
|
||||||
@@ -93,11 +125,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 {
|
||||||
@@ -259,21 +286,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;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lex_comment(&mut self) -> Token {
|
||||||
|
let start = self.pos;
|
||||||
|
while let Some(c) = self.peek() {
|
||||||
|
if c == b'\n' {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
if self.peek() == Some(b'#') {
|
self.pos += 1;
|
||||||
while let Some(c) = self.peek() {
|
}
|
||||||
self.pos += 1;
|
Token {
|
||||||
if c == b'\n' {
|
kind: TokenKind::Comment,
|
||||||
break;
|
span: self.span(start, self.pos),
|
||||||
}
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -453,4 +482,30 @@ mod tests {
|
|||||||
assert!(matches!(tokens[2].kind, TokenKind::Ident(_)));
|
assert!(matches!(tokens[2].kind, TokenKind::Ident(_)));
|
||||||
assert_eq!(tokens[3].kind, TokenKind::RBracket);
|
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)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ pub mod ast;
|
|||||||
pub mod constraints;
|
pub mod constraints;
|
||||||
pub mod diagnostic;
|
pub mod diagnostic;
|
||||||
pub mod embedding;
|
pub mod embedding;
|
||||||
|
pub mod environment;
|
||||||
pub mod eval;
|
pub mod eval;
|
||||||
mod lexer;
|
mod lexer;
|
||||||
pub mod module;
|
pub mod module;
|
||||||
@@ -20,8 +21,15 @@ pub use constraints::normalize_constraints;
|
|||||||
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 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, LoadedSource, LoadedValue, 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, RuntimeValue};
|
||||||
pub use span::{SourceId, Span};
|
pub use span::{SourceId, Span};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use alloc::string::String;
|
use alloc::{string::String, vec::Vec};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
Ast, ExprId, SourceForm, SourceId,
|
Ast, ExprId, HostValue, SourceForm, SourceId,
|
||||||
runtime::{EnvId, ThunkId},
|
runtime::{EnvId, ThunkId},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -24,15 +24,82 @@ pub struct LoadedSource {
|
|||||||
pub source: String,
|
pub source: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait SourceLoader {
|
#[derive(Debug, Clone)]
|
||||||
fn load(&mut self, current_key: Option<&str>, specifier: &str) -> crate::Result<LoadedSource>;
|
pub struct LoadedValue {
|
||||||
|
pub key: String,
|
||||||
|
pub value: HostValue,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum LoadedImport {
|
||||||
|
Source(LoadedSource),
|
||||||
|
Value(LoadedValue),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An import specifier offered by host-owned language tooling.
|
||||||
|
#[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(LoadedSource {
|
||||||
|
key: key.into(),
|
||||||
|
name: name.into(),
|
||||||
|
source: source.into(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn value(key: impl Into<String>, value: HostValue) -> Self {
|
||||||
|
Self::Value(LoadedValue {
|
||||||
|
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(),
|
||||||
|
|||||||
@@ -38,7 +38,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(),
|
||||||
}
|
}
|
||||||
@@ -380,8 +383,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>> {
|
||||||
@@ -692,7 +696,24 @@ mod tests {
|
|||||||
let Expr::ArrayConstraint { item } = parsed.ast.get(parsed.root).expr else {
|
let Expr::ArrayConstraint { item } = parsed.ast.get(parsed.root).expr else {
|
||||||
panic!()
|
panic!()
|
||||||
};
|
};
|
||||||
assert!(matches!(parsed.ast.get(item).expr, Expr::Binary { .. }));
|
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]
|
#[test]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "decodal-derive"
|
name = "decodal-derive"
|
||||||
version.workspace = true
|
version = "0.1.3"
|
||||||
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 = { version = "0.2.0", path = "../decodal-core" }
|
||||||
|
|||||||
@@ -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.2.0", path = "../decodal-core" }
|
||||||
@@ -0,0 +1,905 @@
|
|||||||
|
use std::collections::{BTreeMap, HashMap};
|
||||||
|
|
||||||
|
use decodal::{
|
||||||
|
Engine, HostEnvironment, HostValue, ImportLoader, LoadedImport, Result, 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(source) => source.name.clone(),
|
||||||
|
LoadedImport::Value(value) => value.key.clone(),
|
||||||
|
};
|
||||||
|
fields = match loaded {
|
||||||
|
LoadedImport::Source(source) => collect_fields(&tokenize(&source.source)),
|
||||||
|
LoadedImport::Value(value) => fields_from_host_value(&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),
|
||||||
|
("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),
|
||||||
|
]
|
||||||
|
.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_host_value(value: &HostValue) -> FieldTree {
|
||||||
|
match value {
|
||||||
|
HostValue::Object(fields) => fields
|
||||||
|
.iter()
|
||||||
|
.map(|field| (field.name.clone(), fields_from_host_value(&field.value)))
|
||||||
|
.collect(),
|
||||||
|
HostValue::ArrayConstraint {
|
||||||
|
default: Some(value),
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| HostValue::Abstract {
|
||||||
|
default: Some(value),
|
||||||
|
..
|
||||||
|
} => fields_from_host_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, HostValue, ImportCandidate, LoadedImport, Span,
|
||||||
|
};
|
||||||
|
|
||||||
|
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",
|
||||||
|
HostValue::object([
|
||||||
|
(
|
||||||
|
"frontmatter",
|
||||||
|
HostValue::object([
|
||||||
|
("title", HostValue::string("Hello")),
|
||||||
|
("draft", HostValue::bool(false)),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
("body", HostValue::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",
|
||||||
|
HostValue::object([(
|
||||||
|
"config",
|
||||||
|
HostValue::object([
|
||||||
|
("enabled", HostValue::bool_type()),
|
||||||
|
("port", HostValue::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(&"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()));
|
||||||
|
}
|
||||||
|
|
||||||
|
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, HostValue, ImportLoader, LoadedImport, Span,
|
||||||
|
};
|
||||||
|
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
const ROOT: &str = r#"Post & import "./post.md""#;
|
||||||
|
|
||||||
|
struct ContentEnvironment {
|
||||||
|
draft: HostValue,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ContentLoader {
|
||||||
|
draft: HostValue,
|
||||||
|
}
|
||||||
|
|
||||||
|
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",
|
||||||
|
HostValue::object([
|
||||||
|
(
|
||||||
|
"frontmatter",
|
||||||
|
HostValue::object([
|
||||||
|
("title", HostValue::string("Hello")),
|
||||||
|
("draft", self.draft.clone()),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
("body", HostValue::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",
|
||||||
|
HostValue::object([
|
||||||
|
(
|
||||||
|
"frontmatter",
|
||||||
|
HostValue::object([
|
||||||
|
("title", HostValue::string_type()),
|
||||||
|
("draft", HostValue::bool_type()),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
("body", HostValue::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: HostValue::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: HostValue::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"]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,18 +7,15 @@ license.workspace = true
|
|||||||
repository.workspace = true
|
repository.workspace = true
|
||||||
readme.workspace = true
|
readme.workspace = true
|
||||||
description = "Source-level language tooling for Decodal."
|
description = "Source-level language tooling for Decodal."
|
||||||
keywords = ["decodal", "formatter", "lsp", "tree-sitter"]
|
keywords = ["decodal", "formatter", "lsp", "language-tools"]
|
||||||
categories = ["development-tools", "text-processing"]
|
categories = ["development-tools", "text-processing"]
|
||||||
publish = false
|
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
crate-type = ["cdylib", "rlib"]
|
crate-type = ["cdylib", "rlib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
decodal = { version = "0.1.2", path = "../decodal-core" }
|
decodal = { version = "0.2.0", path = "../decodal-core" }
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
tree-sitter = "0.22.6"
|
|
||||||
tree-sitter-decodal = { path = "../../editors/tree-sitter-decodal" }
|
|
||||||
wasm-bindgen.workspace = true
|
wasm-bindgen.workspace = true
|
||||||
|
|
||||||
[package.metadata.wasm-pack.profile.release]
|
[package.metadata.wasm-pack.profile.release]
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -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.2.0", path = "../decodal-core" }
|
||||||
|
decodal-language-service = { version = "0.2.0", path = "../decodal-language-service" }
|
||||||
|
decodal-language-tools = { version = "0.2.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.2.0", path = "../decodal-core" }
|
||||||
|
decodal-language-service = { version = "0.2.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
|
||||||
|
|||||||
@@ -0,0 +1,228 @@
|
|||||||
|
use decodal::{CompareOp, Constraint, HostValue, LiteralValue, PrimitiveType};
|
||||||
|
use serde_json::{Map, Value};
|
||||||
|
|
||||||
|
pub(crate) fn from_json(value: &Value) -> Result<HostValue, String> {
|
||||||
|
match value {
|
||||||
|
Value::Null => Err(String::from("null is not a Decodal host value")),
|
||||||
|
Value::Bool(value) => Ok(HostValue::bool(*value)),
|
||||||
|
Value::Number(value) => number(value),
|
||||||
|
Value::String(value) => Ok(HostValue::string(value)),
|
||||||
|
Value::Array(items) => items
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, value)| {
|
||||||
|
from_json(value).map_err(|error| format!("array item {index}: {error}"))
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
.map(HostValue::array),
|
||||||
|
Value::Object(fields) => object(fields),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn number(value: &serde_json::Number) -> Result<HostValue, String> {
|
||||||
|
if let Some(value) = value.as_i64() {
|
||||||
|
return Ok(HostValue::int(value));
|
||||||
|
}
|
||||||
|
if let Some(value) = value.as_u64() {
|
||||||
|
return i64::try_from(value)
|
||||||
|
.map(HostValue::int)
|
||||||
|
.map_err(|_| String::from("integer host value is outside the signed 64-bit range"));
|
||||||
|
}
|
||||||
|
value
|
||||||
|
.as_f64()
|
||||||
|
.filter(|value| value.is_finite())
|
||||||
|
.map(HostValue::float)
|
||||||
|
.ok_or_else(|| String::from("invalid numeric host value"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn object(fields: &Map<String, Value>) -> Result<HostValue, 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(HostValue::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 {
|
||||||
|
"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(HostValue::Abstract {
|
||||||
|
constraints: all_constraints,
|
||||||
|
default,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"Array" => {
|
||||||
|
let item = fields
|
||||||
|
.get("item")
|
||||||
|
.ok_or_else(|| String::from("Array descriptor requires `item`"))?;
|
||||||
|
Ok(HostValue::ArrayConstraint {
|
||||||
|
item: Box::new(from_json(item)?),
|
||||||
|
constraints,
|
||||||
|
default,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
"Abstract" => Ok(HostValue::Abstract {
|
||||||
|
constraints,
|
||||||
|
default,
|
||||||
|
}),
|
||||||
|
name => Err(format!("unknown Decodal host descriptor `{name}`")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_constraints(value: Option<&Value>) -> 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: &Value) -> 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 {
|
||||||
|
"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: &Value) -> Result<LiteralValue, String> {
|
||||||
|
match value {
|
||||||
|
Value::String(value) => Ok(LiteralValue::String(value.clone())),
|
||||||
|
Value::Bool(value) => Ok(LiteralValue::Bool(*value)),
|
||||||
|
Value::Number(value) => match number(value)? {
|
||||||
|
HostValue::Int(value) => Ok(LiteralValue::Int(value)),
|
||||||
|
HostValue::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, Value>, name: &str) -> Result<&'a str, String> {
|
||||||
|
fields
|
||||||
|
.get(name)
|
||||||
|
.and_then(Value::as_str)
|
||||||
|
.ok_or_else(|| format!("constraint requires string `{name}`"))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use decodal::{Constraint, HostValue, LiteralValue, PrimitiveType};
|
||||||
|
|
||||||
|
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(),
|
||||||
|
HostValue::object([
|
||||||
|
("body", HostValue::string("# Hello")),
|
||||||
|
(
|
||||||
|
"frontmatter",
|
||||||
|
HostValue::object([("draft", HostValue::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(),
|
||||||
|
HostValue::ArrayConstraint {
|
||||||
|
item: Box::new(HostValue::Abstract {
|
||||||
|
constraints: vec![
|
||||||
|
Constraint::Type(PrimitiveType::Int),
|
||||||
|
Constraint::Compare(decodal::CompareOp::Gt, LiteralValue::Int(0)),
|
||||||
|
],
|
||||||
|
default: None,
|
||||||
|
}),
|
||||||
|
constraints: Vec::new(),
|
||||||
|
default: Some(Box::new(HostValue::array([
|
||||||
|
HostValue::int(1),
|
||||||
|
HostValue::int(2),
|
||||||
|
]))),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rejects_null_values() {
|
||||||
|
assert!(
|
||||||
|
from_json(&serde_json::Value::Null)
|
||||||
|
.unwrap_err()
|
||||||
|
.contains("null")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+148
-141
@@ -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 host_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,
|
||||||
)
|
})
|
||||||
})?;
|
.to_string();
|
||||||
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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve_import(current_key: Option<&str>, specifier: &str) -> Option<String> {
|
|
||||||
if specifier.starts_with('/') {
|
|
||||||
return normalize_path(specifier);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut base = String::new();
|
|
||||||
if let Some(current_key) = current_key {
|
|
||||||
if let Some((parent, _file)) = current_key.rsplit_once('/') {
|
|
||||||
base.push_str(parent);
|
|
||||||
base.push('/');
|
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
base.push_str(specifier);
|
let completion = completion.map(|completion| {
|
||||||
normalize_path(&base)
|
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()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn normalize_path(path: &str) -> Option<String> {
|
#[cfg(target_arch = "wasm32")]
|
||||||
let mut parts = Vec::new();
|
fn completion_kind_name(kind: CompletionKind) -> &'static str {
|
||||||
let normalized = path.replace('\\', "/");
|
match kind {
|
||||||
for part in normalized.split('/') {
|
CompletionKind::Keyword => "keyword",
|
||||||
match part {
|
CompletionKind::Constant => "constant",
|
||||||
"" | "." => {}
|
CompletionKind::Type => "type",
|
||||||
".." => {
|
CompletionKind::Variable => "variable",
|
||||||
parts.pop()?;
|
CompletionKind::Namespace => "namespace",
|
||||||
}
|
CompletionKind::Property => "property",
|
||||||
part => parts.push(part),
|
CompletionKind::File => "file",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(target_arch = "wasm32", test))]
|
||||||
|
fn utf16_offset_to_byte(source: &str, offset: usize) -> usize {
|
||||||
|
let mut utf16 = 0usize;
|
||||||
|
for (byte, ch) in source.char_indices() {
|
||||||
|
if utf16 >= offset {
|
||||||
|
return byte;
|
||||||
}
|
}
|
||||||
|
let next = utf16 + ch.len_utf16();
|
||||||
|
if next > offset {
|
||||||
|
return byte;
|
||||||
|
}
|
||||||
|
utf16 = next;
|
||||||
}
|
}
|
||||||
if parts.is_empty() {
|
source.len()
|
||||||
None
|
}
|
||||||
} else {
|
|
||||||
Some(parts.join("/"))
|
#[cfg(any(target_arch = "wasm32", test))]
|
||||||
|
fn byte_offset_to_utf16(source: &str, mut offset: usize) -> usize {
|
||||||
|
offset = offset.min(source.len());
|
||||||
|
while !source.is_char_boundary(offset) {
|
||||||
|
offset = offset.saturating_sub(1);
|
||||||
}
|
}
|
||||||
|
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,209 @@
|
|||||||
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
use decodal::{
|
||||||
|
Diagnostic, DiagnosticKind, Engine, HostEnvironment, HostValue, ImportCandidate, ImportLoader,
|
||||||
|
LoadedImport, Span,
|
||||||
|
};
|
||||||
|
use js_sys::{Function, Reflect};
|
||||||
|
use serde_json::Value;
|
||||||
|
use wasm_bindgen::{JsCast, JsValue};
|
||||||
|
|
||||||
|
use crate::host_value;
|
||||||
|
|
||||||
|
pub(crate) struct JsEnvironment {
|
||||||
|
globals: BTreeMap<String, HostValue>,
|
||||||
|
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)| {
|
||||||
|
host_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 = host_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)
|
||||||
|
}
|
||||||
@@ -41,9 +41,28 @@ The WebAssembly package is for execution, not syntax highlighting.
|
|||||||
|
|
||||||
## Language tools
|
## Language tools
|
||||||
|
|
||||||
Source-level tooling lives in the Rust language tools crate.
|
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.
|
This component is responsible for operations that must preserve source text details such as comments and whitespace.
|
||||||
It is an internal crate used by the future Rust LSP and by the CodeMirror package's bundled formatter WebAssembly.
|
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:
|
Important paths:
|
||||||
|
|
||||||
@@ -52,12 +71,11 @@ crates/decodal-language-tools/
|
|||||||
```
|
```
|
||||||
|
|
||||||
The current language tools crate exposes the formatter.
|
The current language tools crate exposes the formatter.
|
||||||
Future LSP functionality should build on the same source-level tooling layer rather than on the runtime core.
|
|
||||||
|
|
||||||
## 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, editor syntax tree behavior, and the browser formatter command 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:
|
||||||
|
|
||||||
@@ -103,16 +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 formatting and editor syntax: Lezer / CodeMirror
|
- Web formatting and editor syntax: canonical formatter / CodeMirror / Lezer
|
||||||
- Rust LSP internals: `decodal-language-tools`
|
- 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.
|
||||||
|
|||||||
@@ -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` を返す。
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -131,11 +131,95 @@ 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 `HostValue` 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 `HostValue`, 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(
|
||||||
|
{ frontmatter: {...}, body: "..." }
|
||||||
|
)
|
||||||
|
-> Engine internalizes HostValue
|
||||||
|
-> 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 host value by its stable import key and logical value path, such as `content/post.md` and `frontmatter.draft`.
|
||||||
|
`HostValue` 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.
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ 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 +69,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 または構造化済み `HostValue` を返す。
|
||||||
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 にする。
|
||||||
|
|||||||
@@ -104,7 +104,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 +114,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
|
||||||
|
|
||||||
|
|||||||
@@ -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,33 +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`, the Rust source crate `decodal-wasm`, and `decodal-language-tools` 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 `packages/decodal-wasm/` is published to npm and JSR.
|
||||||
The CodeMirror package bundles the generated formatter WebAssembly from `decodal-language-tools`.
|
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
|
||||||
|
|
||||||
@@ -91,8 +83,10 @@ npm run build
|
|||||||
```
|
```
|
||||||
|
|
||||||
`npm run build:wasm` builds both generated WebAssembly packages.
|
`npm run build:wasm` builds both generated WebAssembly packages.
|
||||||
`npm run build:runtime` writes generated runtime files into `packages/decodal-wasm/`.
|
`npm run build:wasm:runtime` writes generated runtime files into `packages/decodal-wasm/`.
|
||||||
`npm run build:tools` writes generated language tools files into `packages/decodal-codemirror/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.
|
||||||
|
|
||||||
These generated files are committed so the site can be built without requiring every consumer to regenerate the wasm packages first.
|
These generated files are committed so the site can be built without requiring every consumer to regenerate the wasm packages first.
|
||||||
|
|
||||||
@@ -113,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.
|
||||||
@@ -137,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.
|
||||||
|
|
||||||
@@ -166,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
|
||||||
|
|||||||
@@ -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 が構築した `HostValue` を import 結果として返す。
|
||||||
|
Markdown、JSON、TOML などの解釈規則は core に固定せず、loader がファイル種別を判定して構造化する。
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use decodal::{HostValue, 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,
|
||||||
|
HostValue::object([
|
||||||
|
("frontmatter", parsed.frontmatter),
|
||||||
|
("body", HostValue::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 循環に該当する。
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@hare/decodal-codemirror",
|
"name": "@hare/decodal-codemirror",
|
||||||
"version": "0.1.5",
|
"version": "0.1.6",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/mod.ts",
|
".": "./src/mod.ts",
|
||||||
@@ -15,6 +15,8 @@
|
|||||||
"publish": {
|
"publish": {
|
||||||
"include": [
|
"include": [
|
||||||
"README.md",
|
"README.md",
|
||||||
|
"LICENSE-MIT",
|
||||||
|
"LICENSE-APACHE",
|
||||||
"src/mod.ts",
|
"src/mod.ts",
|
||||||
"src/format.ts",
|
"src/format.ts",
|
||||||
"src/decodal.js",
|
"src/decodal.js",
|
||||||
|
|||||||
+3
-2
@@ -1,15 +1,16 @@
|
|||||||
{
|
{
|
||||||
"name": "decodal-codemirror",
|
"name": "decodal-codemirror",
|
||||||
"version": "0.1.5",
|
"version": "0.1.6",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "decodal-codemirror",
|
"name": "decodal-codemirror",
|
||||||
"version": "0.1.5",
|
"version": "0.1.6",
|
||||||
"license": "MIT OR Apache-2.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@codemirror/language": "^6.12.4",
|
"@codemirror/language": "^6.12.4",
|
||||||
|
"@codemirror/state": "^6.7.1",
|
||||||
"@codemirror/view": "^6.43.6",
|
"@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,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "decodal-codemirror",
|
"name": "decodal-codemirror",
|
||||||
"version": "0.1.5",
|
"version": "0.1.6",
|
||||||
"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,6 +9,9 @@
|
|||||||
"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",
|
||||||
@@ -25,6 +28,8 @@
|
|||||||
"types": "./src/decodal.d.ts",
|
"types": "./src/decodal.d.ts",
|
||||||
"files": [
|
"files": [
|
||||||
"README.md",
|
"README.md",
|
||||||
|
"LICENSE-MIT",
|
||||||
|
"LICENSE-APACHE",
|
||||||
"src/",
|
"src/",
|
||||||
"wasm/decodal_language_tools.js",
|
"wasm/decodal_language_tools.js",
|
||||||
"wasm/decodal_language_tools.d.ts",
|
"wasm/decodal_language_tools.d.ts",
|
||||||
@@ -44,6 +49,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@codemirror/language": "^6.12.4",
|
"@codemirror/language": "^6.12.4",
|
||||||
|
"@codemirror/state": "^6.7.1",
|
||||||
"@codemirror/view": "^6.43.6",
|
"@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,4 +1,4 @@
|
|||||||
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';
|
||||||
|
|
||||||
@@ -19,10 +19,10 @@ const parserWithMetadata = parser.configure({
|
|||||||
'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,
|
Array: delimitedIndent({ closing: ']', align: false }),
|
||||||
ArrayConstraint: context => context.column(context.node.from) + context.unit,
|
ArrayConstraint: delimitedIndent({ closing: ']', align: false }),
|
||||||
MatchExpression: context => context.column(context.node.from) + context.unit,
|
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({
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
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';
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
@@ -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 });
|
||||||
|
});
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* Decodal source formatting helpers for CodeMirror integrations.
|
* Decodal source formatting helpers for CodeMirror integrations.
|
||||||
*
|
*
|
||||||
* The formatter is backed by WebAssembly generated from the internal Rust
|
* The formatter is backed by WebAssembly generated from the canonical Rust
|
||||||
* `decodal-language-tools` crate. Call {@link initDecodalFormatter} once before
|
* `decodal-language-tools` formatter. Call {@link initDecodalFormatter} once before
|
||||||
* using {@link formatDecodal} or {@link formatDecodalCommand}.
|
* using {@link formatDecodal} or {@link formatDecodalCommand}.
|
||||||
*
|
*
|
||||||
* @module
|
* @module
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
*
|
*
|
||||||
* @module
|
* @module
|
||||||
*/
|
*/
|
||||||
import { HighlightStyle, LRLanguage, LanguageSupport, foldNodeProp, indentNodeProp, syntaxHighlighting } from 'npm:@codemirror/language@^6.12.4';
|
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';
|
||||||
|
|
||||||
@@ -29,10 +29,10 @@ const parserWithMetadata = parser.configure({
|
|||||||
'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,
|
Array: delimitedIndent({ closing: ']', align: false }),
|
||||||
ArrayConstraint: context => context.column(context.node.from) + context.unit,
|
ArrayConstraint: delimitedIndent({ closing: ']', align: false }),
|
||||||
MatchExpression: context => context.column(context.node.from) + context.unit,
|
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({
|
||||||
|
|||||||
Binary file not shown.
@@ -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,7 +2,7 @@
|
|||||||
|
|
||||||
WebAssembly runtime package for Decodal.
|
WebAssembly runtime package for Decodal.
|
||||||
|
|
||||||
This package exposes the Decodal evaluator to browsers and other JavaScript runtimes that can load WebAssembly modules.
|
This package exposes the Decodal evaluator and shared language service to browsers and other JavaScript runtimes that can load WebAssembly modules.
|
||||||
It is generated from the Rust crate in `crates/decodal-wasm` with `wasm-pack` and is used by the official playground.
|
It is generated from the Rust crate in `crates/decodal-wasm` with `wasm-pack` and is used by the official playground.
|
||||||
|
|
||||||
Use `decodal-codemirror` separately when you need CodeMirror 6 language support.
|
Use `decodal-codemirror` separately when you need CodeMirror 6 language support.
|
||||||
@@ -22,15 +22,48 @@ deno add jsr:@hare/decodal-wasm
|
|||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```js
|
```js
|
||||||
import init, { evaluateProject } from 'decodal-wasm';
|
import init, { DecodalLanguageService } from 'decodal-wasm';
|
||||||
|
|
||||||
await init();
|
await init();
|
||||||
|
|
||||||
const result = evaluateProject('main.dcdl', JSON.stringify({
|
const files = {
|
||||||
'main.dcdl': 'Server = { port = Int default 8080; };',
|
'main.dcdl': 'let schema = import "./schema.dcdl"; in schema.Server',
|
||||||
}));
|
'schema.dcdl': 'Server = App.Server & { port = 8080; };',
|
||||||
|
};
|
||||||
|
|
||||||
|
const service = new DecodalLanguageService({
|
||||||
|
globals: {
|
||||||
|
App: {
|
||||||
|
Server: {
|
||||||
|
port: { $decodal: 'Int' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
loadImport(_currentKey, specifier) {
|
||||||
|
const key = specifier.startsWith('./') ? specifier.slice(2) : specifier;
|
||||||
|
return { kind: 'source', key, name: key, source: files[key] };
|
||||||
|
},
|
||||||
|
completeImport() {
|
||||||
|
return ['./schema.dcdl'];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = service.evaluate('main.dcdl', 'main.dcdl', files['main.dcdl']);
|
||||||
|
|
||||||
console.log(JSON.parse(result));
|
console.log(JSON.parse(result));
|
||||||
|
|
||||||
|
const completion = service.complete(
|
||||||
|
'main.dcdl',
|
||||||
|
'App.Server.po',
|
||||||
|
'App.Server.po'.length,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(JSON.parse(completion));
|
||||||
```
|
```
|
||||||
|
|
||||||
The exported functions return JSON strings so callers can handle diagnostics and successful results without depending on Rust data structures.
|
`globals`, `loadImport`, and `completeImport` are host-owned. The package does not assume a filesystem or virtual project model. Import callbacks are synchronous; preload or cache remote content before evaluation.
|
||||||
|
|
||||||
|
Plain strings, numbers, booleans, arrays, and objects are concrete host values. Use `{ $decodal: 'String' | 'Int' | 'Float' | 'Bool' }` for primitive schemas and `{ $decodal: 'Array', item: value }` for array schemas. Descriptors also accept `constraints` and `default`.
|
||||||
|
|
||||||
|
Methods return JSON strings so callers can handle diagnostics and successful results without depending on Rust data structures.
|
||||||
|
|||||||
+89
-4
@@ -1,20 +1,105 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
|
|
||||||
export function evaluate(source: string): string;
|
/** A concrete JavaScript value or Decodal schema descriptor supplied by the host. */
|
||||||
|
export type DecodalHostValue =
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| DecodalHostValue[]
|
||||||
|
| { [field: string]: DecodalHostValue }
|
||||||
|
| DecodalPrimitiveDescriptor
|
||||||
|
| DecodalArrayDescriptor
|
||||||
|
| DecodalAbstractDescriptor;
|
||||||
|
|
||||||
export function evaluateProject(entry: string, files_json: string): string;
|
export type DecodalPrimitiveType = 'String' | 'Int' | 'Float' | 'Bool';
|
||||||
|
|
||||||
|
export type DecodalConstraint =
|
||||||
|
| { kind: 'type'; value: DecodalPrimitiveType }
|
||||||
|
| { kind: 'compare'; op: '>' | '>=' | '<' | '<='; value: string | number | boolean }
|
||||||
|
| { kind: 'regex'; value: string }
|
||||||
|
| { kind: 'predicate'; value: string };
|
||||||
|
|
||||||
|
export interface DecodalPrimitiveDescriptor {
|
||||||
|
$decodal: DecodalPrimitiveType;
|
||||||
|
constraints?: DecodalConstraint[];
|
||||||
|
default?: DecodalHostValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DecodalArrayDescriptor {
|
||||||
|
$decodal: 'Array';
|
||||||
|
item: DecodalHostValue;
|
||||||
|
constraints?: DecodalConstraint[];
|
||||||
|
default?: DecodalHostValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DecodalAbstractDescriptor {
|
||||||
|
$decodal: 'Abstract';
|
||||||
|
constraints?: DecodalConstraint[];
|
||||||
|
default?: DecodalHostValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DecodalLoadedImport =
|
||||||
|
| { kind: 'source'; key: string; name?: string; source: string }
|
||||||
|
| { kind: 'value'; key: string; value: DecodalHostValue };
|
||||||
|
|
||||||
|
export type DecodalImportCandidate =
|
||||||
|
| string
|
||||||
|
| { specifier: string; detail?: string };
|
||||||
|
|
||||||
|
/** Host-owned environment shared by evaluation and language tooling. */
|
||||||
|
export interface DecodalEnvironment {
|
||||||
|
globals?: Record<string, DecodalHostValue>;
|
||||||
|
/** Synchronous import callback. Preload or cache asynchronous resources first. */
|
||||||
|
loadImport?: (currentKey: string | null, specifier: string) => DecodalLoadedImport;
|
||||||
|
/** Synchronous import completion callback. */
|
||||||
|
completeImport?: (currentKey: string | null, prefix: string) => DecodalImportCandidate[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A browser-facing language service configured entirely by its JavaScript host.
|
||||||
|
*
|
||||||
|
* The host owns globals, import loading, and import completion. This keeps
|
||||||
|
* filesystem, network, and virtual-project policy outside the WASM package.
|
||||||
|
*/
|
||||||
|
export class DecodalLanguageService {
|
||||||
|
free(): void;
|
||||||
|
[Symbol.dispose](): void;
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
*/
|
||||||
|
complete(key: string, source: string, position: number, explicit: boolean): string;
|
||||||
|
/**
|
||||||
|
* Evaluates a source using the injected globals and import loader.
|
||||||
|
*/
|
||||||
|
evaluate(key: string, name: string, source: string): string;
|
||||||
|
constructor(options?: DecodalEnvironment);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluates one standalone Decodal source with no host globals or imports.
|
||||||
|
*/
|
||||||
|
export function evaluate(source: 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 __wbg_decodallanguageservice_free: (a: number, b: number) => void;
|
||||||
|
readonly decodallanguageservice_complete: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => [number, number];
|
||||||
|
readonly decodallanguageservice_evaluate: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => [number, number];
|
||||||
|
readonly decodallanguageservice_new: (a: any) => [number, number, number];
|
||||||
readonly evaluate: (a: number, b: number) => [number, number];
|
readonly evaluate: (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_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;
|
||||||
|
readonly __wbindgen_exn_store: (a: number) => void;
|
||||||
|
readonly __externref_table_alloc: () => number;
|
||||||
|
readonly __wbindgen_externrefs: WebAssembly.Table;
|
||||||
readonly __wbindgen_free: (a: number, b: number, c: number) => void;
|
readonly __wbindgen_free: (a: number, b: number, c: number) => void;
|
||||||
|
readonly __externref_table_dealloc: (a: number) => void;
|
||||||
readonly __wbindgen_start: () => void;
|
readonly __wbindgen_start: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,91 @@
|
|||||||
/* @ts-self-types="./decodal_wasm.d.ts" */
|
/* @ts-self-types="./decodal_wasm.d.ts" */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* A browser-facing language service configured entirely by its JavaScript host.
|
||||||
|
*
|
||||||
|
* The host owns globals, import loading, and import completion. This keeps
|
||||||
|
* filesystem, network, and virtual-project policy outside the WASM package.
|
||||||
|
*/
|
||||||
|
export class DecodalLanguageService {
|
||||||
|
__destroy_into_raw() {
|
||||||
|
const ptr = this.__wbg_ptr;
|
||||||
|
this.__wbg_ptr = 0;
|
||||||
|
DecodalLanguageServiceFinalization.unregister(this);
|
||||||
|
return ptr;
|
||||||
|
}
|
||||||
|
free() {
|
||||||
|
const ptr = this.__destroy_into_raw();
|
||||||
|
wasm.__wbg_decodallanguageservice_free(ptr, 0);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
* @param {string} key
|
||||||
|
* @param {string} source
|
||||||
|
* @param {number} position
|
||||||
|
* @param {boolean} explicit
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
complete(key, source, position, explicit) {
|
||||||
|
let deferred3_0;
|
||||||
|
let deferred3_1;
|
||||||
|
try {
|
||||||
|
const ptr0 = passStringToWasm0(key, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
const len0 = WASM_VECTOR_LEN;
|
||||||
|
const ptr1 = passStringToWasm0(source, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
const len1 = WASM_VECTOR_LEN;
|
||||||
|
const ret = wasm.decodallanguageservice_complete(this.__wbg_ptr, ptr0, len0, ptr1, len1, position, explicit);
|
||||||
|
deferred3_0 = ret[0];
|
||||||
|
deferred3_1 = ret[1];
|
||||||
|
return getStringFromWasm0(ret[0], ret[1]);
|
||||||
|
} finally {
|
||||||
|
wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Evaluates a source using the injected globals and import loader.
|
||||||
|
* @param {string} key
|
||||||
|
* @param {string} name
|
||||||
|
* @param {string} source
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
evaluate(key, name, source) {
|
||||||
|
let deferred4_0;
|
||||||
|
let deferred4_1;
|
||||||
|
try {
|
||||||
|
const ptr0 = passStringToWasm0(key, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
const len0 = WASM_VECTOR_LEN;
|
||||||
|
const ptr1 = passStringToWasm0(name, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
const len1 = WASM_VECTOR_LEN;
|
||||||
|
const ptr2 = passStringToWasm0(source, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
const len2 = WASM_VECTOR_LEN;
|
||||||
|
const ret = wasm.decodallanguageservice_evaluate(this.__wbg_ptr, ptr0, len0, ptr1, len1, ptr2, len2);
|
||||||
|
deferred4_0 = ret[0];
|
||||||
|
deferred4_1 = ret[1];
|
||||||
|
return getStringFromWasm0(ret[0], ret[1]);
|
||||||
|
} finally {
|
||||||
|
wasm.__wbindgen_free(deferred4_0, deferred4_1, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param {any} options
|
||||||
|
*/
|
||||||
|
constructor(options) {
|
||||||
|
const ret = wasm.decodallanguageservice_new(options);
|
||||||
|
if (ret[2]) {
|
||||||
|
throw takeFromExternrefTable0(ret[1]);
|
||||||
|
}
|
||||||
|
this.__wbg_ptr = ret[0];
|
||||||
|
DecodalLanguageServiceFinalization.register(this, this.__wbg_ptr, this);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Symbol.dispose) DecodalLanguageService.prototype[Symbol.dispose] = DecodalLanguageService.prototype.free;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Evaluates one standalone Decodal source with no host globals or imports.
|
||||||
* @param {string} source
|
* @param {string} source
|
||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
@@ -18,31 +103,204 @@ 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,
|
||||||
|
__wbg_Error_fdd633d4bb5dd76a: function(arg0, arg1) {
|
||||||
|
const ret = Error(getStringFromWasm0(arg0, arg1));
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
__wbg_String_8564e559799eccda: function(arg0, arg1) {
|
||||||
|
const ret = String(arg1);
|
||||||
|
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
const len1 = WASM_VECTOR_LEN;
|
||||||
|
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
||||||
|
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
||||||
|
},
|
||||||
|
__wbg___wbindgen_bigint_get_as_i64_d9e915702856f831: function(arg0, arg1) {
|
||||||
|
const v = arg1;
|
||||||
|
const ret = typeof(v) === 'bigint' ? v : undefined;
|
||||||
|
getDataViewMemory0().setBigInt64(arg0 + 8 * 1, isLikeNone(ret) ? BigInt(0) : ret, true);
|
||||||
|
getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true);
|
||||||
|
},
|
||||||
|
__wbg___wbindgen_boolean_get_edaed31a367ce1bd: function(arg0) {
|
||||||
|
const v = arg0;
|
||||||
|
const ret = typeof(v) === 'boolean' ? v : undefined;
|
||||||
|
return isLikeNone(ret) ? 0xFFFFFF : ret ? 1 : 0;
|
||||||
|
},
|
||||||
|
__wbg___wbindgen_debug_string_8a447059637473e2: function(arg0, arg1) {
|
||||||
|
const ret = debugString(arg1);
|
||||||
|
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
const len1 = WASM_VECTOR_LEN;
|
||||||
|
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
||||||
|
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
||||||
|
},
|
||||||
|
__wbg___wbindgen_in_4990f46af709e33c: function(arg0, arg1) {
|
||||||
|
const ret = arg0 in arg1;
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
__wbg___wbindgen_is_bigint_90b5ccfe67c78460: function(arg0) {
|
||||||
|
const ret = typeof(arg0) === 'bigint';
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
__wbg___wbindgen_is_function_acc5528be2b923f2: function(arg0) {
|
||||||
|
const ret = typeof(arg0) === 'function';
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
__wbg___wbindgen_is_null_6d937fbfb6478470: function(arg0) {
|
||||||
|
const ret = arg0 === null;
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
__wbg___wbindgen_is_object_0beba4a1980d3eea: function(arg0) {
|
||||||
|
const val = arg0;
|
||||||
|
const ret = typeof(val) === 'object' && val !== null;
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
__wbg___wbindgen_is_undefined_721f8decd50c87a3: function(arg0) {
|
||||||
|
const ret = arg0 === undefined;
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
__wbg___wbindgen_jsval_eq_4e8c38722cb8ff51: function(arg0, arg1) {
|
||||||
|
const ret = arg0 === arg1;
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
__wbg___wbindgen_jsval_loose_eq_4b9aba9e5b3c4582: function(arg0, arg1) {
|
||||||
|
const ret = arg0 == arg1;
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
__wbg___wbindgen_number_get_1cc01dd708740256: function(arg0, arg1) {
|
||||||
|
const obj = arg1;
|
||||||
|
const ret = typeof(obj) === 'number' ? obj : undefined;
|
||||||
|
getDataViewMemory0().setFloat64(arg0 + 8 * 1, isLikeNone(ret) ? 0 : ret, true);
|
||||||
|
getDataViewMemory0().setInt32(arg0 + 4 * 0, !isLikeNone(ret), true);
|
||||||
|
},
|
||||||
|
__wbg___wbindgen_string_get_71bb4348194e31f0: function(arg0, arg1) {
|
||||||
|
const obj = arg1;
|
||||||
|
const ret = typeof(obj) === 'string' ? obj : undefined;
|
||||||
|
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||||
|
var len1 = WASM_VECTOR_LEN;
|
||||||
|
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
|
||||||
|
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
|
||||||
|
},
|
||||||
|
__wbg___wbindgen_throw_ea4887a5f8f9a9db: function(arg0, arg1) {
|
||||||
|
throw new Error(getStringFromWasm0(arg0, arg1));
|
||||||
|
},
|
||||||
|
__wbg_call_0e855b388e315e17: function() { return handleError(function (arg0, arg1, arg2, arg3) {
|
||||||
|
const ret = arg0.call(arg1, arg2, arg3);
|
||||||
|
return ret;
|
||||||
|
}, arguments); },
|
||||||
|
__wbg_call_8e98ed2f3c86c4b5: function() { return handleError(function (arg0, arg1) {
|
||||||
|
const ret = arg0.call(arg1);
|
||||||
|
return ret;
|
||||||
|
}, arguments); },
|
||||||
|
__wbg_done_b62d4a7d2286852a: function(arg0) {
|
||||||
|
const ret = arg0.done;
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
__wbg_entries_c261c3fa1f281256: function(arg0) {
|
||||||
|
const ret = Object.entries(arg0);
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
__wbg_get_197a3fe98f169e38: function(arg0, arg1) {
|
||||||
|
const ret = arg0[arg1 >>> 0];
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
__wbg_get_9a29be2cb383ed9a: function() { return handleError(function (arg0, arg1) {
|
||||||
|
const ret = Reflect.get(arg0, arg1);
|
||||||
|
return ret;
|
||||||
|
}, arguments); },
|
||||||
|
__wbg_get_dddb90ff5d27a080: function() { return handleError(function (arg0, arg1) {
|
||||||
|
const ret = Reflect.get(arg0, arg1);
|
||||||
|
return ret;
|
||||||
|
}, arguments); },
|
||||||
|
__wbg_get_unchecked_54a4374c38e08460: function(arg0, arg1) {
|
||||||
|
const ret = arg0[arg1 >>> 0];
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
__wbg_instanceof_ArrayBuffer_2a7bb09fee70c2da: function(arg0) {
|
||||||
|
let result;
|
||||||
|
try {
|
||||||
|
result = arg0 instanceof ArrayBuffer;
|
||||||
|
} catch (_) {
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
const ret = result;
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
__wbg_instanceof_Map_afa18d5840c04c15: function(arg0) {
|
||||||
|
let result;
|
||||||
|
try {
|
||||||
|
result = arg0 instanceof Map;
|
||||||
|
} catch (_) {
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
const ret = result;
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
__wbg_instanceof_Uint8Array_f080092dc70f5d58: function(arg0) {
|
||||||
|
let result;
|
||||||
|
try {
|
||||||
|
result = arg0 instanceof Uint8Array;
|
||||||
|
} catch (_) {
|
||||||
|
result = false;
|
||||||
|
}
|
||||||
|
const ret = result;
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
__wbg_isArray_145a34fd0a38d37b: function(arg0) {
|
||||||
|
const ret = Array.isArray(arg0);
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
__wbg_isSafeInteger_a3389a198582f5f6: function(arg0) {
|
||||||
|
const ret = Number.isSafeInteger(arg0);
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
__wbg_iterator_cc47ba25a2be735a: function() {
|
||||||
|
const ret = Symbol.iterator;
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
__wbg_length_589238bdcf171f0e: function(arg0) {
|
||||||
|
const ret = arg0.length;
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
__wbg_length_c6054974c0a6cdb9: function(arg0) {
|
||||||
|
const ret = arg0.length;
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
__wbg_new_81880fb5002cb255: function(arg0) {
|
||||||
|
const ret = new Uint8Array(arg0);
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
__wbg_next_0c4066e251d2eff9: function() { return handleError(function (arg0) {
|
||||||
|
const ret = arg0.next();
|
||||||
|
return ret;
|
||||||
|
}, arguments); },
|
||||||
|
__wbg_next_402fa10b59ab20c3: function(arg0) {
|
||||||
|
const ret = arg0.next;
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
__wbg_prototypesetcall_d721637c7ca66eb8: function(arg0, arg1, arg2) {
|
||||||
|
Uint8Array.prototype.set.call(getArrayU8FromWasm0(arg0, arg1), arg2);
|
||||||
|
},
|
||||||
|
__wbg_value_49f783bb59765962: function(arg0) {
|
||||||
|
const ret = arg0.value;
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
__wbindgen_cast_0000000000000001: function(arg0) {
|
||||||
|
// Cast intrinsic for `I64 -> Externref`.
|
||||||
|
const ret = arg0;
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
__wbindgen_cast_0000000000000002: function(arg0, arg1) {
|
||||||
|
// Cast intrinsic for `Ref(String) -> Externref`.
|
||||||
|
const ret = getStringFromWasm0(arg0, arg1);
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
|
__wbindgen_cast_0000000000000003: function(arg0) {
|
||||||
|
// Cast intrinsic for `U64 -> Externref`.
|
||||||
|
const ret = BigInt.asUintN(64, arg0);
|
||||||
|
return ret;
|
||||||
|
},
|
||||||
__wbindgen_init_externref_table: function() {
|
__wbindgen_init_externref_table: function() {
|
||||||
const table = wasm.__wbindgen_externrefs;
|
const table = wasm.__wbindgen_externrefs;
|
||||||
const offset = table.grow(4);
|
const offset = table.grow(4);
|
||||||
@@ -59,6 +317,94 @@ function __wbg_get_imports() {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DecodalLanguageServiceFinalization = (typeof FinalizationRegistry === 'undefined')
|
||||||
|
? { register: () => {}, unregister: () => {} }
|
||||||
|
: new FinalizationRegistry(ptr => wasm.__wbg_decodallanguageservice_free(ptr, 1));
|
||||||
|
|
||||||
|
function addToExternrefTable0(obj) {
|
||||||
|
const idx = wasm.__externref_table_alloc();
|
||||||
|
wasm.__wbindgen_externrefs.set(idx, obj);
|
||||||
|
return idx;
|
||||||
|
}
|
||||||
|
|
||||||
|
function debugString(val) {
|
||||||
|
// primitive types
|
||||||
|
const type = typeof val;
|
||||||
|
if (type == 'number' || type == 'boolean' || val == null) {
|
||||||
|
return `${val}`;
|
||||||
|
}
|
||||||
|
if (type == 'string') {
|
||||||
|
return `"${val}"`;
|
||||||
|
}
|
||||||
|
if (type == 'symbol') {
|
||||||
|
const description = val.description;
|
||||||
|
if (description == null) {
|
||||||
|
return 'Symbol';
|
||||||
|
} else {
|
||||||
|
return `Symbol(${description})`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (type == 'function') {
|
||||||
|
const name = val.name;
|
||||||
|
if (typeof name == 'string' && name.length > 0) {
|
||||||
|
return `Function(${name})`;
|
||||||
|
} else {
|
||||||
|
return 'Function';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// objects
|
||||||
|
if (Array.isArray(val)) {
|
||||||
|
const length = val.length;
|
||||||
|
let debug = '[';
|
||||||
|
if (length > 0) {
|
||||||
|
debug += debugString(val[0]);
|
||||||
|
}
|
||||||
|
for(let i = 1; i < length; i++) {
|
||||||
|
debug += ', ' + debugString(val[i]);
|
||||||
|
}
|
||||||
|
debug += ']';
|
||||||
|
return debug;
|
||||||
|
}
|
||||||
|
// Test for built-in
|
||||||
|
const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val));
|
||||||
|
let className;
|
||||||
|
if (builtInMatches && builtInMatches.length > 1) {
|
||||||
|
className = builtInMatches[1];
|
||||||
|
} else {
|
||||||
|
// Failed to match the standard '[object ClassName]'
|
||||||
|
return toString.call(val);
|
||||||
|
}
|
||||||
|
if (className == 'Object') {
|
||||||
|
// we're a user defined class or Object
|
||||||
|
// JSON.stringify avoids problems with cycles, and is generally much
|
||||||
|
// easier than looping through ownProperties of `val`.
|
||||||
|
try {
|
||||||
|
return 'Object(' + JSON.stringify(val) + ')';
|
||||||
|
} catch (_) {
|
||||||
|
return 'Object';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// errors
|
||||||
|
if (val instanceof Error) {
|
||||||
|
return `${val.name}: ${val.message}\n${val.stack}`;
|
||||||
|
}
|
||||||
|
// TODO we could test for more things here, like `Set`s and `Map`s.
|
||||||
|
return className;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getArrayU8FromWasm0(ptr, len) {
|
||||||
|
ptr = ptr >>> 0;
|
||||||
|
return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len);
|
||||||
|
}
|
||||||
|
|
||||||
|
let cachedDataViewMemory0 = null;
|
||||||
|
function getDataViewMemory0() {
|
||||||
|
if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) {
|
||||||
|
cachedDataViewMemory0 = new DataView(wasm.memory.buffer);
|
||||||
|
}
|
||||||
|
return cachedDataViewMemory0;
|
||||||
|
}
|
||||||
|
|
||||||
function getStringFromWasm0(ptr, len) {
|
function getStringFromWasm0(ptr, len) {
|
||||||
return decodeText(ptr >>> 0, len);
|
return decodeText(ptr >>> 0, len);
|
||||||
}
|
}
|
||||||
@@ -71,6 +417,19 @@ function getUint8ArrayMemory0() {
|
|||||||
return cachedUint8ArrayMemory0;
|
return cachedUint8ArrayMemory0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleError(f, args) {
|
||||||
|
try {
|
||||||
|
return f.apply(this, args);
|
||||||
|
} catch (e) {
|
||||||
|
const idx = addToExternrefTable0(e);
|
||||||
|
wasm.__wbindgen_exn_store(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isLikeNone(x) {
|
||||||
|
return x === undefined || x === null;
|
||||||
|
}
|
||||||
|
|
||||||
function passStringToWasm0(arg, malloc, realloc) {
|
function passStringToWasm0(arg, malloc, realloc) {
|
||||||
if (realloc === undefined) {
|
if (realloc === undefined) {
|
||||||
const buf = cachedTextEncoder.encode(arg);
|
const buf = cachedTextEncoder.encode(arg);
|
||||||
@@ -108,6 +467,12 @@ function passStringToWasm0(arg, malloc, realloc) {
|
|||||||
return ptr;
|
return ptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function takeFromExternrefTable0(idx) {
|
||||||
|
const value = wasm.__wbindgen_externrefs.get(idx);
|
||||||
|
wasm.__externref_table_dealloc(idx);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
|
let cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
|
||||||
cachedTextDecoder.decode();
|
cachedTextDecoder.decode();
|
||||||
const MAX_SAFARI_DECODE_BYTES = 2146435072;
|
const MAX_SAFARI_DECODE_BYTES = 2146435072;
|
||||||
@@ -142,6 +507,7 @@ function __wbg_finalize_init(instance, module) {
|
|||||||
wasmInstance = instance;
|
wasmInstance = instance;
|
||||||
wasm = instance.exports;
|
wasm = instance.exports;
|
||||||
wasmModule = module;
|
wasmModule = module;
|
||||||
|
cachedDataViewMemory0 = null;
|
||||||
cachedUint8ArrayMemory0 = null;
|
cachedUint8ArrayMemory0 = null;
|
||||||
wasm.__wbindgen_start();
|
wasm.__wbindgen_start();
|
||||||
return wasm;
|
return wasm;
|
||||||
|
|||||||
Binary file not shown.
+8
-2
@@ -1,10 +1,16 @@
|
|||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
export const memory: WebAssembly.Memory;
|
export const memory: WebAssembly.Memory;
|
||||||
|
export const __wbg_decodallanguageservice_free: (a: number, b: number) => void;
|
||||||
|
export const decodallanguageservice_complete: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => [number, number];
|
||||||
|
export const decodallanguageservice_evaluate: (a: number, b: number, c: number, d: number, e: number, f: number, g: number) => [number, number];
|
||||||
|
export const decodallanguageservice_new: (a: any) => [number, number, number];
|
||||||
export const evaluate: (a: number, b: number) => [number, number];
|
export const evaluate: (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_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;
|
||||||
|
export const __wbindgen_exn_store: (a: number) => void;
|
||||||
|
export const __externref_table_alloc: () => number;
|
||||||
|
export const __wbindgen_externrefs: WebAssembly.Table;
|
||||||
export const __wbindgen_free: (a: number, b: number, c: number) => void;
|
export const __wbindgen_free: (a: number, b: number, c: number) => void;
|
||||||
|
export const __externref_table_dealloc: (a: number) => void;
|
||||||
export const __wbindgen_start: () => void;
|
export const __wbindgen_start: () => void;
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
{
|
{
|
||||||
"name": "@hare/decodal-wasm",
|
"name": "@hare/decodal-wasm",
|
||||||
"version": "0.1.3",
|
"version": "0.1.4",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"exports": "./mod.ts",
|
"exports": "./mod.ts",
|
||||||
"publish": {
|
"publish": {
|
||||||
"include": [
|
"include": [
|
||||||
"README.md",
|
"README.md",
|
||||||
|
"LICENSE-MIT",
|
||||||
|
"LICENSE-APACHE",
|
||||||
"mod.ts",
|
"mod.ts",
|
||||||
"decodal_wasm.js",
|
"decodal_wasm.js",
|
||||||
"decodal_wasm.d.ts",
|
"decodal_wasm.d.ts",
|
||||||
|
|||||||
@@ -1,66 +1,26 @@
|
|||||||
/**
|
/**
|
||||||
* Browser and JavaScript runtime bindings for the Decodal evaluator.
|
* Host-configurable Decodal evaluator and language service for JavaScript.
|
||||||
*
|
|
||||||
* This module wraps the wasm-bindgen output generated from the Rust
|
|
||||||
* `decodal-wasm` crate and exposes stable, documented entrypoints for JSR
|
|
||||||
* users. The exported evaluator functions return JSON strings.
|
|
||||||
*
|
*
|
||||||
* @module
|
* @module
|
||||||
*/
|
*/
|
||||||
import initWasm, {
|
export {
|
||||||
evaluate as evaluateImpl,
|
DecodalLanguageService,
|
||||||
evaluateProject as evaluateProjectImpl,
|
default,
|
||||||
initSync as initSyncImpl,
|
evaluate,
|
||||||
|
initSync,
|
||||||
} from './decodal_wasm.js';
|
} from './decodal_wasm.js';
|
||||||
|
|
||||||
/** Input accepted by the wasm-bindgen initializer. */
|
export type {
|
||||||
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
|
DecodalAbstractDescriptor,
|
||||||
|
DecodalArrayDescriptor,
|
||||||
/** Initialized Decodal WebAssembly exports. */
|
DecodalConstraint,
|
||||||
export interface InitOutput {
|
DecodalEnvironment,
|
||||||
/** Linear memory exported by the WebAssembly module. */
|
DecodalHostValue,
|
||||||
readonly memory: WebAssembly.Memory;
|
DecodalImportCandidate,
|
||||||
readonly evaluate: (source: number, len: number) => [number, number, number, number];
|
DecodalLoadedImport,
|
||||||
readonly evaluateProject: (entry: number, entryLen: number, files: number, filesLen: number) => [number, number, number, number];
|
DecodalPrimitiveDescriptor,
|
||||||
}
|
DecodalPrimitiveType,
|
||||||
|
InitInput,
|
||||||
/**
|
InitOutput,
|
||||||
* Initialize the Decodal WebAssembly runtime asynchronously.
|
SyncInitInput,
|
||||||
*
|
} from './decodal_wasm.js';
|
||||||
* @param moduleOrPath - Optional WebAssembly module, bytes, response, URL, or request.
|
|
||||||
* @returns The initialized WebAssembly exports.
|
|
||||||
*/
|
|
||||||
export default async function initDecodalRuntime(moduleOrPath?: InitInput): Promise<InitOutput> {
|
|
||||||
return await initWasm(moduleOrPath) as InitOutput;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the Decodal WebAssembly runtime synchronously.
|
|
||||||
*
|
|
||||||
* @param module - Compiled module, bytes, or wasm-bindgen sync initialization input.
|
|
||||||
* @returns The initialized WebAssembly exports.
|
|
||||||
*/
|
|
||||||
export function initSync(module: WebAssembly.Module | BufferSource): InitOutput {
|
|
||||||
return initSyncImpl(module) as InitOutput;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Evaluate and materialize a single Decodal source string.
|
|
||||||
*
|
|
||||||
* @param source - Decodal source text.
|
|
||||||
* @returns A JSON string containing either `{ ok: true, output }` or `{ ok: false, error }`.
|
|
||||||
*/
|
|
||||||
export function evaluate(source: string): string {
|
|
||||||
return evaluateImpl(source);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Evaluate and materialize a virtual multi-file Decodal project.
|
|
||||||
*
|
|
||||||
* @param entry - Entry point file path to materialize.
|
|
||||||
* @param filesJson - JSON object mapping virtual file paths to source strings.
|
|
||||||
* @returns A JSON string containing either `{ ok: true, output }` or `{ ok: false, error }`.
|
|
||||||
*/
|
|
||||||
export function evaluateProject(entry: string, filesJson: string): string {
|
|
||||||
return evaluateProjectImpl(entry, filesJson);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
{
|
{
|
||||||
"name": "decodal-wasm",
|
"name": "decodal-wasm",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "WebAssembly wrapper for evaluating Decodal in browser playgrounds.",
|
"description": "Host-configurable Decodal evaluator and language service for JavaScript runtimes.",
|
||||||
"version": "0.1.3",
|
"version": "0.1.4",
|
||||||
"license": "MIT OR Apache-2.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "https://gitea.hareworks.net/Hare/Decodal"
|
"url": "https://gitea.hareworks.net/Hare/Decodal"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
|
"README.md",
|
||||||
|
"LICENSE-MIT",
|
||||||
|
"LICENSE-APACHE",
|
||||||
"decodal_wasm_bg.wasm",
|
"decodal_wasm_bg.wasm",
|
||||||
"decodal_wasm.js",
|
"decodal_wasm.js",
|
||||||
"decodal_wasm.d.ts"
|
"decodal_wasm.d.ts"
|
||||||
@@ -23,5 +26,13 @@
|
|||||||
"wasm",
|
"wasm",
|
||||||
"dsl",
|
"dsl",
|
||||||
"config"
|
"config"
|
||||||
]
|
],
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./decodal_wasm.d.ts",
|
||||||
|
"import": "./decodal_wasm.js"
|
||||||
|
},
|
||||||
|
"./decodal_wasm_bg.wasm": "./decodal_wasm_bg.wasm",
|
||||||
|
"./package.json": "./package.json"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+3
-2
@@ -25,10 +25,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"../../packages/decodal-codemirror": {
|
"../../packages/decodal-codemirror": {
|
||||||
"version": "0.1.5",
|
"version": "0.1.6",
|
||||||
"license": "MIT OR Apache-2.0",
|
"license": "MIT OR Apache-2.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@codemirror/language": "^6.12.4",
|
"@codemirror/language": "^6.12.4",
|
||||||
|
"@codemirror/state": "^6.7.1",
|
||||||
"@codemirror/view": "^6.43.6",
|
"@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"
|
||||||
@@ -41,7 +42,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"../../packages/decodal-wasm": {
|
"../../packages/decodal-wasm": {
|
||||||
"version": "0.1.3",
|
"version": "0.1.4",
|
||||||
"license": "MIT OR Apache-2.0"
|
"license": "MIT OR Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@astrojs/check": {
|
"node_modules/@astrojs/check": {
|
||||||
|
|||||||
@@ -6,13 +6,14 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "astro dev --host 0.0.0.0",
|
"dev": "astro dev --host 0.0.0.0",
|
||||||
"build": "astro build",
|
"build": "astro build",
|
||||||
|
"build:all": "npm run build:wasm && npm run build",
|
||||||
|
"build:wasm": "npm run build:wasm:runtime && npm run build:wasm:formatter",
|
||||||
|
"build:wasm:runtime": "wasm-pack build ../../crates/decodal-wasm --target web --out-dir ../../packages/decodal-wasm --release && node scripts/prepare-wasm-package.mjs",
|
||||||
|
"build:wasm:formatter": "wasm-pack build ../../crates/decodal-language-tools --target web --out-dir ../../packages/decodal-codemirror/wasm --release && node scripts/prepare-codemirror-wasm.mjs",
|
||||||
|
"test": "node --test src/scripts/*.test.mjs",
|
||||||
"preview": "astro preview --host 0.0.0.0",
|
"preview": "astro preview --host 0.0.0.0",
|
||||||
"build:runtime": "wasm-pack build ../../crates/decodal-wasm --target web --out-dir ../../packages/decodal-wasm --release && node scripts/prepare-wasm-package.mjs",
|
|
||||||
"build:tools": "wasm-pack build ../../crates/decodal-language-tools --target web --out-dir ../../packages/decodal-codemirror/wasm --release && node scripts/prepare-codemirror-wasm.mjs",
|
|
||||||
"build:wasm": "npm run build:runtime && npm run build:tools",
|
|
||||||
"build:packages": "npm run build:wasm",
|
|
||||||
"deploy": "npm run build && node scripts/deploy-pages.mjs",
|
"deploy": "npm run build && node scripts/deploy-pages.mjs",
|
||||||
"deploy:wasm": "npm run build:wasm && npm run deploy"
|
"deploy:all": "npm run build:wasm && npm run deploy"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/check": "^0.9.4",
|
"@astrojs/check": "^0.9.4",
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import { rmSync, writeFileSync } from 'node:fs';
|
import { copyFileSync, rmSync, writeFileSync } from 'node:fs';
|
||||||
import { resolve } from 'node:path';
|
import { resolve } from 'node:path';
|
||||||
|
|
||||||
const wasmDir = resolve(import.meta.dirname, '../../../packages/decodal-codemirror/wasm');
|
const wasmDir = resolve(import.meta.dirname, '../../../packages/decodal-codemirror/wasm');
|
||||||
|
const packageDir = resolve(wasmDir, '..');
|
||||||
|
|
||||||
|
copyFileSync(resolve(packageDir, '../../LICENSE-MIT'), resolve(packageDir, 'LICENSE-MIT'));
|
||||||
|
copyFileSync(resolve(packageDir, '../../LICENSE-APACHE'), resolve(packageDir, 'LICENSE-APACHE'));
|
||||||
|
|
||||||
rmSync(resolve(wasmDir, 'README.md'), { force: true });
|
rmSync(resolve(wasmDir, 'README.md'), { force: true });
|
||||||
rmSync(resolve(wasmDir, 'package.json'), { force: true });
|
rmSync(resolve(wasmDir, 'package.json'), { force: true });
|
||||||
|
|||||||
@@ -1,8 +1,78 @@
|
|||||||
import { readFileSync, writeFileSync } from 'node:fs';
|
import { copyFileSync, readFileSync, writeFileSync } from 'node:fs';
|
||||||
import { resolve } from 'node:path';
|
import { resolve } from 'node:path';
|
||||||
|
|
||||||
const packageDir = resolve(import.meta.dirname, '../../../packages/decodal-wasm');
|
const packageDir = resolve(import.meta.dirname, '../../../packages/decodal-wasm');
|
||||||
|
|
||||||
|
copyFileSync(resolve(packageDir, '../../LICENSE-MIT'), resolve(packageDir, 'LICENSE-MIT'));
|
||||||
|
copyFileSync(resolve(packageDir, '../../LICENSE-APACHE'), resolve(packageDir, 'LICENSE-APACHE'));
|
||||||
|
|
||||||
|
const declarationPath = resolve(packageDir, 'decodal_wasm.d.ts');
|
||||||
|
let declarations = readFileSync(declarationPath, 'utf8');
|
||||||
|
const hostTypes = `
|
||||||
|
/** A concrete JavaScript value or Decodal schema descriptor supplied by the host. */
|
||||||
|
export type DecodalHostValue =
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| DecodalHostValue[]
|
||||||
|
| { [field: string]: DecodalHostValue }
|
||||||
|
| DecodalPrimitiveDescriptor
|
||||||
|
| DecodalArrayDescriptor
|
||||||
|
| DecodalAbstractDescriptor;
|
||||||
|
|
||||||
|
export type DecodalPrimitiveType = 'String' | 'Int' | 'Float' | 'Bool';
|
||||||
|
|
||||||
|
export type DecodalConstraint =
|
||||||
|
| { kind: 'type'; value: DecodalPrimitiveType }
|
||||||
|
| { kind: 'compare'; op: '>' | '>=' | '<' | '<='; value: string | number | boolean }
|
||||||
|
| { kind: 'regex'; value: string }
|
||||||
|
| { kind: 'predicate'; value: string };
|
||||||
|
|
||||||
|
export interface DecodalPrimitiveDescriptor {
|
||||||
|
$decodal: DecodalPrimitiveType;
|
||||||
|
constraints?: DecodalConstraint[];
|
||||||
|
default?: DecodalHostValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DecodalArrayDescriptor {
|
||||||
|
$decodal: 'Array';
|
||||||
|
item: DecodalHostValue;
|
||||||
|
constraints?: DecodalConstraint[];
|
||||||
|
default?: DecodalHostValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DecodalAbstractDescriptor {
|
||||||
|
$decodal: 'Abstract';
|
||||||
|
constraints?: DecodalConstraint[];
|
||||||
|
default?: DecodalHostValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type DecodalLoadedImport =
|
||||||
|
| { kind: 'source'; key: string; name?: string; source: string }
|
||||||
|
| { kind: 'value'; key: string; value: DecodalHostValue };
|
||||||
|
|
||||||
|
export type DecodalImportCandidate =
|
||||||
|
| string
|
||||||
|
| { specifier: string; detail?: string };
|
||||||
|
|
||||||
|
/** Host-owned environment shared by evaluation and language tooling. */
|
||||||
|
export interface DecodalEnvironment {
|
||||||
|
globals?: Record<string, DecodalHostValue>;
|
||||||
|
/** Synchronous import callback. Preload or cache asynchronous resources first. */
|
||||||
|
loadImport?: (currentKey: string | null, specifier: string) => DecodalLoadedImport;
|
||||||
|
/** Synchronous import completion callback. */
|
||||||
|
completeImport?: (currentKey: string | null, prefix: string) => DecodalImportCandidate[];
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
if (!declarations.includes('export interface DecodalEnvironment')) {
|
||||||
|
declarations = declarations.replace('/* eslint-disable */\n', `/* eslint-disable */\n${hostTypes}`);
|
||||||
|
}
|
||||||
|
declarations = declarations.replace(
|
||||||
|
'constructor(options: any);',
|
||||||
|
'constructor(options?: DecodalEnvironment);',
|
||||||
|
);
|
||||||
|
writeFileSync(declarationPath, declarations);
|
||||||
|
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
resolve(packageDir, '.gitignore'),
|
resolve(packageDir, '.gitignore'),
|
||||||
'# wasm-pack output is committed for the browser runtime package.\n',
|
'# wasm-pack output is committed for the browser runtime package.\n',
|
||||||
@@ -10,7 +80,23 @@ writeFileSync(
|
|||||||
|
|
||||||
const packageJsonPath = resolve(packageDir, 'package.json');
|
const packageJsonPath = resolve(packageDir, 'package.json');
|
||||||
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
||||||
packageJson.version = '0.1.3';
|
packageJson.version = '0.1.4';
|
||||||
|
packageJson.files = [
|
||||||
|
'README.md',
|
||||||
|
'LICENSE-MIT',
|
||||||
|
'LICENSE-APACHE',
|
||||||
|
'decodal_wasm_bg.wasm',
|
||||||
|
'decodal_wasm.js',
|
||||||
|
'decodal_wasm.d.ts',
|
||||||
|
];
|
||||||
|
packageJson.exports = {
|
||||||
|
'.': {
|
||||||
|
types: './decodal_wasm.d.ts',
|
||||||
|
import: './decodal_wasm.js',
|
||||||
|
},
|
||||||
|
'./decodal_wasm_bg.wasm': './decodal_wasm_bg.wasm',
|
||||||
|
'./package.json': './package.json',
|
||||||
|
};
|
||||||
writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
|
writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
|
||||||
|
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
@@ -19,7 +105,7 @@ writeFileSync(
|
|||||||
|
|
||||||
WebAssembly runtime package for Decodal.
|
WebAssembly runtime package for Decodal.
|
||||||
|
|
||||||
This package exposes the Decodal evaluator to browsers and other JavaScript runtimes that can load WebAssembly modules.
|
This package exposes the Decodal evaluator and shared language service to browsers and other JavaScript runtimes that can load WebAssembly modules.
|
||||||
It is generated from the Rust crate in \`crates/decodal-wasm\` with \`wasm-pack\` and is used by the official playground.
|
It is generated from the Rust crate in \`crates/decodal-wasm\` with \`wasm-pack\` and is used by the official playground.
|
||||||
|
|
||||||
Use \`decodal-codemirror\` separately when you need CodeMirror 6 language support.
|
Use \`decodal-codemirror\` separately when you need CodeMirror 6 language support.
|
||||||
@@ -39,18 +125,51 @@ deno add jsr:@hare/decodal-wasm
|
|||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
\`\`\`js
|
\`\`\`js
|
||||||
import init, { evaluateProject } from 'decodal-wasm';
|
import init, { DecodalLanguageService } from 'decodal-wasm';
|
||||||
|
|
||||||
await init();
|
await init();
|
||||||
|
|
||||||
const result = evaluateProject('main.dcdl', JSON.stringify({
|
const files = {
|
||||||
'main.dcdl': 'Server = { port = Int default 8080; };',
|
'main.dcdl': 'let schema = import "./schema.dcdl"; in schema.Server',
|
||||||
}));
|
'schema.dcdl': 'Server = App.Server & { port = 8080; };',
|
||||||
|
};
|
||||||
|
|
||||||
|
const service = new DecodalLanguageService({
|
||||||
|
globals: {
|
||||||
|
App: {
|
||||||
|
Server: {
|
||||||
|
port: { $decodal: 'Int' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
loadImport(_currentKey, specifier) {
|
||||||
|
const key = specifier.startsWith('./') ? specifier.slice(2) : specifier;
|
||||||
|
return { kind: 'source', key, name: key, source: files[key] };
|
||||||
|
},
|
||||||
|
completeImport() {
|
||||||
|
return ['./schema.dcdl'];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = service.evaluate('main.dcdl', 'main.dcdl', files['main.dcdl']);
|
||||||
|
|
||||||
console.log(JSON.parse(result));
|
console.log(JSON.parse(result));
|
||||||
|
|
||||||
|
const completion = service.complete(
|
||||||
|
'main.dcdl',
|
||||||
|
'App.Server.po',
|
||||||
|
'App.Server.po'.length,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
|
console.log(JSON.parse(completion));
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
The exported functions return JSON strings so callers can handle diagnostics and successful results without depending on Rust data structures.
|
\`globals\`, \`loadImport\`, and \`completeImport\` are host-owned. The package does not assume a filesystem or virtual project model. Import callbacks are synchronous; preload or cache remote content before evaluation.
|
||||||
|
|
||||||
|
Plain strings, numbers, booleans, arrays, and objects are concrete host values. Use \`{ $decodal: 'String' | 'Int' | 'Float' | 'Bool' }\` for primitive schemas and \`{ $decodal: 'Array', item: value }\` for array schemas. Descriptors also accept \`constraints\` and \`default\`.
|
||||||
|
|
||||||
|
Methods return JSON strings so callers can handle diagnostics and successful results without depending on Rust data structures.
|
||||||
`,
|
`,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -59,12 +178,16 @@ writeFileSync(
|
|||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
{
|
{
|
||||||
name: '@hare/decodal-wasm',
|
name: '@hare/decodal-wasm',
|
||||||
version: '0.1.3',
|
version: '0.1.4',
|
||||||
|
// JSR's publisher currently accepts a single recognized identifier.
|
||||||
|
// npm package metadata remains dual-licensed and both files are included.
|
||||||
license: 'MIT',
|
license: 'MIT',
|
||||||
exports: './mod.ts',
|
exports: './mod.ts',
|
||||||
publish: {
|
publish: {
|
||||||
include: [
|
include: [
|
||||||
'README.md',
|
'README.md',
|
||||||
|
'LICENSE-MIT',
|
||||||
|
'LICENSE-APACHE',
|
||||||
'mod.ts',
|
'mod.ts',
|
||||||
'decodal_wasm.js',
|
'decodal_wasm.js',
|
||||||
'decodal_wasm.d.ts',
|
'decodal_wasm.d.ts',
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ Production = Server & {
|
|||||||
<strong>crates.io/crates/decodal</strong>
|
<strong>crates.io/crates/decodal</strong>
|
||||||
</a>
|
</a>
|
||||||
<ul class="package-list">
|
<ul class="package-list">
|
||||||
<li>decodal-wasm on <a href="https://jsr.io/@hare/decodal-wasm@0.1.3">jsr</a> / <a href="https://www.npmjs.com/package/decodal-wasm">npm</a></li>
|
<li>decodal-wasm on <a href="https://jsr.io/@hare/decodal-wasm@0.1.4">jsr</a> / <a href="https://www.npmjs.com/package/decodal-wasm">npm</a></li>
|
||||||
<li>decodal-codemirror on <a href="https://jsr.io/@hare/decodal-codemirror@0.1.5">jsr</a> / <a href="https://www.npmjs.com/package/decodal-codemirror">npm</a></li>
|
<li>decodal-codemirror on <a href="https://jsr.io/@hare/decodal-codemirror@0.1.6">jsr</a> / <a href="https://www.npmjs.com/package/decodal-codemirror">npm</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
const completionTypes = {
|
||||||
|
keyword: 'keyword',
|
||||||
|
constant: 'constant',
|
||||||
|
type: 'type',
|
||||||
|
variable: 'variable',
|
||||||
|
namespace: 'namespace',
|
||||||
|
property: 'property',
|
||||||
|
file: 'text',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function createLanguageServiceCompletionSource({ complete, getActivePath }) {
|
||||||
|
return (context) => {
|
||||||
|
const activePath = getActivePath();
|
||||||
|
let response;
|
||||||
|
try {
|
||||||
|
response = complete({
|
||||||
|
key: activePath,
|
||||||
|
source: context.state.doc.toString(),
|
||||||
|
position: context.pos,
|
||||||
|
explicit: context.explicit,
|
||||||
|
});
|
||||||
|
if (!response) return null;
|
||||||
|
if (typeof response === 'string') response = JSON.parse(response);
|
||||||
|
} catch (_error) {
|
||||||
|
// The editor can become interactive just before WASM initialization
|
||||||
|
// finishes. A later completion request will use the initialized service.
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!response.ok || !response.completion) return null;
|
||||||
|
return completionResultToCodeMirror(response.completion);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function completionResultToCodeMirror(completion) {
|
||||||
|
const fileCompletion = completion.options.some((option) => option.kind === 'file');
|
||||||
|
return {
|
||||||
|
from: completion.from,
|
||||||
|
options: completion.options.map((option) => ({
|
||||||
|
label: option.label,
|
||||||
|
type: completionTypes[option.kind] ?? 'text',
|
||||||
|
detail: option.detail ?? undefined,
|
||||||
|
boost: option.priority ?? 0,
|
||||||
|
})),
|
||||||
|
validFor: fileCompletion ? /^[^"\n]*$/ : /^[A-Za-z_][A-Za-z0-9_]*$/,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
|
||||||
|
import {
|
||||||
|
completionResultToCodeMirror,
|
||||||
|
createLanguageServiceCompletionSource,
|
||||||
|
} from './playground-completion.js';
|
||||||
|
|
||||||
|
test('passes the active document to an injected language service', () => {
|
||||||
|
const calls = [];
|
||||||
|
const source = createLanguageServiceCompletionSource({
|
||||||
|
getActivePath: () => 'main.dcdl',
|
||||||
|
complete(request) {
|
||||||
|
calls.push(request);
|
||||||
|
return JSON.stringify({ ok: true, completion: null });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(source({
|
||||||
|
state: { doc: { toString: () => 'Post.ti' } },
|
||||||
|
pos: 7,
|
||||||
|
explicit: true,
|
||||||
|
}), null);
|
||||||
|
assert.deepEqual(calls, [{
|
||||||
|
key: 'main.dcdl',
|
||||||
|
source: 'Post.ti',
|
||||||
|
position: 7,
|
||||||
|
explicit: true,
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('adapts shared language-service completions for CodeMirror', () => {
|
||||||
|
const result = completionResultToCodeMirror({
|
||||||
|
from: 12,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: 'port',
|
||||||
|
kind: 'property',
|
||||||
|
detail: 'schemas/service.dcdl',
|
||||||
|
priority: 30,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(result.from, 12);
|
||||||
|
assert.deepEqual(result.options, [
|
||||||
|
{
|
||||||
|
label: 'port',
|
||||||
|
type: 'property',
|
||||||
|
detail: 'schemas/service.dcdl',
|
||||||
|
boost: 30,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
assert.ok(result.validFor.test('partial_name'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('uses import-path filtering for file completions', () => {
|
||||||
|
const result = completionResultToCodeMirror({
|
||||||
|
from: 8,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
label: './schemas/service.dcdl',
|
||||||
|
kind: 'file',
|
||||||
|
detail: 'project file',
|
||||||
|
priority: 30,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.ok(result.validFor.test('./schemas/serv'));
|
||||||
|
assert.equal(result.options[0].type, 'text');
|
||||||
|
});
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
export function createPlaygroundEnvironment(getFiles) {
|
||||||
|
return {
|
||||||
|
globals: {},
|
||||||
|
|
||||||
|
loadImport(currentKey, specifier) {
|
||||||
|
const key = resolveImportPath(currentKey, specifier);
|
||||||
|
const source = getFiles()[key];
|
||||||
|
if (source === undefined) {
|
||||||
|
throw new Error(`import ${JSON.stringify(specifier)} resolved to ${JSON.stringify(key)}, but that file does not exist`);
|
||||||
|
}
|
||||||
|
return { kind: 'source', key, name: key, source };
|
||||||
|
},
|
||||||
|
|
||||||
|
completeImport(currentKey, prefix) {
|
||||||
|
return Object.keys(getFiles())
|
||||||
|
.filter((path) => path !== currentKey)
|
||||||
|
.map((path) => ({
|
||||||
|
specifier: prefix.startsWith('/')
|
||||||
|
? `/${path}`
|
||||||
|
: relativeImportPath(currentKey, path),
|
||||||
|
detail: 'project file',
|
||||||
|
}))
|
||||||
|
.filter((candidate) => candidate.specifier.startsWith(prefix))
|
||||||
|
.sort((left, right) => left.specifier.localeCompare(right.specifier));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveImportPath(currentKey, specifier) {
|
||||||
|
if (specifier.startsWith('/')) return normalizeVirtualPath(specifier);
|
||||||
|
const parent = currentKey?.includes('/')
|
||||||
|
? currentKey.slice(0, currentKey.lastIndexOf('/'))
|
||||||
|
: '';
|
||||||
|
return normalizeVirtualPath(parent ? `${parent}/${specifier}` : specifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function relativeImportPath(currentKey, target) {
|
||||||
|
const from = currentKey?.includes('/')
|
||||||
|
? currentKey.slice(0, currentKey.lastIndexOf('/')).split('/').filter(Boolean)
|
||||||
|
: [];
|
||||||
|
const to = target.split('/').filter(Boolean);
|
||||||
|
let common = 0;
|
||||||
|
while (common < from.length && common < to.length && from[common] === to[common]) {
|
||||||
|
common += 1;
|
||||||
|
}
|
||||||
|
const parts = [
|
||||||
|
...Array(from.length - common).fill('..'),
|
||||||
|
...to.slice(common),
|
||||||
|
];
|
||||||
|
const relative = parts.join('/');
|
||||||
|
return relative.startsWith('.') ? relative : `./${relative}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeVirtualPath(path) {
|
||||||
|
const parts = [];
|
||||||
|
for (const part of String(path).replaceAll('\\', '/').split('/')) {
|
||||||
|
if (!part || part === '.') continue;
|
||||||
|
if (part === '..') {
|
||||||
|
if (parts.length === 0) throw new Error(`invalid virtual path ${JSON.stringify(path)}`);
|
||||||
|
parts.pop();
|
||||||
|
} else {
|
||||||
|
parts.push(part);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (parts.length === 0) throw new Error(`invalid virtual path ${JSON.stringify(path)}`);
|
||||||
|
return parts.join('/');
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
|
||||||
|
import {
|
||||||
|
createPlaygroundEnvironment,
|
||||||
|
relativeImportPath,
|
||||||
|
resolveImportPath,
|
||||||
|
} from './playground-environment.js';
|
||||||
|
|
||||||
|
test('resolves virtual imports in the playground host', () => {
|
||||||
|
assert.equal(
|
||||||
|
resolveImportPath('content/pages/home.dcdl', '../schemas/page.dcdl'),
|
||||||
|
'content/schemas/page.dcdl',
|
||||||
|
);
|
||||||
|
assert.equal(
|
||||||
|
relativeImportPath('content/pages/home.dcdl', 'content/schemas/page.dcdl'),
|
||||||
|
'../schemas/page.dcdl',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('loads source and completes paths from playground-owned files', () => {
|
||||||
|
const files = {
|
||||||
|
'main.dcdl': 'import "./schemas/page.dcdl"',
|
||||||
|
'schemas/page.dcdl': 'Page = { title = String; };',
|
||||||
|
};
|
||||||
|
const environment = createPlaygroundEnvironment(() => files);
|
||||||
|
|
||||||
|
assert.deepEqual(environment.loadImport('main.dcdl', './schemas/page.dcdl'), {
|
||||||
|
kind: 'source',
|
||||||
|
key: 'schemas/page.dcdl',
|
||||||
|
name: 'schemas/page.dcdl',
|
||||||
|
source: files['schemas/page.dcdl'],
|
||||||
|
});
|
||||||
|
assert.deepEqual(environment.completeImport('main.dcdl', './sch'), [
|
||||||
|
{ specifier: './schemas/page.dcdl', detail: 'project file' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import test from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
|
||||||
|
import initRuntime, { DecodalLanguageService } from 'decodal-wasm';
|
||||||
|
|
||||||
|
const wasmPath = new URL('../../../../packages/decodal-wasm/decodal_wasm_bg.wasm', import.meta.url);
|
||||||
|
|
||||||
|
test('injects one JavaScript host environment into evaluation and completion', async () => {
|
||||||
|
await initRuntime({ module_or_path: await readFile(wasmPath) });
|
||||||
|
const files = {
|
||||||
|
'schema.dcdl': 'Server = { port = Int; };',
|
||||||
|
};
|
||||||
|
const service = new DecodalLanguageService({
|
||||||
|
globals: {
|
||||||
|
App: {
|
||||||
|
enabled: { $decodal: 'Bool', default: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
loadImport(_currentKey, specifier) {
|
||||||
|
if (specifier === './post.md') {
|
||||||
|
return {
|
||||||
|
kind: 'value',
|
||||||
|
key: 'post.md',
|
||||||
|
value: { frontmatter: { draft: false }, body: '# Hello' },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
kind: 'source',
|
||||||
|
key: 'schema.dcdl',
|
||||||
|
source: files['schema.dcdl'],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
completeImport() {
|
||||||
|
return [{ specifier: './schema.dcdl', detail: 'test source' }];
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const evaluated = JSON.parse(service.evaluate(
|
||||||
|
'main.dcdl',
|
||||||
|
'main.dcdl',
|
||||||
|
'let s = import "./schema.dcdl"; in { server = s.Server & { port = 8080; }; enabled = App.enabled; post = import "./post.md"; }',
|
||||||
|
));
|
||||||
|
assert.equal(evaluated.ok, true, evaluated.error);
|
||||||
|
assert.match(evaluated.output, /"port": 8080/);
|
||||||
|
assert.match(evaluated.output, /"body": "# Hello"/);
|
||||||
|
|
||||||
|
const member = JSON.parse(service.complete('main.dcdl', 'App.en', 6, false));
|
||||||
|
assert.equal(member.ok, true, member.error);
|
||||||
|
assert.ok(member.completion.options.some((item) => item.label === 'enabled'));
|
||||||
|
|
||||||
|
const imported = JSON.parse(service.complete('main.dcdl', 'import "./sch', 13, false));
|
||||||
|
assert.equal(imported.ok, true, imported.error);
|
||||||
|
assert.ok(imported.completion.options.some((item) => item.label === './schema.dcdl'));
|
||||||
|
|
||||||
|
service.free();
|
||||||
|
});
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
import initRuntime, { evaluateProject } from 'decodal-wasm';
|
import initRuntime, { DecodalLanguageService } from 'decodal-wasm';
|
||||||
import runtimeWasmUrl from 'decodal-wasm/decodal_wasm_bg.wasm?url';
|
import runtimeWasmUrl from 'decodal-wasm/decodal_wasm_bg.wasm?url';
|
||||||
import { EditorView, basicSetup } from 'codemirror';
|
import { EditorView, basicSetup } from 'codemirror';
|
||||||
import { keymap } from '@codemirror/view';
|
import { keymap } from '@codemirror/view';
|
||||||
import { decodal } from 'decodal-codemirror';
|
import { decodal, decodalLanguage } from 'decodal-codemirror';
|
||||||
import { formatDecodal, initDecodalFormatter } from 'decodal-codemirror/format';
|
import { formatDecodal, initDecodalFormatter } from 'decodal-codemirror/format';
|
||||||
import toolsWasmUrl from 'decodal-codemirror/wasm/decodal_language_tools_bg.wasm?url';
|
import toolsWasmUrl from 'decodal-codemirror/wasm/decodal_language_tools_bg.wasm?url';
|
||||||
import { playgroundExamples } from './playground-examples.js';
|
import { playgroundExamples } from './playground-examples.js';
|
||||||
|
import { createLanguageServiceCompletionSource } from './playground-completion.js';
|
||||||
|
import { createPlaygroundEnvironment } from './playground-environment.js';
|
||||||
|
|
||||||
const STORAGE_KEY = 'decodal-playground-project-v2';
|
const STORAGE_KEY = 'decodal-playground-project-v2';
|
||||||
const starterProject = playgroundExamples[0];
|
const starterProject = playgroundExamples[0];
|
||||||
@@ -24,6 +26,7 @@ const entrySelect = document.getElementById('entry-select');
|
|||||||
const loadExample = document.getElementById('load-example');
|
const loadExample = document.getElementById('load-example');
|
||||||
|
|
||||||
const project = loadProject();
|
const project = loadProject();
|
||||||
|
let languageService;
|
||||||
|
|
||||||
const editorTheme = EditorView.theme({
|
const editorTheme = EditorView.theme({
|
||||||
'&': {
|
'&': {
|
||||||
@@ -56,14 +59,44 @@ const editorTheme = EditorView.theme({
|
|||||||
'&.cm-focused .cm-selectionBackground, .cm-selectionBackground, ::selection': {
|
'&.cm-focused .cm-selectionBackground, .cm-selectionBackground, ::selection': {
|
||||||
backgroundColor: 'rgb(59 130 246 / 0.35)',
|
backgroundColor: 'rgb(59 130 246 / 0.35)',
|
||||||
},
|
},
|
||||||
|
'.cm-tooltip-autocomplete': {
|
||||||
|
backgroundColor: '#111827',
|
||||||
|
border: '1px solid #334155',
|
||||||
|
borderRadius: '8px',
|
||||||
|
color: '#e5e7eb',
|
||||||
|
overflow: 'hidden',
|
||||||
|
},
|
||||||
|
'.cm-tooltip-autocomplete > ul > li': {
|
||||||
|
padding: '3px 8px',
|
||||||
|
},
|
||||||
|
'.cm-tooltip-autocomplete > ul > li[aria-selected]': {
|
||||||
|
backgroundColor: '#1d4ed8',
|
||||||
|
color: '#fff',
|
||||||
|
},
|
||||||
|
'.cm-completionDetail': {
|
||||||
|
color: '#94a3b8',
|
||||||
|
fontStyle: 'normal',
|
||||||
|
marginLeft: '1.5em',
|
||||||
|
},
|
||||||
}, { dark: true });
|
}, { dark: true });
|
||||||
|
|
||||||
|
const completionSource = createLanguageServiceCompletionSource({
|
||||||
|
getActivePath: () => project.activePath,
|
||||||
|
complete: ({ key, source, position, explicit }) => languageService?.complete(
|
||||||
|
key,
|
||||||
|
source,
|
||||||
|
position,
|
||||||
|
explicit,
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
const editor = new EditorView({
|
const editor = new EditorView({
|
||||||
doc: project.files[project.activePath] ?? '',
|
doc: project.files[project.activePath] ?? '',
|
||||||
parent: editorHost,
|
parent: editorHost,
|
||||||
extensions: [
|
extensions: [
|
||||||
basicSetup,
|
basicSetup,
|
||||||
decodal(),
|
decodal(),
|
||||||
|
decodalLanguage.data.of({ autocomplete: completionSource }),
|
||||||
editorTheme,
|
editorTheme,
|
||||||
keymap.of([
|
keymap.of([
|
||||||
{
|
{
|
||||||
@@ -215,7 +248,11 @@ function execute() {
|
|||||||
project.entryPath = entryPath;
|
project.entryPath = entryPath;
|
||||||
updateRunLabel();
|
updateRunLabel();
|
||||||
saveProject();
|
saveProject();
|
||||||
const result = JSON.parse(evaluateProject(entryPath, JSON.stringify(project.files)));
|
const result = JSON.parse(languageService.evaluate(
|
||||||
|
entryPath,
|
||||||
|
entryPath,
|
||||||
|
project.files[entryPath],
|
||||||
|
));
|
||||||
output.textContent = result.ok ? result.output : result.error;
|
output.textContent = result.ok ? result.output : result.error;
|
||||||
output.classList.toggle('error', !result.ok);
|
output.classList.toggle('error', !result.ok);
|
||||||
}
|
}
|
||||||
@@ -286,9 +323,12 @@ function compareNodes(a, b) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await Promise.all([initRuntime(runtimeWasmUrl), initDecodalFormatter(toolsWasmUrl)]);
|
await Promise.all([initRuntime(runtimeWasmUrl), initDecodalFormatter(toolsWasmUrl)]);
|
||||||
|
languageService = new DecodalLanguageService(
|
||||||
|
createPlaygroundEnvironment(() => project.files),
|
||||||
|
);
|
||||||
run.disabled = false;
|
run.disabled = false;
|
||||||
formatButton.disabled = false;
|
formatButton.disabled = false;
|
||||||
status.textContent = '';
|
status.textContent = 'Ctrl+Space: complete';
|
||||||
execute();
|
execute();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
status.textContent = `Failed to load WASM: ${error?.message ?? error}`;
|
status.textContent = `Failed to load WASM: ${error?.message ?? error}`;
|
||||||
|
|||||||
Reference in New Issue
Block a user