Compare commits

36 Commits
Author SHA1 Message Date
Hare 8198b615a8 Clarify site build and deploy scripts 2026-08-13 19:53:38 +09:00
Hare 62aa81b91c Fix JSR license metadata 2026-08-13 19:45:04 +09:00
Hare 9c46635ee0 Bump Rust crates for breaking release 2026-08-13 19:40:08 +09:00
Hare 515adc2533 Unify formatting on canonical syntax 2026-08-13 19:31:21 +09:00
Hare d9cf24d0eb Prepare Decodal packages for release 2026-08-13 19:01:14 +09:00
Hare d28edcf041 Share host-aware completion across runtimes 2026-08-13 18:55:03 +09:00
Hare e862e53f3e Add project-aware playground completion 2026-08-13 18:07:24 +09:00
Hare 610358110f Add host-configurable LSP server 2026-08-12 05:42:45 +09:00
Hare bdaccd9803 Add host-configurable language service 2026-08-11 22:05:17 +09:00
Hare 08603dc4b5 Report structured import origins in diagnostics 2026-08-11 19:20:11 +09:00
Hare 165daada23 Add host-structured imports 2026-08-11 18:33:49 +09:00
Hare 46b2b3b1b6 Add required array element constraints 2026-08-11 17:24:04 +09:00
Hare 5a05da6fe4 Use non-underscore Astro asset directory 2026-07-10 04:36:22 +09:00
Hare aa0f464320 Bump CodeMirror package to 0.1.5 2026-07-10 04:30:38 +09:00
Hare fc1f3e0b25 Improve JSR package metadata 2026-07-10 04:19:00 +09:00
Hare 8eac5c5cb4 Bundle language tools with CodeMirror package 2026-07-09 21:25:04 +09:00
Hare 59cb8f31e4 Simplify package link labels 2026-07-09 20:50:09 +09:00
Hare 4eee2cd2c9 Make package links flex layout 2026-07-09 20:45:16 +09:00
Hare 44b6041e60 Compact package links on landing page 2026-07-09 20:36:57 +09:00
Hare a95afe0463 Align playground control heights 2026-07-09 19:36:15 +09:00
Hare 43ab6244ef Reduce button vertical padding 2026-07-09 19:34:33 +09:00
Hare 1d4503b015 Simplify playground action separator 2026-07-09 19:33:48 +09:00
Hare 250e29e00c Remove playground helper copy 2026-07-09 19:29:22 +09:00
Hare 1080081cec Reset state before loading playground examples 2026-07-09 19:27:19 +09:00
Hare 43f692e75c Add playground entry selector 2026-07-09 19:21:12 +09:00
Hare 5b26d071ca Run playground examples from entrypoint 2026-07-09 19:12:18 +09:00
Hare a653f0b7c6 Nest repeated playground fields 2026-07-09 19:06:28 +09:00
Hare 384d17b539 Replace playground examples 2026-07-09 19:02:11 +09:00
Hare 09a84264ab Format playground examples 2026-07-09 18:54:29 +09:00
Hare 0a8c261545 Load playground wasm assets through Vite 2026-07-09 18:50:26 +09:00
Hare a8a46998a8 Make build:wasm build all wasm packages 2026-07-09 18:46:24 +09:00
Hare e4a9f80617 Dedupe CodeMirror dependencies 2026-07-09 18:43:09 +09:00
Hare 0e2a7b35d2 Add language tools package 2026-07-09 18:34:41 +09:00
Hare ac99c4cb4e Revert "Add Decodal formatter"
This reverts commit 22f6bc1ab9.
2026-07-09 17:46:24 +09:00
Hare 22f6bc1ab9 Add Decodal formatter 2026-07-09 17:39:32 +09:00
Hare 16006d000a Move wasm package under packages 2026-07-09 04:13:08 +09:00
111 changed files with 13942 additions and 5679 deletions
Generated
+182 -8
View File
@@ -11,6 +11,12 @@ dependencies = [
"memchr",
]
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bumpalo"
version = "3.20.3"
@@ -23,9 +29,24 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "decodal"
version = "0.1.2"
version = "0.2.0"
dependencies = [
"decodal-derive",
"regex",
@@ -33,36 +54,143 @@ dependencies = [
[[package]]
name = "decodal-cli"
version = "0.1.2"
version = "0.2.0"
dependencies = [
"decodal",
]
[[package]]
name = "decodal-derive"
version = "0.1.2"
version = "0.1.3"
dependencies = [
"decodal",
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
]
[[package]]
name = "decodal-wasm"
version = "0.1.2"
name = "decodal-language-service"
version = "0.2.0"
dependencies = [
"decodal",
]
[[package]]
name = "decodal-language-tools"
version = "0.2.0"
dependencies = [
"decodal",
"serde_json",
"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]]
name = "decodal-wasm"
version = "0.2.0"
dependencies = [
"decodal",
"decodal-language-service",
"js-sys",
"serde-wasm-bindgen",
"serde_json",
"wasm-bindgen",
]
[[package]]
name = "fluent-uri"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d"
dependencies = [
"bitflags",
]
[[package]]
name = "futures-core"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e"
[[package]]
name = "futures-task"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cd417de3d1d015fc3bfd2b1ea46dfc7bab72ef86f1cc7cc9c78e728b34a6d1fd"
[[package]]
name = "futures-util"
version = "0.3.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc"
dependencies = [
"futures-core",
"futures-task",
"pin-project-lite",
"slab",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
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]]
name = "memchr"
version = "2.8.2"
@@ -75,6 +203,12 @@ version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "proc-macro2"
version = "1.0.106"
@@ -135,6 +269,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"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]]
@@ -154,7 +300,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
]
[[package]]
@@ -170,6 +316,23 @@ dependencies = [
"zmij",
]
[[package]]
name = "serde_repr"
version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d3b1629de253c70a0508c3899572da79ca359fdab27c7920ff00406df418906"
dependencies = [
"proc-macro2",
"quote",
"syn 3.0.3",
]
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "syn"
version = "2.0.117"
@@ -181,6 +344,17 @@ dependencies = [
"unicode-ident",
]
[[package]]
name = "syn"
version = "3.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
@@ -219,7 +393,7 @@ dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"syn 2.0.117",
"wasm-bindgen-shared",
]
+5 -1
View File
@@ -4,11 +4,15 @@ members = [
"crates/decodal-cli",
"crates/decodal-wasm",
"crates/decodal-derive",
"crates/decodal-language-tools",
"crates/decodal-language-service",
"crates/decodal-lsp",
]
exclude = ["editors/tree-sitter-decodal"]
resolver = "2"
[workspace.package]
version = "0.1.2"
version = "0.2.0"
edition = "2024"
rust-version = "1.85"
license = "MIT OR Apache-2.0"
+25 -2
View File
@@ -4,7 +4,8 @@ Decodal is a small deterministic DSL for describing, composing, validating, and
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
- concrete and abstract values with constraints and defaults
- deterministic expression evaluation
@@ -13,7 +14,7 @@ It is designed around a lightweight Rust library:
## 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
[dependencies]
@@ -64,6 +65,26 @@ Enable optional regex support when needed:
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
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/
```
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
Licensed under either of:
+61
View File
@@ -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.
+1 -1
View File
@@ -20,4 +20,4 @@ default = []
regex = ["decodal/regex"]
[dependencies]
decodal = { version = "0.1.2", path = "../decodal-core" }
decodal = { version = "0.2.0", path = "../decodal-core" }
+5 -5
View File
@@ -5,8 +5,8 @@ use std::{
};
use decodal::{
Data, Diagnostic, DiagnosticKind, Engine, LoadedSource, SourceId, SourceLoader, Span,
format_diagnostic_with,
Data, Diagnostic, DiagnosticKind, Engine, ImportLoader, LoadedImport, LoadedSource, SourceId,
Span, format_diagnostic_with,
};
fn main() -> ExitCode {
@@ -95,12 +95,12 @@ fn read_root_source(path: &str) -> Result<LoadedSource, Diagnostic> {
#[derive(Debug, Clone, Copy)]
struct FsLoader;
impl SourceLoader for FsLoader {
impl ImportLoader for FsLoader {
fn load(
&mut self,
current_key: Option<&str>,
specifier: &str,
) -> Result<LoadedSource, Diagnostic> {
) -> Result<LoadedImport, Diagnostic> {
let path = Path::new(specifier);
let path = if path.is_absolute() {
PathBuf::from(path)
@@ -112,7 +112,7 @@ impl SourceLoader for FsLoader {
} else {
PathBuf::from(path)
};
load_path(&path)
load_path(&path).map(LoadedImport::Source)
}
}
+1 -1
View File
@@ -17,5 +17,5 @@ derive = ["dep:decodal-derive"]
regex = ["std", "dep:regex"]
[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 }
@@ -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(())
}
+10
View File
@@ -50,6 +50,9 @@ pub enum Expr {
Ident(String),
Object(Vec<Field>),
Array(Vec<ExprId>),
ArrayConstraint {
item: ExprId,
},
Let {
bindings: Vec<Field>,
body: ExprId,
@@ -67,6 +70,13 @@ pub enum Expr {
params: Vec<Param>,
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 {
scrutinee: ExprId,
arms: Vec<MatchArm>,
+37 -4
View File
@@ -11,6 +11,7 @@ pub fn normalize_constraints(
span: Span,
) -> crate::Result<Vec<ConstraintEntry>> {
let mut primitive: Option<(PrimitiveType, Span)> = None;
let mut array_span: Option<Span> = None;
let mut lower: Option<(Bound, Span)> = None;
let mut upper: Option<(Bound, Span)> = None;
let mut rest = Vec::new();
@@ -61,6 +62,13 @@ pub fn normalize_constraints(
constraint: Constraint::Regex(pattern),
span: entry.span,
}),
Constraint::ArrayItems(item) => {
array_span.get_or_insert(entry.span);
rest.push(ConstraintEntry {
constraint: Constraint::ArrayItems(item),
span: entry.span,
});
}
Constraint::BuiltinPredicate(name) => rest.push(ConstraintEntry {
constraint: Constraint::BuiltinPredicate(name),
span: entry.span,
@@ -68,12 +76,37 @@ pub fn normalize_constraints(
}
}
if let (Some((_, primitive_span)), Some(array_span)) = (primitive, array_span) {
return Err(Diagnostic::new(
DiagnosticKind::Conflict,
span,
"array element constraints conflict with primitive type constraints",
)
.with_label(primitive_span, "primitive type constraint")
.with_label(array_span, "array element constraint"));
}
if let Some(array_span) = array_span
&& (lower.is_some() || upper.is_some())
{
let mut diagnostic = Diagnostic::new(
DiagnosticKind::Conflict,
span,
"numeric comparison constraints conflict with array element constraints",
)
.with_label(array_span, "array element constraint");
if let Some((_, lower_span)) = lower {
diagnostic = diagnostic.with_label(lower_span, "numeric comparison constraint");
}
if let Some((_, upper_span)) = upper {
diagnostic = diagnostic.with_label(upper_span, "numeric comparison constraint");
}
return Err(diagnostic);
}
if matches!(
primitive,
Some((
PrimitiveType::String | PrimitiveType::Bool | PrimitiveType::Array,
_
))
Some((PrimitiveType::String | PrimitiveType::Bool, _))
) && (lower.is_some() || upper.is_some())
{
let mut diagnostic = Diagnostic::new(
+7
View File
@@ -10,6 +10,7 @@ pub struct Diagnostic {
pub span: Span,
pub message: String,
pub labels: Vec<DiagnosticLabel>,
pub notes: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
@@ -25,6 +26,7 @@ impl Diagnostic {
span,
message: message.into(),
labels: Vec::new(),
notes: Vec::new(),
}
}
@@ -39,6 +41,11 @@ impl Diagnostic {
});
self
}
pub fn with_note(mut self, message: impl Into<String>) -> Self {
self.notes.push(message.into());
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+28 -2
View File
@@ -10,6 +10,11 @@ pub enum HostValue {
Float(f64),
Bool(bool),
Array(Vec<HostValue>),
ArrayConstraint {
item: Box<HostValue>,
constraints: Vec<Constraint>,
default: Option<Box<HostValue>>,
},
Object(Vec<HostField>),
Abstract {
constraints: Vec<Constraint>,
@@ -91,8 +96,12 @@ impl HostValue {
Self::abstract_with_constraint(Constraint::Type(PrimitiveType::Bool))
}
pub fn array_type() -> Self {
Self::abstract_with_constraint(Constraint::Type(PrimitiveType::Array))
pub fn array_of(item: HostValue) -> Self {
Self::ArrayConstraint {
item: Box::new(item),
constraints: Vec::new(),
default: None,
}
}
pub fn builtin_predicate(name: impl Into<String>) -> Self {
@@ -109,6 +118,7 @@ impl HostValue {
pub fn with_constraint(mut self, constraint: Constraint) -> Self {
match &mut self {
Self::Abstract { constraints, .. } => constraints.push(constraint),
Self::ArrayConstraint { constraints, .. } => constraints.push(constraint),
_ => {
self = Self::Abstract {
constraints: alloc::vec![constraint],
@@ -143,6 +153,22 @@ impl HostValue {
pub fn default(self, value: HostValue) -> Result<Self> {
match self {
Self::ArrayConstraint {
item,
constraints,
default: None,
} => Ok(Self::ArrayConstraint {
item,
constraints,
default: Some(Box::new(value)),
}),
Self::ArrayConstraint {
default: Some(_), ..
} => Err(Diagnostic::new(
DiagnosticKind::DefaultConflict,
Span::default(),
"host value already has a default",
)),
Self::Abstract {
constraints,
default: None,
+34
View File
@@ -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)
}
}
File diff suppressed because it is too large Load Diff
+97 -21
View File
@@ -32,6 +32,7 @@ pub enum TokenKind {
Semicolon,
Comma,
Dot,
Ellipsis,
Colon,
Equal,
EqualEqual,
@@ -51,9 +52,25 @@ pub enum TokenKind {
Gte,
Lt,
Lte,
/// A line comment, including its leading `#` and excluding its newline.
Comment,
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> {
source_id: SourceId,
source: &'a str,
@@ -77,10 +94,26 @@ impl<'a> Lexer<'a> {
}
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 previous = None;
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;
if !is_eof {
previous = Some(token.kind.clone());
@@ -92,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> {
let start = self.pos;
let Some(ch) = self.peek() else {
@@ -141,7 +169,18 @@ impl<'a> Lexer<'a> {
}
b'.' => {
self.pos += 1;
TokenKind::Dot
if self.consume(b'.') {
if self.consume(b'.') {
TokenKind::Ellipsis
} else {
return Err(Diagnostic::syntax(
self.span(start, self.pos),
"expected third '.' in ellipsis",
));
}
} else {
TokenKind::Dot
}
}
b':' => {
self.pos += 1;
@@ -247,21 +286,23 @@ impl<'a> Lexer<'a> {
})
}
fn skip_ws_and_comments(&mut self) {
loop {
while matches!(self.peek(), Some(b' ' | b'\t' | b'\r' | b'\n')) {
self.pos += 1;
fn skip_whitespace(&mut self) {
while matches!(self.peek(), Some(b' ' | b'\t' | b'\r' | b'\n')) {
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'#') {
while let Some(c) = self.peek() {
self.pos += 1;
if c == b'\n' {
break;
}
}
continue;
}
break;
self.pos += 1;
}
Token {
kind: TokenKind::Comment,
span: self.span(start, self.pos),
}
}
@@ -432,4 +473,39 @@ mod tests {
assert_eq!(tokens[3].kind, TokenKind::Amp);
assert_eq!(tokens[4].kind, TokenKind::Gte);
}
#[test]
fn tokenizes_array_constraint_ellipsis() {
let tokens = Lexer::new("[...String]").tokenize().unwrap();
assert_eq!(tokens[0].kind, TokenKind::LBracket);
assert_eq!(tokens[1].kind, TokenKind::Ellipsis);
assert!(matches!(tokens[2].kind, TokenKind::Ident(_)));
assert_eq!(tokens[3].kind, TokenKind::RBracket);
}
#[test]
fn tooling_tokens_retain_comments_and_whitespace_gaps() {
let source = "value = 1; # trailing\n\n# leading\nnext = 2;";
let tokens = tokenize_source(source).unwrap();
let comments = tokens
.iter()
.filter(|token| token.kind == TokenKind::Comment)
.collect::<Vec<_>>();
assert_eq!(comments.len(), 2);
assert_eq!(
&source[comments[0].span.start as usize..comments[0].span.end as usize],
"# trailing"
);
assert!(
source[comments[0].span.end as usize..comments[1].span.start as usize].contains("\n\n")
);
let source_id = SourceId(7);
let identified = tokenize_source_with_source_id(source_id, "# comment").unwrap();
assert!(
identified
.iter()
.all(|token| token.span.source == source_id)
);
}
}
+9 -1
View File
@@ -6,6 +6,7 @@ pub mod ast;
pub mod constraints;
pub mod diagnostic;
pub mod embedding;
pub mod environment;
pub mod eval;
mod lexer;
pub mod module;
@@ -20,8 +21,15 @@ pub use constraints::normalize_constraints;
pub use decodal_derive::Decodal;
pub use diagnostic::{Diagnostic, DiagnosticKind, Result};
pub use embedding::{HostField, HostValue};
pub use environment::HostEnvironment;
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 runtime::{Constraint, Data, ExprRef, LiteralValue, ModuleId, PrimitiveType, RuntimeValue};
pub use span::{SourceId, Span};
+73 -6
View File
@@ -1,7 +1,7 @@
use alloc::string::String;
use alloc::{string::String, vec::Vec};
use crate::{
Ast, ExprId, SourceForm, SourceId,
Ast, ExprId, HostValue, SourceForm, SourceId,
runtime::{EnvId, ThunkId},
};
@@ -24,15 +24,82 @@ pub struct LoadedSource {
pub source: String,
}
pub trait SourceLoader {
fn load(&mut self, current_key: Option<&str>, specifier: &str) -> crate::Result<LoadedSource>;
#[derive(Debug, Clone)]
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)]
pub struct EmptyLoader;
impl SourceLoader for EmptyLoader {
fn load(&mut self, _current_key: Option<&str>, specifier: &str) -> crate::Result<LoadedSource> {
impl ImportLoader for EmptyLoader {
fn load(&mut self, _current_key: Option<&str>, specifier: &str) -> crate::Result<LoadedImport> {
Err(crate::Diagnostic::new(
crate::DiagnosticKind::Import,
crate::Span::default(),
+49 -3
View File
@@ -38,7 +38,10 @@ pub struct Parser {
impl Parser {
pub fn new(tokens: Vec<Token>) -> Self {
Self {
tokens,
tokens: tokens
.into_iter()
.filter(|token| token.kind != TokenKind::Comment)
.collect(),
pos: 0,
ast: Ast::new(),
}
@@ -338,6 +341,17 @@ impl Parser {
}
fn parse_array_after_lbracket(&mut self, start_span: Span) -> Result<ExprId> {
if self.consume_kind(&TokenKind::Ellipsis).is_some() {
let item = self.parse_expr(0)?;
let _ = self.consume_kind(&TokenKind::Comma);
self.expect_kind(
&TokenKind::RBracket,
"expected ']' after array element constraint",
)?;
let span = start_span.join(self.previous_span());
return Ok(self.ast.push(Expr::ArrayConstraint { item }, span));
}
let mut items = Vec::new();
if self.consume_kind(&TokenKind::RBracket).is_some() {
return Ok(self
@@ -369,8 +383,9 @@ impl Parser {
}
let expr = self.parse_expr(0)?;
self.expect_kind(&TokenKind::RParen, "expected ')' after expression")?;
Ok(expr)
let end_span = self.expect_kind(&TokenKind::RParen, "expected ')' after expression")?;
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>> {
@@ -674,4 +689,35 @@ mod tests {
};
assert_eq!(fields[0].path, ["feature", "enable"]);
}
#[test]
fn parses_array_constraint() {
let parsed = parse_source("[...(String & /^api-/)]").unwrap();
let Expr::ArrayConstraint { item } = parsed.ast.get(parsed.root).expr else {
panic!()
};
let Expr::Parenthesized { expr } = parsed.ast.get(item).expr else {
panic!()
};
assert!(matches!(parsed.ast.get(expr).expr, Expr::Binary { .. }));
assert_eq!(parsed.ast.span(item), Span::new(SourceId(0), 4, 22));
}
#[test]
fn parser_accepts_the_public_lossless_token_stream() {
let tokens = crate::tokenize_source("value = (# note\n 1);").unwrap();
let parsed = Parser::new(tokens).parse().unwrap();
let Expr::Object(fields) = &parsed.ast.get(parsed.root).expr else {
panic!()
};
assert!(matches!(
parsed.ast.get(fields[0].value).expr,
Expr::Parenthesized { .. }
));
}
#[test]
fn rejects_array_constraint_without_element_constraint() {
assert!(parse_source("[...]").is_err());
}
}
+1 -1
View File
@@ -68,6 +68,7 @@ pub struct ConstraintEntry {
#[derive(Debug, Clone, PartialEq)]
pub enum Constraint {
Type(PrimitiveType),
ArrayItems(ThunkId),
Compare(CompareOp, LiteralValue),
Regex(String),
BuiltinPredicate(String),
@@ -79,7 +80,6 @@ pub enum PrimitiveType {
Int,
Float,
Bool,
Array,
}
#[derive(Debug, Clone, PartialEq)]
+2 -2
View File
@@ -198,9 +198,9 @@ impl IntoHostValue for bool {
}
}
impl<T> DecodalSchema for Vec<T> {
impl<T: DecodalSchema> DecodalSchema for Vec<T> {
fn decodal_schema() -> HostValue {
HostValue::array_type()
HostValue::array_of(T::decodal_schema())
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "decodal-derive"
version.workspace = true
version = "0.1.3"
edition.workspace = true
rust-version.workspace = true
license.workspace = true
@@ -19,4 +19,4 @@ quote = "1"
syn = { version = "2", features = ["full"] }
[dev-dependencies]
decodal = { version = "0.1", path = "../decodal-core" }
decodal = { version = "0.2.0", path = "../decodal-core" }
+71
View File
@@ -12,6 +12,18 @@ struct Service {
tags: Vec<String>,
}
#[derive(Debug, PartialEq, Decodal)]
struct Worker {
name: String,
#[decodal(default = true)]
enabled: bool,
}
#[derive(Debug, PartialEq, Decodal)]
struct Fleet {
workers: Vec<Worker>,
}
#[test]
fn derives_schema_and_decode() {
let mut engine = Engine::new(EmptyLoader);
@@ -77,3 +89,62 @@ fn decode_reports_field_path() {
let error = Service::decodal_decode(&data).unwrap_err();
assert_eq!(error.path, "name");
}
#[test]
fn vec_schema_rejects_invalid_element_before_decode() {
let mut engine = Engine::new(EmptyLoader);
engine
.bind_global("Service", Service::decodal_schema())
.unwrap();
let module = engine
.add_root_source(
"test",
"test",
r#"
Service & {
name = "api";
port = 9443;
tags = ["web", 1];
}
"#,
)
.unwrap();
let error = engine.eval_module(module).unwrap_err();
assert!(
error
.labels
.iter()
.any(|label| label.message.contains("array element [1]"))
);
}
#[test]
fn vec_schema_applies_nested_struct_defaults() {
let mut engine = Engine::new(EmptyLoader);
engine
.bind_global("Fleet", Fleet::decodal_schema())
.unwrap();
let module = engine
.add_root_source(
"test",
"test",
r#"
Fleet & {
workers = [{ name = "api"; }];
}
"#,
)
.unwrap();
let value = engine.eval_module(module).unwrap();
let data = engine.materialize(&value).unwrap();
let fleet = Fleet::decodal_decode(&data).unwrap();
assert_eq!(
fleet,
Fleet {
workers: vec![Worker {
name: "api".into(),
enabled: true,
}],
}
);
}
@@ -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("/")
}
}
+204
View File
@@ -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"]
);
}
}
+22
View File
@@ -0,0 +1,22 @@
[package]
name = "decodal-language-tools"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
readme.workspace = true
description = "Source-level language tooling for Decodal."
keywords = ["decodal", "formatter", "lsp", "language-tools"]
categories = ["development-tools", "text-processing"]
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
decodal = { version = "0.2.0", path = "../decodal-core" }
serde_json.workspace = true
wasm-bindgen.workspace = true
[package.metadata.wasm-pack.profile.release]
wasm-opt = false
+759
View File
@@ -0,0 +1,759 @@
use std::{error::Error, fmt};
use decodal::{
Ast, BinaryOp, CompareOp, Expr, ExprId, Field, Param, SourceForm, Span, SyntaxToken,
SyntaxTokenKind,
ast::{MatchArm, UnaryOp},
parse_source, tokenize_source,
};
use wasm_bindgen::prelude::*;
const INDENT: usize = 4;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FormatError {
message: String,
}
impl FormatError {
fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
}
}
pub fn message(&self) -> &str {
&self.message
}
}
impl fmt::Display for FormatError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.message)
}
}
impl Error for FormatError {}
/// Formats a complete Decodal source with the canonical parser and syntax tokens.
///
/// Native and WebAssembly callers execute this exact implementation. Comments
/// are recovered from the lossless token stream while expression structure and
/// precedence come from the same AST used by evaluation.
pub fn format_source(source: &str) -> Result<String, FormatError> {
let parsed = parse_source(source)
.map_err(|diagnostic| FormatError::new(format!("parse error: {}", diagnostic.message)))?;
let tokens = tokenize_source(source)
.map_err(|diagnostic| FormatError::new(format!("lex error: {}", diagnostic.message)))?;
Ok(Formatter::new(source, &parsed.ast, tokens).format(parsed.root, parsed.source_form))
}
#[wasm_bindgen(js_name = formatSource)]
pub fn format_source_json(source: &str) -> String {
match format_source(source) {
Ok(formatted) => serde_json::json!({ "ok": true, "source": formatted }).to_string(),
Err(error) => serde_json::json!({ "ok": false, "error": error.message() }).to_string(),
}
}
struct Formatter<'a> {
source: &'a str,
ast: &'a Ast,
tokens: Vec<SyntaxToken>,
comments: Vec<Span>,
}
impl<'a> Formatter<'a> {
fn new(source: &'a str, ast: &'a Ast, tokens: Vec<SyntaxToken>) -> Self {
let comments = tokens
.iter()
.filter(|token| token.kind == SyntaxTokenKind::Comment)
.map(|token| token.span)
.collect();
Self {
source,
ast,
tokens,
comments,
}
}
fn format(&self, root: ExprId, source_form: SourceForm) -> String {
let mut out = String::new();
if source_form == SourceForm::Fields {
let Expr::Object(fields) = &self.ast.get(root).expr else {
unreachable!("field-form sources parse to an object")
};
self.write_field_list(&mut out, fields, 0, self.source.len(), 0);
} else {
let span = self.ast.span(root);
self.write_between(&mut out, 0, span.start as usize, None, 0);
self.write_expr(&mut out, root, 0, 0);
self.write_between(
&mut out,
span.end as usize,
self.source.len(),
Some(span.end as usize),
0,
);
}
trim_trailing_whitespace(&mut out);
out.push('\n');
out
}
fn write_field_list(
&self,
out: &mut String,
fields: &[Field],
start: usize,
end: usize,
indent: usize,
) {
let mut cursor = start;
let mut previous_end = None;
for field in fields {
let field_start = field.span.start as usize;
self.write_between(out, cursor, field_start, previous_end, indent);
write_indent(out, indent);
self.write_field(out, field, indent);
out.push(';');
cursor = field.span.end as usize;
previous_end = Some(cursor);
}
self.write_between(out, cursor, end, previous_end, indent);
}
fn write_field(&self, out: &mut String, field: &Field, indent: usize) {
let value_start = self.ast.span(field.value).start as usize;
if self.has_comment_between(field.span.start as usize, value_start) {
out.push_str(self.raw(field.span));
return;
}
for (index, part) in field.path.iter().enumerate() {
if index > 0 {
out.push('.');
}
out.push_str(part);
}
out.push_str(" = ");
self.write_expr(out, field.value, indent, 0);
}
fn write_expr(&self, out: &mut String, id: ExprId, indent: usize, parent_prec: u8) {
let node = self.ast.get(id);
if self.has_comment(node.span)
&& !matches!(
node.expr,
Expr::Object(_) | Expr::Array(_) | Expr::Let { .. } | Expr::Match { .. }
)
{
out.push_str(self.raw(node.span));
return;
}
let precedence = self.precedence(id);
let parenthesize = precedence < parent_prec;
if parenthesize {
out.push('(');
}
match &node.expr {
Expr::Literal(_) | Expr::Ident(_) | Expr::RegexConstraint(_) | Expr::Wildcard => {
out.push_str(self.raw(node.span));
}
Expr::Object(fields) => self.write_object(out, node.span, fields, indent),
Expr::Array(items) => self.write_array(out, node.span, items, indent),
Expr::ArrayConstraint { item } => {
out.push_str("[...");
self.write_expr(out, *item, indent, 0);
out.push(']');
}
Expr::Let { bindings, body } => self.write_let(out, node.span, bindings, *body, indent),
Expr::Import(_) => {
out.push_str("import ");
let raw = self.raw(node.span);
out.push_str(raw.strip_prefix("import").unwrap_or(raw).trim());
}
Expr::Path { base, field } => {
self.write_expr(out, *base, indent, precedence);
out.push('.');
out.push_str(field);
}
Expr::Call { callee, args } => {
self.write_expr(out, *callee, indent, precedence);
out.push('(');
for (index, argument) in args.iter().enumerate() {
if index > 0 {
out.push_str(", ");
}
self.write_expr(out, *argument, indent, 0);
}
out.push(')');
}
Expr::Function { params, body } => self.write_function(out, params, *body, indent),
Expr::Parenthesized { expr } => {
out.push('(');
self.write_expr(out, *expr, indent, 0);
out.push(')');
}
Expr::Match { scrutinee, arms } => {
self.write_match(out, node.span, *scrutinee, arms, indent)
}
Expr::Unary { op, expr } => {
out.push_str(match op {
UnaryOp::Neg => "-",
UnaryOp::Not => "!",
});
self.write_expr(out, *expr, indent, precedence);
}
Expr::Binary { op, lhs, rhs } => {
self.write_expr(out, *lhs, indent, precedence);
out.push(' ');
out.push_str(binary_operator(*op));
out.push(' ');
self.write_expr(out, *rhs, indent, precedence + 1);
}
Expr::Default { base, fallback } => {
self.write_expr(out, *base, indent, precedence);
out.push_str(" default ");
self.write_expr(out, *fallback, indent, precedence + 1);
}
Expr::CompareConstraint { op, value } => {
out.push_str(compare_operator(*op));
out.push(' ');
self.write_expr(out, *value, indent, precedence);
}
}
if parenthesize {
out.push(')');
}
}
fn write_object(&self, out: &mut String, span: Span, fields: &[Field], indent: usize) {
let (start, end) =
self.delimited_range(span, SyntaxTokenKind::LBrace, SyntaxTokenKind::RBrace);
if fields.is_empty() && !self.has_comment_between(start, end) {
out.push_str("{}");
return;
}
out.push_str("{\n");
self.write_field_list(out, fields, start, end, indent + INDENT);
write_indent(out, indent);
out.push('}');
}
fn write_array(&self, out: &mut String, span: Span, items: &[ExprId], indent: usize) {
let (start, end) =
self.delimited_range(span, SyntaxTokenKind::LBracket, SyntaxTokenKind::RBracket);
if items.is_empty() && !self.has_comment_between(start, end) {
out.push_str("[]");
return;
}
let inline = !self.has_comment_between(start, end)
&& items.iter().all(|item| self.is_inline_expr(*item));
if inline {
out.push('[');
for (index, item) in items.iter().enumerate() {
if index > 0 {
out.push_str(", ");
}
self.write_expr(out, *item, indent, 0);
}
out.push(']');
return;
}
out.push_str("[\n");
let mut cursor = start;
let mut previous_end = None;
for item in items {
let item_span = self.ast.span(*item);
self.write_between(
out,
cursor,
item_span.start as usize,
previous_end,
indent + INDENT,
);
write_indent(out, indent + INDENT);
self.write_expr(out, *item, indent + INDENT, 0);
out.push(',');
cursor = item_span.end as usize;
previous_end = Some(cursor);
}
self.write_between(out, cursor, end, previous_end, indent + INDENT);
write_indent(out, indent);
out.push(']');
}
fn write_let(
&self,
out: &mut String,
span: Span,
bindings: &[Field],
body: ExprId,
indent: usize,
) {
let body_span = self.ast.span(body);
let in_token = self
.tokens_between(span.start as usize, body_span.start as usize)
.rev()
.find(|token| token.kind == SyntaxTokenKind::In)
.expect("parsed let expressions contain `in`");
let let_token = self
.tokens_between(span.start as usize, span.end as usize)
.find(|token| token.kind == SyntaxTokenKind::Let)
.expect("parsed let expressions contain `let`");
out.push_str("let\n");
self.write_field_list(
out,
bindings,
let_token.span.end as usize,
in_token.span.start as usize,
indent + INDENT,
);
write_indent(out, indent);
out.push_str("in");
self.write_between(
out,
in_token.span.end as usize,
body_span.start as usize,
Some(in_token.span.end as usize),
indent + INDENT,
);
write_indent(out, indent + INDENT);
self.write_expr(out, body, indent + INDENT, 0);
}
fn write_function(&self, out: &mut String, params: &[Param], body: ExprId, indent: usize) {
out.push('(');
for (index, parameter) in params.iter().enumerate() {
if index > 0 {
out.push_str(", ");
}
out.push_str(&parameter.name);
if let Some(constraint) = parameter.constraint {
out.push_str(": ");
self.write_expr(out, constraint, indent, 0);
}
}
out.push_str(") =>");
if self.is_inline_expr(body) {
out.push(' ');
self.write_expr(out, body, indent, 0);
} else {
out.push('\n');
write_indent(out, indent + INDENT);
self.write_expr(out, body, indent + INDENT, 0);
}
}
fn write_match(
&self,
out: &mut String,
span: Span,
scrutinee: ExprId,
arms: &[MatchArm],
indent: usize,
) {
out.push_str("match ");
self.write_expr(out, scrutinee, indent, 0);
let scrutinee_end = self.ast.span(scrutinee).end as usize;
let open = self
.tokens_between(scrutinee_end, span.end as usize)
.find(|token| token.kind == SyntaxTokenKind::LBrace)
.expect("parsed match expressions contain `{`");
let close = self
.tokens_between(open.span.end as usize, span.end as usize)
.rev()
.find(|token| token.kind == SyntaxTokenKind::RBrace)
.expect("parsed match expressions contain `}`");
let start = open.span.end as usize;
let end = close.span.start as usize;
if self.has_comment_between(scrutinee_end, open.span.start as usize) {
self.write_between(
out,
scrutinee_end,
open.span.start as usize,
Some(scrutinee_end),
indent,
);
write_indent(out, indent);
out.push('{');
} else {
out.push_str(" {");
}
if arms.is_empty() && !self.has_comment_between(start, end) {
out.push('}');
return;
}
out.push('\n');
let mut cursor = start;
let mut previous_end = None;
for arm in arms {
self.write_between(
out,
cursor,
arm.span.start as usize,
previous_end,
indent + INDENT,
);
write_indent(out, indent + INDENT);
if self.has_comment_between(
self.ast.span(arm.pattern).end as usize,
self.ast.span(arm.body).start as usize,
) {
out.push_str(self.raw(arm.span));
} else {
self.write_expr(out, arm.pattern, indent + INDENT, 0);
out.push_str(": ");
self.write_expr(out, arm.body, indent + INDENT, 0);
}
out.push(';');
cursor = arm.span.end as usize;
previous_end = Some(cursor);
}
self.write_between(out, cursor, end, previous_end, indent + INDENT);
write_indent(out, indent);
out.push('}');
}
fn write_between(
&self,
out: &mut String,
start: usize,
end: usize,
previous_end: Option<usize>,
indent: usize,
) {
let mut cursor = start.min(end);
for comment in self.comments_between(start, end) {
let comment_start = comment.start as usize;
let trailing = previous_end.is_some_and(|previous| {
!out.ends_with('\n') && self.same_line(previous, comment_start)
});
if trailing {
out.push(' ');
out.push_str(self.raw(*comment));
out.push('\n');
} else {
ensure_line_break(out);
if self.has_blank_line(cursor, comment_start) {
ensure_blank_line(out);
}
write_indent(out, indent);
out.push_str(self.raw(*comment));
out.push('\n');
}
cursor = comment.end as usize;
}
if previous_end.is_some() {
ensure_line_break(out);
}
if self.has_blank_line(cursor, end) {
ensure_blank_line(out);
}
}
fn precedence(&self, id: ExprId) -> u8 {
match &self.ast.get(id).expr {
Expr::Default { .. } => 1,
Expr::Binary { op, .. } => match op {
BinaryOp::Patch => 3,
BinaryOp::And => 5,
BinaryOp::LogicalOr => 7,
BinaryOp::LogicalAnd => 9,
BinaryOp::Equal
| BinaryOp::NotEqual
| BinaryOp::Greater
| BinaryOp::GreaterEqual
| BinaryOp::Less
| BinaryOp::LessEqual => 11,
BinaryOp::Concat => 12,
BinaryOp::Add | BinaryOp::Sub => 13,
BinaryOp::Mul | BinaryOp::Div => 15,
},
Expr::Unary { .. } | Expr::CompareConstraint { .. } => 17,
Expr::Call { .. } | Expr::Path { .. } => 19,
Expr::Parenthesized { .. } => 20,
_ => 20,
}
}
fn is_inline_expr(&self, id: ExprId) -> bool {
let node = self.ast.get(id);
if self.has_comment(node.span) {
return false;
}
match &node.expr {
Expr::Literal(_)
| Expr::Ident(_)
| Expr::RegexConstraint(_)
| Expr::Wildcard
| Expr::Import(_)
| Expr::CompareConstraint { .. } => true,
Expr::Path { base, .. } => self.is_inline_expr(*base),
Expr::Call { callee, args } => {
self.is_inline_expr(*callee)
&& args.iter().all(|argument| self.is_inline_expr(*argument))
}
Expr::Unary { expr, .. } => self.is_inline_expr(*expr),
Expr::Binary { lhs, rhs, .. } => self.is_inline_expr(*lhs) && self.is_inline_expr(*rhs),
Expr::Default { base, fallback } => {
self.is_inline_expr(*base) && self.is_inline_expr(*fallback)
}
Expr::Array(items) => items.iter().all(|item| self.is_inline_expr(*item)),
Expr::ArrayConstraint { item } => self.is_inline_expr(*item),
Expr::Parenthesized { expr } => self.is_inline_expr(*expr),
Expr::Object(_) | Expr::Let { .. } | Expr::Function { .. } | Expr::Match { .. } => {
false
}
}
}
fn delimited_range(
&self,
span: Span,
open_kind: SyntaxTokenKind,
close_kind: SyntaxTokenKind,
) -> (usize, usize) {
let open = self
.tokens_between(span.start as usize, span.end as usize)
.find(|token| same_kind(&token.kind, &open_kind))
.expect("parsed delimited expressions contain an opening token");
let close = self
.tokens_between(open.span.end as usize, span.end as usize)
.rev()
.find(|token| same_kind(&token.kind, &close_kind))
.expect("parsed delimited expressions contain a closing token");
(open.span.end as usize, close.span.start as usize)
}
fn tokens_between(
&self,
start: usize,
end: usize,
) -> impl DoubleEndedIterator<Item = &SyntaxToken> {
self.tokens.iter().filter(move |token| {
token.span.start as usize >= start && token.span.end as usize <= end
})
}
fn comments_between(&self, start: usize, end: usize) -> impl Iterator<Item = &Span> {
self.comments
.iter()
.filter(move |comment| comment.start as usize >= start && comment.end as usize <= end)
}
fn has_comment(&self, span: Span) -> bool {
self.has_comment_between(span.start as usize, span.end as usize)
}
fn has_comment_between(&self, start: usize, end: usize) -> bool {
self.comments_between(start, end).next().is_some()
}
fn same_line(&self, start: usize, end: usize) -> bool {
!self.slice(start, end).contains('\n')
}
fn has_blank_line(&self, start: usize, end: usize) -> bool {
self.slice(start, end)
.bytes()
.filter(|byte| *byte == b'\n')
.count()
>= 2
}
fn raw(&self, span: Span) -> &'a str {
self.slice(span.start as usize, span.end as usize).trim()
}
fn slice(&self, start: usize, end: usize) -> &'a str {
&self.source[start.min(self.source.len())..end.min(self.source.len())]
}
}
fn binary_operator(operator: BinaryOp) -> &'static str {
match operator {
BinaryOp::Add => "+",
BinaryOp::Sub => "-",
BinaryOp::Mul => "*",
BinaryOp::Div => "/",
BinaryOp::Concat => "++",
BinaryOp::Equal => "==",
BinaryOp::NotEqual => "!=",
BinaryOp::Greater => ">",
BinaryOp::GreaterEqual => ">=",
BinaryOp::Less => "<",
BinaryOp::LessEqual => "<=",
BinaryOp::LogicalAnd => "&&",
BinaryOp::LogicalOr => "||",
BinaryOp::And => "&",
BinaryOp::Patch => "//",
}
}
fn compare_operator(operator: CompareOp) -> &'static str {
match operator {
CompareOp::Gt => ">",
CompareOp::Gte => ">=",
CompareOp::Lt => "<",
CompareOp::Lte => "<=",
CompareOp::Eq => "==",
}
}
fn same_kind(left: &SyntaxTokenKind, right: &SyntaxTokenKind) -> bool {
core::mem::discriminant(left) == core::mem::discriminant(right)
}
fn write_indent(out: &mut String, indent: usize) {
out.extend(core::iter::repeat_n(' ', indent));
}
fn ensure_line_break(out: &mut String) {
if !out.is_empty() && !out.ends_with('\n') {
out.push('\n');
}
}
fn ensure_blank_line(out: &mut String) {
if !out.is_empty() && !out.ends_with("\n\n") {
ensure_line_break(out);
out.push('\n');
}
}
fn trim_trailing_whitespace(out: &mut String) {
while out.ends_with(char::is_whitespace) {
out.pop();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn formats_fields_and_preserves_comments() {
let source =
"Server={\n# host\nhost=String default \"localhost\"; # trailing\n\nport=Int&>0;\n};";
let formatted = format_source(source).unwrap();
assert_eq!(
formatted,
"Server = {\n # host\n host = String default \"localhost\"; # trailing\n\n port = Int & > 0;\n};\n"
);
}
#[test]
fn formats_arrays_and_match_comments() {
let source = "result=match x{# c\n_: [1,{a=2;}];};";
let formatted = format_source(source).unwrap();
assert_eq!(
formatted,
"result = match x {\n # c\n _: [\n 1,\n {\n a = 2;\n },\n ];\n};\n"
);
}
#[test]
fn formats_array_constraints() {
let source = "tags=[...String];ports=[...(Int&>=1)];";
let formatted = format_source(source).unwrap();
assert_eq!(
formatted,
"tags = [...String];\nports = [...(Int & >= 1)];\n"
);
}
#[test]
fn formats_let_functions_and_precedence_from_the_canonical_ast() {
let source = "value=let x=1+2*3;in(a:Int)=>{result=(x+a)*2;};";
let formatted = format_source(source).unwrap();
assert_eq!(
formatted,
"value = let\n x = 1 + 2 * 3;\nin\n (a: Int) =>\n {\n result = (x + a) * 2;\n };\n"
);
}
#[test]
fn preserves_regex_and_escaped_strings() {
let source = r#"value={pattern=/^api\/.+$/;text="a\n\"b";};"#;
let formatted = format_source(source).unwrap();
assert!(formatted.contains(r#"pattern = /^api\/.+$/;"#));
assert!(formatted.contains(r#"text = "a\n\"b";"#));
}
#[test]
fn preserves_comments_inside_grouping_and_between_structural_tokens() {
let source = "value = (# grouped\n 1 + 2); result = match x # before brace\n { _ # before body\n : value; };";
let formatted = format_source(source).unwrap();
assert!(formatted.contains("(# grouped\n 1 + 2)"));
assert!(formatted.contains("match x # before brace\n{"));
assert!(formatted.contains("_ # before body\n : value;"));
assert_eq!(format_source(&formatted).unwrap(), formatted);
}
#[test]
fn wasm_export_returns_the_same_formatter_result() {
let source = "value={a=1;};";
let expected = format_source(source).unwrap();
let output: serde_json::Value = serde_json::from_str(&format_source_json(source)).unwrap();
assert_eq!(output["source"], expected);
}
#[test]
fn formats_all_repository_examples_idempotently() {
let examples = [
(
"advanced/main",
include_str!("../../../examples/advanced/main.dcdl"),
),
(
"advanced/profiles",
include_str!("../../../examples/advanced/profiles.dcdl"),
),
(
"advanced/schema",
include_str!("../../../examples/advanced/schema.dcdl"),
),
(
"arithmetic",
include_str!("../../../examples/arithmetic.dcdl"),
),
(
"array-concat",
include_str!("../../../examples/array-concat.dcdl"),
),
(
"array-constraint",
include_str!("../../../examples/array-constraint.dcdl"),
),
("basic", include_str!("../../../examples/basic.dcdl")),
(
"import/main",
include_str!("../../../examples/import/main.dcdl"),
),
(
"import/schema",
include_str!("../../../examples/import/schema.dcdl"),
),
("logical", include_str!("../../../examples/logical.dcdl")),
(
"regex/main",
include_str!("../../../examples/regex/main.dcdl"),
),
];
for (name, source) in examples {
let once = format_source(source).unwrap_or_else(|error| panic!("{name}: {error}"));
let twice = format_source(&once).unwrap_or_else(|error| {
panic!("{name} reformatted to invalid source: {error}\n{once}")
});
assert_eq!(once, twice, "{name}");
}
}
}
+19
View File
@@ -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
+11
View File
@@ -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
}
}
}
+7 -2
View File
@@ -6,7 +6,7 @@ rust-version.workspace = true
license.workspace = true
repository.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"]
categories = ["wasm", "config"]
publish = false
@@ -15,9 +15,14 @@ publish = false
crate-type = ["cdylib", "rlib"]
[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
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]
wasm-opt = false
+228
View File
@@ -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
View File
@@ -1,25 +1,67 @@
use std::collections::BTreeMap;
use decodal::{
Data, Diagnostic, DiagnosticKind, EmptyLoader, Engine, LoadedSource, SourceId, SourceLoader,
Span, format_diagnostic_with,
};
#[cfg(target_arch = "wasm32")]
use decodal::HostEnvironment;
use decodal::{Data, EmptyLoader, Engine, SourceId, format_diagnostic_with};
#[cfg(target_arch = "wasm32")]
use decodal_language_service::{CompletionKind, LanguageService};
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]
pub fn evaluate(source: &str) -> String {
encode_result(evaluate_inner(source))
}
#[wasm_bindgen(js_name = evaluateProject)]
pub fn evaluate_project(entry: &str, files_json: &str) -> String {
encode_result(evaluate_project_inner(entry, files_json))
/// 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.
#[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 {
match result {
Ok(output) => format!("{{\"ok\":true,\"output\":{}}}", json_string(&output)),
Err(error) => format!("{{\"ok\":false,\"error\":{}}}", json_string(&error)),
Ok(output) => serde_json::json!({ "ok": true, "output": output }).to_string(),
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))
}
fn evaluate_project_inner(entry: &str, files_json: &str) -> Result<String, String> {
let raw_files: BTreeMap<String, String> = serde_json::from_str(files_json)
.map_err(|error| format!("failed to read playground files: {error}"))?;
let mut files = BTreeMap::new();
for (path, source) in raw_files {
let path = normalize_path(&path).ok_or_else(|| format!("invalid file path `{path}`"))?;
files.insert(path, source);
}
let entry = normalize_path(entry).ok_or_else(|| format!("invalid entry path `{entry}`"))?;
let source = files
.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) {
#[cfg(target_arch = "wasm32")]
fn evaluate_with_environment(
environment: &JsEnvironment,
key: &str,
name: &str,
source: &str,
) -> Result<String, String> {
let mut engine = environment
.create_engine()
.map_err(|diagnostic| diagnostic.message)?;
let module = match engine.add_root_source(key, name, source) {
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) {
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) {
Ok(data) => data,
Err(error) => return Err(engine.format_diagnostic(&error)),
Err(diagnostic) => return Err(engine.format_diagnostic(&diagnostic)),
};
Ok(format_data(&data, 0))
}
#[derive(Debug, Clone)]
struct VirtualLoader {
files: BTreeMap<String, String>,
}
impl SourceLoader for VirtualLoader {
fn load(
&mut self,
current_key: Option<&str>,
specifier: &str,
) -> decodal::Result<LoadedSource> {
let key = resolve_import(current_key, specifier).ok_or_else(|| {
Diagnostic::new(
DiagnosticKind::Import,
Span::default(),
format!("invalid import path `{specifier}`"),
)
})?;
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('/');
#[cfg(target_arch = "wasm32")]
fn encode_completion(
environment: &JsEnvironment,
key: &str,
source: &str,
position: usize,
explicit: bool,
) -> String {
let byte_position = utf16_offset_to_byte(source, position);
let service = LanguageService::new(environment);
let completion = match service.complete(key, source, byte_position, explicit) {
Ok(completion) => completion,
Err(diagnostic) => {
return serde_json::json!({
"ok": false,
"error": diagnostic.message,
})
.to_string();
}
}
base.push_str(specifier);
normalize_path(&base)
};
let completion = completion.map(|completion| {
let from = byte_offset_to_utf16(source, completion.from);
let options = completion
.items
.into_iter()
.map(|item| {
serde_json::json!({
"label": item.label,
"kind": completion_kind_name(item.kind),
"detail": item.detail,
"priority": item.priority,
})
})
.collect::<Vec<_>>();
serde_json::json!({ "from": from, "options": options })
});
serde_json::json!({ "ok": true, "completion": completion }).to_string()
}
fn normalize_path(path: &str) -> Option<String> {
let mut parts = Vec::new();
let normalized = path.replace('\\', "/");
for part in normalized.split('/') {
match part {
"" | "." => {}
".." => {
parts.pop()?;
}
part => parts.push(part),
#[cfg(target_arch = "wasm32")]
fn completion_kind_name(kind: CompletionKind) -> &'static str {
match kind {
CompletionKind::Keyword => "keyword",
CompletionKind::Constant => "constant",
CompletionKind::Type => "type",
CompletionKind::Variable => "variable",
CompletionKind::Namespace => "namespace",
CompletionKind::Property => "property",
CompletionKind::File => "file",
}
}
#[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() {
None
} else {
Some(parts.join("/"))
source.len()
}
#[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 {
@@ -191,66 +236,28 @@ fn format_data(data: &Data, indent: usize) -> String {
}
fn json_string(value: &str) -> String {
let mut out = String::from("\"");
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
serde_json::to_string(value).expect("strings are always JSON-serializable")
}
#[cfg(test)]
mod tests {
use super::{evaluate_project_inner, normalize_path, resolve_import};
use super::{byte_offset_to_utf16, evaluate_inner, utf16_offset_to_byte};
#[test]
fn normalizes_virtual_paths() {
fn evaluates_standalone_sources() {
assert_eq!(
normalize_path("/schemas/../main.dcdl"),
Some("main.dcdl".into())
);
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())
evaluate_inner("value = 1;").unwrap(),
"{\n \"value\": 1\n}"
);
}
#[test]
fn evaluates_project_imports() {
let files = r#"{
"main.dcdl":"let dep = import \"./schemas/service.dcdl\"; in dep.Service & { port = 9443; }",
"schemas/service.dcdl":"Service = { name = String default \"api\"; port = Int & > 443 default 8443; }"
}"#;
let output = evaluate_project_inner("main.dcdl", files).unwrap();
assert!(output.contains("\"port\": 9443"));
}
#[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:"));
fn converts_web_utf16_offsets() {
let source = "a😀β";
assert_eq!(utf16_offset_to_byte(source, 0), 0);
assert_eq!(utf16_offset_to_byte(source, 1), 1);
assert_eq!(utf16_offset_to_byte(source, 3), 5);
assert_eq!(byte_offset_to_utf16(source, 5), 3);
assert_eq!(byte_offset_to_utf16(source, source.len()), 4);
}
}
+209
View File
@@ -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, &current_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, &current_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)
}
+52 -9
View File
@@ -30,16 +30,52 @@ Important paths:
```text
crates/decodal-wasm/
site/decodal-site/src/wasm/
packages/decodal-wasm/
```
The generated files in `site/decodal-site/src/wasm/` are committed so the site can build without requiring every consumer to run `wasm-pack` first.
The npm package is `decodal-wasm`.
The JSR package is `@hare/decodal-wasm`.
The generated files in `packages/decodal-wasm/` are committed so the site can build without requiring every consumer to run `wasm-pack` first.
The WebAssembly package is for execution, not syntax highlighting.
## Language tools
Semantic editor integration lives in the host-configurable language service crate.
It depends only on the runtime and accepts the same `HostEnvironment` implementation used by a production application.
The LSP crate adapts that service to the Language Server Protocol over stdin/stdout.
It provides full document synchronization, semantic diagnostics, and whole-document formatting.
A host-specific LSP binary injects its loader and global schema configuration without reimplementing evaluation rules.
Important paths:
```text
crates/decodal-language-service/
crates/decodal-lsp/
```
The default `decodal-lsp` binary reads Decodal imports from the filesystem.
Embedded hosts can call its library entry point with a custom `LspEnvironment` to reuse structured imports and to make unsaved external documents, such as Markdown, visible to the loader.
Source formatting lives in a separate Rust language tools crate.
It uses the canonical Decodal AST together with the runtime lexer's lossless syntax tokens, so native LSP and WebAssembly callers execute the same formatter implementation.
This component is responsible for operations that must preserve source text details such as comments and whitespace.
It is used by the CodeMirror package's bundled formatter WebAssembly and can also be used by an LSP adapter for formatting.
It does not depend on Tree-sitter or Lezer.
Important paths:
```text
crates/decodal-language-tools/
```
The current language tools crate exposes the formatter.
## Web editor components
The Web playground editor uses CodeMirror 6 with a generated Lezer parser.
This is the source of syntax highlighting, folding, indentation, and editor syntax tree behavior in the browser UI.
Lezer provides syntax highlighting, folding, indentation, and editor syntax tree behavior. The browser formatter command calls the canonical Rust formatter compiled to WebAssembly.
Important paths:
@@ -48,6 +84,8 @@ editors/lezer-decodal/decodal.grammar
packages/decodal-codemirror/src/decodal.js
packages/decodal-codemirror/src/decodal-parser.js
packages/decodal-codemirror/src/decodal-parser.terms.js
packages/decodal-codemirror/src/format.js
packages/decodal-codemirror/wasm/
```
The npm package is `decodal-codemirror`.
@@ -83,15 +121,20 @@ doc/manual/souce/language/grammar.md
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.
## What is not a public component
## Syntax token API
The Rust lexer is an implementation detail of the Rust parser.
Decodal does not expose a standalone public tokenizer API for editor tooling.
Consumers that need syntax information should use the component that matches their environment:
The `decodal` crate exposes `tokenize_source`, `tokenize_source_with_source_id`, `SyntaxToken`, and `SyntaxTokenKind` for source-preserving tooling.
Comments have explicit tokens, while whitespace is represented by gaps between token spans and can be recovered from the original source.
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`
- Browser execution: `decodal-wasm`
- Web editor syntax: Lezer / CodeMirror
- Web formatting and editor syntax: canonical formatter / CodeMirror / Lezer
- Semantic editor analysis: `decodal-language-service`
- Language Server Protocol integration: `decodal-lsp`
- Rust formatting: `decodal-language-tools`
- General editor syntax: Tree-sitter
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
message: String
labels: Vec<DiagnosticLabel>
notes: Vec<String>
}
DiagnosticLabel {
@@ -22,6 +23,7 @@ DiagnosticLabel {
`span` は primary location を示す。
表示時には `Span.source` を source id のまま出すのではなく、可能な限り file path や virtual file name に解決する。
`labels` は同じ error に関係する追加 location を示す。
`notes` は source location を持たない semantic context を示す。
合成や materialize の失敗では、衝突した constraint、value、default、または処理中 field path を label に含める。
代表的な diagnostic kind:
@@ -37,6 +39,9 @@ DiagnosticLabel {
- match 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` を返す。
+93 -5
View File
@@ -1,7 +1,7 @@
# Embedding API
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
@@ -17,7 +17,7 @@ let / function env
```
Module top-level bindings shadow prelude bindings.
Primitive type names such as `String`, `Int`, `Float`, `Bool`, and `Array` are handled before environment lookup, so they are reserved and cannot be shadowed by host bindings.
Primitive type names such as `String`, `Int`, `Float`, and `Bool` are handled before environment lookup, so they are reserved and cannot be shadowed by host bindings.
## Global bindings
@@ -59,12 +59,16 @@ HostValue =
Float
Bool
Array(Vec<HostValue>)
ArrayConstraint { item, constraints, default }
Object(Vec<HostField>)
Abstract { constraints, default }
```
When a host value is bound, the engine internalizes it into `RuntimeValue` and allocates value thunks for object fields, array items, and defaults.
`HostValue::array_of(item)` builds an array constraint with a required element schema.
There is no host API for an unconstrained abstract array.
## Abstract host objects
A host-provided schema object is represented as a concrete object structure whose fields may contain abstract values.
@@ -127,11 +131,95 @@ Supported field attributes are intentionally small:
The derive does not add host callbacks or reflection.
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.
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.
+11 -6
View File
@@ -23,6 +23,9 @@ materialize
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 は source を AST に変換する。
@@ -66,14 +69,16 @@ ModuleRegistry:
処理系は、まず root module を parse / desugar して registry に登録する。
import 先 module は、この段階で全て読み込む必要はない。
import expression が評価されたとき、処理系は `SourceLoader` に現在の module key と import specifier を渡す。
loader は module key、表示名、source text を返す。
module registry は key が未登録なら対象 module を parse / desugar して登録する。
登録された module は module root thunk を持つ。
同じ module が複数回 import された場合は、同じ `ModuleId` を返す
import expression が評価されたとき、処理系は `ImportLoader::load` に現在の module key と import specifier を渡す。
loader は module key、表示名、および DCDL source text または構造化済み `HostValue` を返す。
DCDL source の場合、module registry は key が未登録なら対象 module を parse / desugar して登録する。
登録された source module は module root thunk を持つ。
構造化値の場合、host value を runtime value に internalize した root thunk を key でキャッシュする
同じ key が複数回 import された場合は、同じ source module または structured root thunk を使う。
つまり import は module を即時評価しない。
つまり source import は module を即時評価しない。
module を読み込み、module root を thunk として登録するだけにする。
structured import も root value を thunk として保持し、object field や array item の既存 thunk model を利用する。
AST の `ExprId` は module-local である。
そのため runtime が保持する式参照は `ExprRef { module, expr }` として module-qualified にする。
+6 -2
View File
@@ -104,7 +104,7 @@ Concrete(Int(8000))
## Runtime scope
Decodal runtime は application runtime ではなく、pure value evaluator である。
同じ source、同じ import sources、同じ host globals が与えられた場合、評価結果は決定的である。
同じ source、同じ import results、同じ host globals が与えられた場合、評価結果は決定的である。
runtime が扱う責務は以下に限る。
@@ -114,7 +114,7 @@ runtime が扱う責務は以下に限る。
- materialize 時に constraint を検証する。
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
@@ -123,12 +123,16 @@ constraint は concrete value とは別の型として扱う。
```text
Constraint =
Type(PrimitiveType)
ArrayItems(ThunkId)
Compare(Op, Literal)
Regex(Pattern)
BuiltinPredicate(Symbol)
ObjectConstraint(...)
```
`ArrayItems` は配列そのものの型と、すべての要素へ合成する schema thunk を表す。
配列用の primitive type は持たず、抽象配列には必ず要素制約が必要である。
初期実装では、object の形は主に `Concrete(Object)` の field に `Abstract` を置くことで表現する。
object 全体にかかる constraint は必要になった時点で追加する。
+34 -36
View File
@@ -13,6 +13,18 @@ cargo check -p decodal --no-default-features
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.
```sh
@@ -22,32 +34,13 @@ cargo run -q -p decodal-cli --features regex -- examples/regex/main.dcdl
## crates.io release
The primary crates.io package is `decodal`, which contains the embeddable library.
`decodal-derive` provides optional derive macros for Rust struct integration and is published only when the derive crate changes.
Workspace support crates such as `decodal-cli` and the Rust source crate `decodal-wasm` are not published to crates.io.
The generated WebAssembly package under `site/decodal-site/src/wasm/` is published to npm and JSR.
The crates.io release contains `decodal`, `decodal-derive`, `decodal-language-service`, `decodal-language-tools`, and `decodal-lsp`.
`decodal-cli` and the Rust source crate `decodal-wasm` remain repository-only packages.
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 project is dual licensed as `MIT OR Apache-2.0`.
```sh
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`.
The authoritative validation commands, dependency order, and publish commands are maintained in `RELEASING.md` at the repository root.
## Web site and playground
@@ -58,7 +51,7 @@ site/decodal-site/
```
The site imports Markdown files from `doc/manual/souce/` and renders them as mdBook-style pages.
The playground loads `decodal-wasm` and evaluates DCDL entirely in the browser.
The playground loads `decodal-wasm` for evaluation and `decodal-codemirror` for editor features and source formatting.
Important files:
@@ -72,10 +65,15 @@ site/decodal-site/src/lib/docs.js
site/decodal-site/src/lib/highlight.js
packages/decodal-codemirror/src/decodal.js
packages/decodal-codemirror/src/decodal-parser.js
packages/decodal-wasm/decodal_wasm.js
packages/decodal-wasm/decodal_wasm_bg.wasm
packages/decodal-codemirror/wasm/decodal_language_tools.js
packages/decodal-codemirror/wasm/decodal_language_tools_bg.wasm
crates/decodal-wasm/src/lib.rs
crates/decodal-language-tools/src/lib.rs
```
Build the WebAssembly package before building the site:
Build the WebAssembly packages before building the site:
```sh
cd site/decodal-site
@@ -84,18 +82,18 @@ npm run build:wasm
npm run build
```
`npm run build:wasm` writes generated files into:
`npm run build:wasm` builds both generated WebAssembly packages.
`npm run build:wasm:runtime` writes generated runtime files into `packages/decodal-wasm/`.
`npm run build:wasm:formatter` writes generated formatter files into `packages/decodal-codemirror/wasm/`.
`npm run build` builds only the static site from the current package files.
Use `npm run build:all` to regenerate both WebAssembly packages and then build the static site in one command.
```text
site/decodal-site/src/wasm/
```
These generated files are committed so the site can be built without requiring every consumer to regenerate the wasm package first.
These generated files are committed so the site can be built without requiring every consumer to regenerate the wasm packages first.
Publish the generated WebAssembly package from its package directory:
```sh
cd site/decodal-site/src/wasm
cd packages/decodal-wasm
npm publish
npx jsr publish
```
@@ -109,7 +107,7 @@ npx jsr publish --dry-run
The npm package name is `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 canonical grammar is documented in `doc/manual/souce/language/grammar.md`; regenerate the Lezer parser when that grammar or `editors/lezer-decodal/decodal.grammar` changes.
@@ -133,7 +131,7 @@ npx jsr publish --dry-run
The npm package name is `decodal-codemirror`.
The 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.
@@ -162,7 +160,7 @@ CLOUDFLARE_PROJECT_NAME=my-pages-project npm run deploy
If the committed WASM package must be regenerated before deploy, use:
```sh
npm run deploy:wasm
npm run deploy:all
```
## Tree-sitter grammar
@@ -124,6 +124,19 @@ String & /^a$/ & /^b$/
Host = IPv4Address;
```
## 配列制約
配列制約には要素制約が必須であり、`[...T]` と書く。
```dcdl
Names = [...String];
PositiveInts = [...(Int & > 0)];
```
複数の配列制約を `&` で合成した場合、各 concrete 要素へすべての要素制約を合成する。
要素が object schema の場合は、その schema に含まれる default も各要素へ適用される。
要素制約のない `Array` primitive type は存在しない。
## default
`default` は制約ではない。
+17
View File
@@ -31,6 +31,23 @@ disabled_config = NewConfig & {
enabled_config = NewConfig;
```
## 配列スキーマ
```dcdl
Services = [...{
name = String;
port = Int default 8080;
}];
Services & [
{ name = "api"; },
{ name = "worker"; port = 9000; },
]
```
抽象配列には要素制約が必須である。
この例では、1 番目の要素の `port``8080` に materialize される。
## 関数と制約
```dcdl
@@ -7,6 +7,46 @@ array expression は、順序付きの値の列を表す。
["a", "b", "c"]
```
配列リテラルは concrete value であり、要素型を揃える必要はない。
```dcdl
["api", 8080, true]
```
## Array constraint
配列制約は `[` の直後に ellipsis を置き、その後へ要素制約を記述する。
```dcdl
Names = [...String];
Ports = [...(Int & >= 1 & <= 65535)];
```
`[...T]` は、すべての要素が `T` を満たす長さ 0 以上の配列を表す。
空配列は任意の配列制約を満たす。
```dcdl
[...String] & []
[...String] & ["api", "worker"]
```
要素制約は object schema にもできる。
要素 schema の default は、配列へ制約を合成するときに各要素へ適用される。
```dcdl
Services = [...{
name = String;
enabled = Bool default true;
}];
Services & [{ name = "api"; }]
```
要素制約のない抽象配列型は提供しない。
旧来の `Array` primitive type は使用できず、`[...T]``T` は必須である。
`[String]` は配列制約ではなく、未解決の `String` 制約を 1 要素に持つ concrete array になる。
長さ制約、位置別の tuple 制約、unique 制約は現在サポートしない。
## Array concat
`++` は concrete array 同士を連結する。
@@ -11,6 +11,7 @@ Expr
├─ path reference
├─ object
├─ array
├─ array constraint
├─ function
├─ function call
├─ let
+2
View File
@@ -57,6 +57,7 @@ primary_expression = literal
| identifier
| comparison_constraint
| object
| array_constraint
| array
| let_expression
| function_expression
@@ -73,6 +74,7 @@ field_definition = field_path , "=" , expression ;
field_path = identifier , { "." , identifier } ;
array = "[" , [ expression , { "," , expression } , [ "," ] ] , "]" ;
array_constraint = "[" , "..." , expression , [ "," ] , "]" ;
let_expression = "let" , { field_definition , ";" } , "in" , expression ;
function_expression = "(" , [ parameter_list ] , ")" , "=>" , expression ;
@@ -32,15 +32,77 @@ result = schema;
通常の object literal 内の field は、その object 内の sibling field を暗黙には識別子として参照できない。
object 内の値を参照する場合は、外側で束縛された値や明示的な path reference を使う。
## SourceLoader
## ImportLoader
`import` specifier の解決は処理系 core ではなく host 側の `SourceLoader` が行う。
`import` specifier の解決は処理系 core ではなく host 側の `ImportLoader` が行う。
CLI では、specifier を現在の module path からの相対 path として解決する。
組み込み利用では、resource table や static source map など、filesystem 以外の loader を使える。
module cache の key は loader が返す安定 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
モジュール間に循環参照があっても、必要なフィールドの依存関係が循環していなければ評価できる。
@@ -93,4 +155,5 @@ Module func
- ファイルが読めない。
- import 先の構文解析に失敗する。
- import 先の評価で必要な値がエラーになる。
- host による非 DCDL content の読み込みまたは構造化に失敗する。
- 実装が禁止する import 循環に該当する。
+1
View File
@@ -121,6 +121,7 @@ false
default fallback 指定
=> 関数
. フィールド参照 / ドットパス定義
... 配列の要素制約
```
演算子の優先順位は [合成演算子](./operators.md) で定義する。
+3 -2
View File
@@ -9,10 +9,11 @@ name = String;
retry = Int default 3;
ratio = Float;
enable = Bool default true;
tags = Array;
tags = [...String];
```
現在の primitive type は `String``Int``Float``Bool``Array` である。
現在の primitive type は `String``Int``Float``Bool` である。
配列は primitive type ではなく、必須の要素制約を持つ `[...T]` で表現する。
各型の個別仕様へのリンクは [Manual Index](../../index.md) に集約する。
primitive type と制約合成の詳細は [制約と default](../constraints-and-defaults.md) も参照する。
+3
View File
@@ -71,6 +71,7 @@ PrimaryExpression {
Identifier |
ComparisonConstraint |
Object |
ArrayConstraint |
Array |
LetExpression |
FunctionExpression |
@@ -88,6 +89,7 @@ FieldDefinition { FieldPath Equal Expression }
FieldPath { Identifier !fieldPath (Dot Identifier)* }
Array { LBracket (Expression (Comma Expression)* Comma?)? RBracket }
ArrayConstraint { LBracket Ellipsis Expression Comma? RBracket }
LetExpression { Let FieldDefinitionList In Expression }
FieldDefinitionList { (FieldDefinition Semicolon)* }
@@ -143,6 +145,7 @@ ImportExpression { Import String }
Semicolon { ";" }
Comma { "," }
Dot { "." }
Ellipsis { "..." }
Colon { ":" }
Equal { "=" }
EqualEqual { "==" }
+2 -2
View File
@@ -6,7 +6,7 @@ license = "MIT"
readme = "README.md"
keywords = ["incremental", "parsing", "tree-sitter", "decodal"]
categories = ["parsing", "text-editors"]
repository = "https://github.com/tree-sitter/tree-sitter-decodal"
repository = "https://gitea.hareworks.net/Hare/Decodal"
edition = "2021"
autoexamples = false
@@ -17,7 +17,7 @@ include = ["bindings/rust/*", "grammar.js", "queries/*", "src/*"]
path = "bindings/rust/lib.rs"
[dependencies]
tree-sitter = ">=0.22.6"
tree-sitter = "0.22.6"
[build-dependencies]
cc = "1.0.87"
@@ -158,3 +158,27 @@ Array concat
right: (literal (integer))))
right: (array
(literal (integer)))))))
==================
Array constraints
==================
{
tags = [...String];
ports = [...(Int & >= 1),];
}
---
(source_file
(object
(field_definition
path: (field_path (identifier))
value: (array_constraint
element: (identifier)))
(field_definition
path: (field_path (identifier))
value: (array_constraint
element: (parenthesized_expression
(binary_expression
left: (identifier)
right: (comparison_constraint
value: (integer))))))))
+9
View File
@@ -54,6 +54,7 @@ module.exports = grammar({
$.regex_literal,
$.comparison_constraint,
$.object,
$.array_constraint,
$.array,
$.let_expression,
$.function_expression,
@@ -116,6 +117,14 @@ module.exports = grammar({
']',
),
array_constraint: $ => seq(
'[',
'...',
field('element', $._expression),
optional(','),
']',
),
let_expression: $ => seq(
'let',
repeat(seq($.field_definition, ';')),
@@ -22,6 +22,7 @@
"*"
"/"
"++"
"..."
"&&"
"||"
"!"
+41
View File
@@ -95,6 +95,10 @@
"type": "SYMBOL",
"name": "object"
},
{
"type": "SYMBOL",
"name": "array_constraint"
},
{
"type": "SYMBOL",
"name": "array"
@@ -427,6 +431,43 @@
}
]
},
"array_constraint": {
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "["
},
{
"type": "STRING",
"value": "..."
},
{
"type": "FIELD",
"name": "element",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
},
{
"type": "CHOICE",
"members": [
{
"type": "STRING",
"value": ","
},
{
"type": "BLANK"
}
]
},
{
"type": "STRING",
"value": "]"
}
]
},
"let_expression": {
"type": "SEQ",
"members": [
+160
View File
@@ -11,6 +11,10 @@
"type": "array",
"named": true
},
{
"type": "array_constraint",
"named": true
},
{
"type": "binary_expression",
"named": true
@@ -74,6 +78,86 @@
]
}
},
{
"type": "array_constraint",
"named": true,
"fields": {
"element": {
"multiple": false,
"required": true,
"types": [
{
"type": "array",
"named": true
},
{
"type": "array_constraint",
"named": true
},
{
"type": "binary_expression",
"named": true
},
{
"type": "call_expression",
"named": true
},
{
"type": "comparison_constraint",
"named": true
},
{
"type": "default_expression",
"named": true
},
{
"type": "function_expression",
"named": true
},
{
"type": "identifier",
"named": true
},
{
"type": "import_expression",
"named": true
},
{
"type": "let_expression",
"named": true
},
{
"type": "literal",
"named": true
},
{
"type": "match_expression",
"named": true
},
{
"type": "object",
"named": true
},
{
"type": "parenthesized_expression",
"named": true
},
{
"type": "path_expression",
"named": true
},
{
"type": "regex_literal",
"named": true
},
{
"type": "unary_expression",
"named": true
}
]
}
}
},
{
"type": "binary_expression",
"named": true,
@@ -86,6 +170,10 @@
"type": "array",
"named": true
},
{
"type": "array_constraint",
"named": true
},
{
"type": "binary_expression",
"named": true
@@ -222,6 +310,10 @@
"type": "array",
"named": true
},
{
"type": "array_constraint",
"named": true
},
{
"type": "binary_expression",
"named": true
@@ -303,6 +395,10 @@
"type": "array",
"named": true
},
{
"type": "array_constraint",
"named": true
},
{
"type": "binary_expression",
"named": true
@@ -374,6 +470,10 @@
"type": "array",
"named": true
},
{
"type": "array_constraint",
"named": true
},
{
"type": "binary_expression",
"named": true
@@ -471,6 +571,10 @@
"type": "array",
"named": true
},
{
"type": "array_constraint",
"named": true
},
{
"type": "binary_expression",
"named": true
@@ -547,6 +651,10 @@
"type": "array",
"named": true
},
{
"type": "array_constraint",
"named": true
},
{
"type": "binary_expression",
"named": true
@@ -617,6 +725,10 @@
"type": "array",
"named": true
},
{
"type": "array_constraint",
"named": true
},
{
"type": "binary_expression",
"named": true
@@ -703,6 +815,10 @@
"type": "array",
"named": true
},
{
"type": "array_constraint",
"named": true
},
{
"type": "binary_expression",
"named": true
@@ -794,6 +910,10 @@
"type": "array",
"named": true
},
{
"type": "array_constraint",
"named": true
},
{
"type": "binary_expression",
"named": true
@@ -896,6 +1016,10 @@
"type": "array",
"named": true
},
{
"type": "array_constraint",
"named": true
},
{
"type": "binary_expression",
"named": true
@@ -1009,6 +1133,10 @@
"type": "array",
"named": true
},
{
"type": "array_constraint",
"named": true
},
{
"type": "binary_expression",
"named": true
@@ -1083,6 +1211,10 @@
"type": "array",
"named": true
},
{
"type": "array_constraint",
"named": true
},
{
"type": "binary_expression",
"named": true
@@ -1159,6 +1291,10 @@
"type": "array",
"named": true
},
{
"type": "array_constraint",
"named": true
},
{
"type": "binary_expression",
"named": true
@@ -1260,6 +1396,10 @@
"type": "array",
"named": true
},
{
"type": "array_constraint",
"named": true
},
{
"type": "binary_expression",
"named": true
@@ -1346,6 +1486,10 @@
"type": "array",
"named": true
},
{
"type": "array_constraint",
"named": true
},
{
"type": "binary_expression",
"named": true
@@ -1431,6 +1575,10 @@
"type": "array",
"named": true
},
{
"type": "array_constraint",
"named": true
},
{
"type": "binary_expression",
"named": true
@@ -1507,6 +1655,10 @@
"type": "array",
"named": true
},
{
"type": "array_constraint",
"named": true
},
{
"type": "binary_expression",
"named": true
@@ -1586,6 +1738,10 @@
"type": "array",
"named": true
},
{
"type": "array_constraint",
"named": true
},
{
"type": "binary_expression",
"named": true
@@ -1712,6 +1868,10 @@
"type": ".",
"named": false
},
{
"type": "...",
"named": false
},
{
"type": "/",
"named": false
+5487 -4941
View File
File diff suppressed because it is too large Load Diff
+11
View File
@@ -0,0 +1,11 @@
let
Service = {
name = String;
enabled = Bool default true;
};
Services = [...Service];
in
Services & [
{ name = "api"; },
{ name = "worker"; enabled = false; },
]
+1
View File
@@ -29,6 +29,7 @@
apps.default = mkApp "decodal" "Run Decodal";
apps.decodal = mkApp "decodal" "Run Decodal";
apps.decodal-lsp = mkApp "decodal-lsp" "Run the Decodal language server";
checks.default = decodal;
+4 -1
View File
@@ -24,7 +24,7 @@ let
in
rustPlatform.buildRustPackage {
pname = "decodal";
version = "0.1.1";
version = "0.1.2";
src = lib.cleanSourceWith {
src = srcRoot;
@@ -38,6 +38,8 @@ rustPlatform.buildRustPackage {
cargoBuildFlags = [
"-p"
"decodal-cli"
"-p"
"decodal-lsp"
];
doInstallCheck = true;
@@ -45,6 +47,7 @@ rustPlatform.buildRustPackage {
runHook preInstallCheck
test -x "$out/bin/decodal"
test -x "$out/bin/decodal-lsp"
"$out/bin/decodal" >/dev/null
runHook postInstallCheck
+173
View File
@@ -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
+21
View File
@@ -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.
+22 -2
View File
@@ -2,8 +2,8 @@
CodeMirror 6 language support for Decodal.
This package provides the Lezer parser metadata, highlighting tags, indentation rules, and block folding used by the Decodal Web playground.
It does not include the Decodal runtime or WebAssembly evaluator.
This package provides the Lezer parser metadata, highlighting tags, indentation rules, block folding, and formatter command used by the Decodal Web playground.
It does not include the Decodal runtime evaluator.
Use `decodal-wasm` when you want to evaluate Decodal in a browser.
## Install
@@ -52,3 +52,23 @@ decodal({ highlight: false })
```
Use this when you want the language support without the bundled highlight style.
## Formatting
The formatter is exposed as a separate subpath so plain syntax highlighting does not force callers to initialize WebAssembly.
```js
import {
formatDecodal,
formatDecodalCommand,
initDecodalFormatter,
} from 'decodal-codemirror/format';
import formatterWasm from 'decodal-codemirror/wasm/decodal_language_tools_bg.wasm?url';
await initDecodalFormatter(formatterWasm);
const result = formatDecodal('Server={port=Int default 8080;};');
if (result.ok) console.log(result.source);
formatDecodalCommand(view);
```
+16 -2
View File
@@ -1,23 +1,37 @@
{
"name": "@hare/decodal-codemirror",
"version": "0.1.2",
"version": "0.1.6",
"license": "MIT",
"exports": "./src/mod.ts",
"exports": {
".": "./src/mod.ts",
"./format": "./src/format.ts"
},
"imports": {
"@codemirror/language": "npm:@codemirror/language@^6.12.4",
"@codemirror/view": "npm:@codemirror/view@^6.43.6",
"@lezer/highlight": "npm:@lezer/highlight@^1.2.3",
"@lezer/lr": "npm:@lezer/lr@^1.4.10"
},
"publish": {
"include": [
"README.md",
"LICENSE-MIT",
"LICENSE-APACHE",
"src/mod.ts",
"src/format.ts",
"src/decodal.js",
"src/decodal.d.ts",
"src/decodal.js.d.ts",
"src/format.js",
"src/format.d.ts",
"src/format.js.d.ts",
"src/decodal-parser-jsr.js",
"src/decodal-parser.js",
"src/decodal-parser.terms.js",
"wasm/decodal_language_tools.js",
"wasm/decodal_language_tools.d.ts",
"wasm/decodal_language_tools_bg.wasm",
"wasm/decodal_language_tools_bg.wasm.d.ts",
"package.json"
]
}
+21 -3
View File
@@ -1,15 +1,23 @@
{
"name": "decodal-codemirror",
"version": "0.1.2",
"version": "0.1.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "decodal-codemirror",
"version": "0.1.2",
"version": "0.1.6",
"license": "MIT OR Apache-2.0",
"dependencies": {
"devDependencies": {
"@codemirror/language": "^6.12.4",
"@codemirror/state": "^6.7.1",
"@codemirror/view": "^6.43.6",
"@lezer/highlight": "^1.2.3",
"@lezer/lr": "^1.4.10"
},
"peerDependencies": {
"@codemirror/language": "^6.12.4",
"@codemirror/view": "^6.43.6",
"@lezer/highlight": "^1.2.3",
"@lezer/lr": "^1.4.10"
}
@@ -18,6 +26,7 @@
"version": "6.12.4",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.4.tgz",
"integrity": "sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
@@ -32,6 +41,7 @@
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.7.1.tgz",
"integrity": "sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@marijn/find-cluster-break": "^1.0.0"
@@ -41,6 +51,7 @@
"version": "6.43.6",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.6.tgz",
"integrity": "sha512-EVunGSYN1wz1p75WY1s3Xg7t3i8Yol0kGZGizNdX9BUFgMFILYVe8/u6EVpo7Ff5PwbZuILb4QAq7IZoKzIEQA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.7.0",
@@ -53,12 +64,14 @@
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz",
"integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@lezer/highlight": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz",
"integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.3.0"
@@ -68,6 +81,7 @@
"version": "1.4.10",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz",
"integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.0.0"
@@ -77,24 +91,28 @@
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.3.tgz",
"integrity": "sha512-FY+MKLBoTsLNJF/eLWaOsXGdz6uh3Iu1axjPf6TUq92IYumcTcXWHoS747JARLkcdlJ/Waiaxc5wQfFO8jC6NA==",
"dev": true,
"license": "MIT"
},
"node_modules/crelt": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.7.tgz",
"integrity": "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==",
"dev": true,
"license": "MIT"
},
"node_modules/style-mod": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz",
"integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==",
"dev": true,
"license": "MIT"
},
"node_modules/w3c-keyname": {
"version": "2.2.8",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz",
"integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==",
"dev": true,
"license": "MIT"
}
}
+26 -4
View File
@@ -1,6 +1,6 @@
{
"name": "decodal-codemirror",
"version": "0.1.2",
"version": "0.1.6",
"description": "CodeMirror 6 language support for Decodal.",
"type": "module",
"license": "MIT OR Apache-2.0",
@@ -9,26 +9,48 @@
"url": "git+https://gitea.hareworks.net/Hare/Decodal.git",
"directory": "packages/decodal-codemirror"
},
"scripts": {
"test": "node --test src/*.test.mjs"
},
"exports": {
".": {
"types": "./src/decodal.d.ts",
"import": "./src/decodal.js"
},
"./format": {
"types": "./src/format.d.ts",
"import": "./src/format.js"
},
"./parser": "./src/decodal-parser.js",
"./terms": "./src/decodal-parser.terms.js"
"./terms": "./src/decodal-parser.terms.js",
"./wasm/decodal_language_tools_bg.wasm": "./wasm/decodal_language_tools_bg.wasm"
},
"types": "./src/decodal.d.ts",
"files": [
"README.md",
"src/"
"LICENSE-MIT",
"LICENSE-APACHE",
"src/",
"wasm/decodal_language_tools.js",
"wasm/decodal_language_tools.d.ts",
"wasm/decodal_language_tools_bg.wasm",
"wasm/decodal_language_tools_bg.wasm.d.ts"
],
"keywords": [
"decodal",
"codemirror",
"lezer"
],
"dependencies": {
"peerDependencies": {
"@codemirror/language": "^6.12.4",
"@codemirror/view": "^6.43.6",
"@lezer/highlight": "^1.2.3",
"@lezer/lr": "^1.4.10"
},
"devDependencies": {
"@codemirror/language": "^6.12.4",
"@codemirror/state": "^6.7.1",
"@codemirror/view": "^6.43.6",
"@lezer/highlight": "^1.2.3",
"@lezer/lr": "^1.4.10"
}
@@ -2,15 +2,15 @@
import {LRParser} from "npm:@lezer/lr@^1.4.10"
export const parser = LRParser.deserialize({
version: 14,
states: "7vQYQPOOO#nQPO'#CaOOQO'#Cr'#CrO$wQPO'#CyO&UQPO'#DOO&^QPO'#DSO&eQPO'#DWO&mQPO'#CqO$wQPO'#DcO&wQPO'#DhOOQO'#Cq'#CqOOQO'#Cp'#CpO&|QPO'#CoO$wQPO'#CoOOQO'#Cn'#CnO)fQPO'#CmO.XQPO'#ClO0]QPO'#CkOOQO'#Cj'#CjO2TQPO'#CiO3xQPO'#ChO5jQPO'#CgO7UQPO'#CfOOQO'#Ce'#CeO7`QPO'#C`O7eQPO'#C_OOQO'#Dz'#DzQYQPOOO8xQPO'#D{O8}QPO,58{OOQO,59e,59eO9VQPO'#CaOOQO,59j,59jO9_QPO,59jOOQO,59n,59nO9gQPO,59nO9oQPO'#EOO9tQPO'#DYO9|QPO,59rO:RQPO'#CqO:`QPO'#D^O:hQQO,59vO:mQPO,59vO:rQPO,59]O:wQPO,59}OOQO,5:S,5:SO:|QPO'#DjOOQO,59[,59[O;TQPO,59[OOQO,59Z,59ZO$wQPO,59YO$wQPO,59XO$wQPO,59WOOQO'#Dr'#DrO$wQPO,59VO$wQPO,59UO$wQPO,59TO$wQPO,59SO$wQPO,59RO$wQPO,59QO$wQPO,58zOOQO,58y,58yOOQO-E7x-E7xOOQO,5:g,5:gOOQO-E7y-E7yO;YQPO1G/UO;bQPO1G/UOOQO1G/U1G/UO;jQPO1G/YO;qQPO1G/YOOQO1G/Y1G/YOOQO,5:j,5:jOOQO-E7|-E7|O$wQPO1G/^O$wQPO,59yO;yQPO,59xO<RQPO,59xO$wQPO1G/bO<ZQQO1G/bOOQO1G.w1G.wO<`QPO1G/iO<jQPO'#DkOOQO,5:U,5:UO<rQPO,5:UOOQO1G.v1G.vOOQO1G.t1G.tO<wQPO1G.sO@_QPO1G.rOAyQPO1G.qOOQO1G.p1G.pOCqQPO1G.oOEfQPO1G.nOFxQPO1G.mOOQO1G.l1G.lOOQO1G.f1G.fOOQO1G.s1G.sOOQO1G.r1G.rOOQO1G.q1G.qOOQO1G.o1G.oOOQO1G.n1G.nOOQO1G.m1G.mOOQO,5:h,5:hOOQO7+$p7+$pOHfQPO7+$pOOQO-E7z-E7zOOQO,5:i,5:iOOQO7+$t7+$tOHnQPO7+$tOOQO-E7{-E7{OOQO7+$x7+$xOOQO1G/e1G/eOHuQPO'#D_OOQO,5:k,5:kOHzQPO1G/dOOQO-E7}-E7}OOQO7+$|7+$|O$wQPO7+$|OOQO'#Df'#DfOISQPO'#DeOOQO7+%T7+%TOIXQPO7+%TOIaQPO,5:VOIhQPO,5:VOOQO1G/p1G/pOOQO<<H[<<H[PIpQPO'#D|OOQO<<H`<<H`P$wQPO'#D}PIuQPO'#EPOOQO<<Hh<<HhO$wQPO,5:POIzQPO<<HoOJUQPO<<HoOOQO<<Ho<<HoOJ^QPO1G/qOOQO1G/k1G/kOOQO,5:l,5:lOOQOAN>ZAN>ZOJeQPOAN>ZOOQO-E8O-E8OOOQOG23uG23uP<cQPO'#EQO$wQPO,59XO$wQPO,59WO$wQPO,59VO$wQPO,59TO$wQPO,59SO$wQPO,59ROJoQPO1G.rOKVQPO'#ClOKjQPO'#CkOL^QPO'#ChOM^QPO'#CgONaQPO'#CfO$wQPO,59WO$wQPO,59UO$wQPO,59QO! jQPO1G.qO!!QQPO'#CkO!#_QPO'#CiO$wQPO,59VO$wQPO,59UO$wQPO,59TO$wQPO,59SO$wQPO,59RO$wQPO,59QO$wQPO,58zO!$[QPO1G.oO!$rQPO1G.nO!%YQPO1G.mO!%zQPO'#CiO!&rQPO'#ChO!'gQPO'#CgO!(XQPO'#CfO!(vQPO'#C`",
stateData: "!)Y~O!wOSPOS~OUPOgQOhQOiQOjQOkQOlQOnROoROpROqROsSOwTO{UO!PVO!WWO!]XO!`]O!a]O~OneXoeXpeXqeX!PeX!aeX!beX!ceX!deX!eeX!geX!heX!ieX!jeX!keX!leX!meX~OVlOUeXWTXgeXheXieXjeXkeXleXseXteXweX{eX!WeX!]eX!`eX!ueX~P!gOUYOgQOhQOiQOjQOkQOlQOnROoROpROqROsSOwTO{UO!PVO!WWO!]XO!`]O!a]O~OUoOupO~OyrO~P$wOUoO}|P~OUwO!TyO~P]Og}O~OV!QO!P!OOUcXgcXhcXicXjcXkcXlcXncXocXpcXqcXscXtcXwcX{cX!WcX!]cX!`cX!acX!bcX!ccX!dcX!ecX!gcX!hcX!icX!jcX!kcX!lcX!mcX!ucXxcXycX!TcXucX!ScX~O!b!SO!c!SOUaXgaXhaXiaXjaXkaXlaXnaXoaXpaXqaXsaXtaXwaX{aX!PaX!WaX!]aX!`aX!aaX!daX!eaX!gaX!haX!iaX!jaX!kaX!laX!maX!uaXVaXxaXyaX!TaXuaX!SaX~OU`Xg`Xh`Xi`Xj`Xk`Xl`Xn`Xo`Xp`Xq`Xs`Xt`Xw`X{`X!P`X!W`X!]`X!``X!e`X!g`X!h`X!i`X!j`X!k`X!l`X!m`X!u`Xx`Xy`X!T`Xu`X!S`X~O!a!TO!d!TO~P,OOn!VOo!VOp!VOq!VO!g!VO!h!VOU_Xg_Xh_Xi_Xj_Xk_Xl_Xs_Xt_Xw_X{_X!P_X!W_X!]_X!`_X!a_X!i_X!j_X!k_X!l_X!m_X!u_X~O!e!UO~P.cOU]Xg]Xh]Xi]Xj]Xk]Xl]Xn]Xo]Xp]Xq]Xs]Xt]Xw]X{]X!P]X!W]X!]]X!`]X!a]X!j]X!k]X!l]X!m]X!u]X~O!i!XO~P0dOU[Xg[Xh[Xi[Xj[Xk[Xl[Xn[Xo[Xp[Xq[Xs[Xt[Xw[X{[X!P[X!W[X!][X!`[X!a[X!k[X!l[X!m[X!u[X~O!j!YO~P2[OUZXgZXhZXiZXjZXkZXlZXnZXoZXpZXqZXsZXtZXwZX{ZX!PZX!WZX!]ZX!`ZX!aZX!lZX!mZX!uZX~O!k!ZO~P4POUYXgYXhYXiYXjYXkYXlYXnYXoYXpYXqYXsYXtYXwYX{YX!PYX!WYX!]YX!`YX!aYX!uYX~O!l![O!m!]O~P5qOW!^O~Ot!_OURXgRXhRXiRXjRXkRXlRXnRXoRXpRXqRXsRXwRX{RX!PRX!WRX!]RX!`RX!aRX!uRX~OU!aO~OVlOWTa~OVlOWTX~Ot!cOu!eO~Ox!fOy!hO~Ot!iO~OUoO}|X~O}!kO~O!S!lOVeX!TeX~P!gOx!mO!T!QX~O!U!oO~O!T!pO~O!T!qO~Os!rO~O!T!tO~P$wOU!vO~OUoOu#YO~Ot#ZOu#YO~Oy#^O~P$wOx#_Oy#^O~OU#cO!T!Qa~Ox#eO!T!Qa~O!U#hO~Ou#kO!Z#iO~P$wOx#mO!T!_X~O!T#oO~O!b!SO!c!SOUaigaihaiiaijaikailainaioaipaiqaisaitaiwai{ai!Pai!Wai!]ai!`ai!aai!dai!eai!gai!hai!iai!jai!kai!lai!mai!uaixaiyai!Taiuai!Sai~O!d!TOn`io`ip`iq`is`it`i!e`i!g`i!h`i!i`i!j`i!k`i!l`i!m`i~OU`ig`ih`ii`ij`ik`il`iw`i{`i!P`i!W`i!]`i!``i!a`i!u`i~P?^Os_it_i!i_i!j_i!k_i!l_i!m_i~O!e!UOU_ig_ih_ii_ij_ik_il_in_io_ip_iq_iw_i{_i!P_i!W_i!]_i!`_i!a_i!u_i~PAbOs]it]i!j]i!k]i!l]i!m]i~O!i!XOU]ig]ih]ii]ij]ik]il]in]io]ip]iq]iw]i{]i!P]i!W]i!]]i!`]i!a]i!u]i~PC]Os[it[i!k[i!l[i!m[i~O!j!YOU[ig[ih[ii[ij[ik[il[in[io[ip[iq[iw[i{[i!P[i!W[i!][i!`[i!a[i!u[i~PETO!k!ZOUZigZihZiiZijZikZilZinZioZipZiqZisZitZiwZi{Zi!PZi!WZi!]Zi!`Zi!aZi!lZi!mZi!uZi~OUoOu#pO~Oy#rO~P$wO!S!lO~OU#cO!T!Qi~O!S#vO~Ot#wOu#yO~O!T!_a~P$wOx#zO!T!_a~OUoO~OU#cO~Ou#}O!Z#iO~P$wOt$OOu#}O~O!T!_i~P$wOu$QO!Z#iO~P$wO!a!TOx`iy`i!T`iu`i!S`i~P?^O!a$SO!d$SOV`X!b`X!c`X~P,OO!e$TOV_X!b_X!c_X!d_Xx_Xy_X!T_Xu_X!S_X~P.cO!j$VOV[X!b[X!c[X!d[X!e[X!g[X!h[X!i[Xx[Xy[X!T[Xu[X!S[X~P2[O!k$WOVZX!bZX!cZX!dZX!eZX!gZX!hZX!iZX!jZXxZXyZX!TZXuZX!SZX~P4PO!l$XO!m$bOVYX!bYX!cYX!dYX!eYX!gYX!hYX!iYX!jYX!kYXxYXyYX!TYXuYX!SYX~P5qO!e$`Ox_iy_i!T_iu_i!S_i~PAbOn!VOo!VOp!VOq!VO!e$`O!g!VO!h!VOx_Xy_X!i_X!j_X!k_X!l_X!m_X!T_Xs_Xt_Xu_X!S_X~O!i$aOV]X!b]X!c]X!d]X!e]X!g]X!h]Xx]Xy]X!T]Xu]X!S]X~P0dO!i$gOx]iy]i!T]iu]i!S]i~PC]O!j$hOx[iy[i!T[iu[i!S[i~PETO!k$iOxZiyZi!lZi!mZi!TZisZitZiuZi!SZi~O!i$gOx]Xy]X!j]X!k]X!l]X!m]X!T]Xs]Xt]Xu]X!S]X~O!j$hOx[Xy[X!k[X!l[X!m[X!T[Xs[Xt[Xu[X!S[X~O!k$iOxZXyZX!lZX!mZX!TZXsZXtZXuZX!SZX~O!l$jO!m$kOxYXyYX!TYXsYXtYXuYX!SYX~OW$lO~O{}!W!]!mjkih!ZUj~",
goto: "3l!uPPP!v!z#[PPP#h$m%]%}&x'v(w)s*z,S-Y.b/f0jPPPPPP0jPPPP0jPPP0jPPP0jP1nP0jP1q1tPPP0jP1|2UP0jP2[2_PPPPPP2bPPPPPPP2k2q2x3O3Y3`3fTjOkSiOkQqSStUuV#X!c#Z#qShOk]$tSUu!c#Z#qSiOkQnRQsTQ{VQ|WQ!s!OS#Q!^$lY#]!f#_#m#s#zQ#a!kQ#b!lQ#g!oW#i!r#w$O$RQ#u#hR#{#v!OgORTVWk!O!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$lV#P!]$b$kWfOk!]!^Y$_R!k!o#h$bs$sTVW!O!f!l!r#_#m#s#v#w#z$O$R$k$lWeOk!]!^Q#O![Q#W$XY$^R!k!o#h$bQ$o$js$rTVW!O!f!l!r#_#m#s#v#w#z$O$R$k$lYdOk![!]!^Q!}!ZQ#V$W[$]R!k!o#h$X$bQ$n$iu$qTVW!O!f!l!r#_#m#s#v#w#z$O$R$j$k$l[cOk!Z![!]!^Q!|!YQ#U$V^$eR!k!o#h$W$X$bQ$m$hw$pTVW!O!f!l!r#_#m#s#v#w#z$O$R$i$j$k$l!hbORTVWk!O!Y!Z![!]!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$V$W$X$b$h$i$j$k$lV!{!X$a$g`aOk!X!Y!Z![!]!^Q!z!WQ#T$Ub$[R!k!o#h$V$W$X$a$bQ$c$f{$dTVW!O!f!l!r#_#m#s#v#w#z$O$R$g$h$i$j$k$l!``OTVWk!O!W!X!Y!Z![!]!^!f!l!r#_#m#s#v#w#z$O$R$f$g$h$i$j$k$lQ!y!UQ#S$TQ$Y$`e$ZR!k!o#h$U$V$W$X$a$b!z_ORTVWk!O!U!W!X!Y!Z![!]!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$T$U$V$W$X$`$a$b$f$g$h$i$j$k$lQ!x!TR#R$S#O^ORTVWk!O!T!U!W!X!Y!Z![!]!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$S$T$U$V$W$X$`$a$b$f$g$h$i$j$k$lQ!R]R!w!S#T[ORTVW]k!O!S!T!U!W!X!Y!Z![!]!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$S$T$U$V$W$X$`$a$b$f$g$h$i$j$k$l#TZORTVW]k!O!S!T!U!W!X!Y!Z![!]!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$S$T$U$V$W$X$`$a$b$f$g$h$i$j$k$l#TYORTVW]k!O!S!T!U!W!X!Y!Z![!]!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$S$T$U$V$W$X$`$a$b$f$g$h$i$j$k$lRvURzVQxVV#d!m#e#tQ#l!rV#|#w$O$RX#j!r#w$O$RR!P[R!u!OQ!WaQ$U$[R$f$dQkOR!`kSmPoR!bmQ!dqR#[!dQ!gsS#`!g#nR#n!sQuUR!juQ!nxR#f!nQ#x#lR$P#x",
nodeNames: "⚠ Comment Source Statement FieldDefinition FieldPath Identifier Dot Equal Expression DefaultExpression PatchExpression ComposeExpression LogicalOrExpression LogicalAndExpression ComparisonExpression ConcatExpression AdditiveExpression MultiplicativeExpression UnaryExpression PostfixExpression PrimaryExpression Literal String Integer Float True False Regex ComparisonConstraint Lt Lte Gt Gte Object LBrace Semicolon RBrace Array LBracket Comma RBracket LetExpression Let FieldDefinitionList In FunctionExpression LParen ParameterList Parameter Colon RParen Arrow MatchExpression Match MatchArm Pattern Underscore ImportExpression Import CallSuffix ArgumentList Bang Minus Star Slash Plus PlusPlus CompareOperator EqualEqual BangEqual AmpAmp PipePipe Amp SlashSlash Default",
maxTerm: 85,
states: "8fQYQPOOO#nQPO'#CaOOQO'#Cr'#CrO$wQPO'#CyO&UQPO'#DOO&^QQO'#DXO&hQPO'#DYO&pQPO'#CqO$wQPO'#DeO&zQPO'#DjOOQO'#Cq'#CqOOQO'#Cp'#CpO'PQPO'#CoO$wQPO'#CoOOQO'#Cn'#CnO)iQPO'#CmO.[QPO'#ClO0`QPO'#CkOOQO'#Cj'#CjO2WQPO'#CiO3{QPO'#ChO5mQPO'#CgO7XQPO'#CfOOQO'#Ce'#CeO7cQPO'#C`O7hQPO'#C_OOQO'#D|'#D|QYQPOOO8{QPO'#D}O9QQPO,58{OOQO,59e,59eO9YQPO'#CaOOQO,59j,59jO9bQPO,59jO$wQPO,59nOOQO,59s,59sO9jQPO,59sO9rQPO'#EQO9wQPO'#D[O:PQPO,59tO:UQPO'#CqO:cQPO'#D`O:kQQO,59xO:pQPO,59xO:uQPO,59]O:zQPO,5:POOQO,5:U,5:UO;PQPO'#DlOOQO,59[,59[O;WQPO,59[OOQO,59Z,59ZO$wQPO,59YO$wQPO,59XO$wQPO,59WOOQO'#Dt'#DtO$wQPO,59VO$wQPO,59UO$wQPO,59TO$wQPO,59SO$wQPO,59RO$wQPO,59QO$wQPO,58zOOQO,58y,58yOOQO-E7z-E7zOOQO,5:i,5:iOOQO-E7{-E7{O;]QPO1G/UO;eQPO1G/UOOQO1G/U1G/UO;mQPO1G/YO;uQPO1G/_O;|QPO1G/_OOQO1G/_1G/_OOQO,5:l,5:lOOQO-E8O-E8OO$wQPO1G/`O$wQPO,59{O<UQPO,59zO<^QPO,59zO$wQPO1G/dO<fQQO1G/dOOQO1G.w1G.wO<kQPO1G/kO<uQPO'#DmOOQO,5:W,5:WO<}QPO,5:WOOQO1G.v1G.vOOQO1G.t1G.tO=SQPO1G.sO@jQPO1G.rOBUQPO1G.qOOQO1G.p1G.pOC|QPO1G.oOEqQPO1G.nOGTQPO1G.mOOQO1G.l1G.lOOQO1G.f1G.fOOQO1G.s1G.sOOQO1G.r1G.rOOQO1G.q1G.qOOQO1G.o1G.oOOQO1G.n1G.nOOQO1G.m1G.mOOQO,5:j,5:jOOQO7+$p7+$pOHqQPO7+$pOOQO-E7|-E7|OOQO7+$t7+$tOHyQPO7+$tOOQO,5:k,5:kOOQO7+$y7+$yOIOQPO7+$yOOQO-E7}-E7}OOQO7+$z7+$zOOQO1G/g1G/gOIVQPO'#DaOOQO,5:m,5:mOI[QPO1G/fOOQO-E8P-E8POOQO7+%O7+%OO$wQPO7+%OOOQO'#Dh'#DhOIdQPO'#DgOOQO7+%V7+%VOIiQPO7+%VOIqQPO,5:XOIxQPO,5:XOOQO1G/r1G/rOOQO<<H[<<H[PJQQPO'#EOOOQO<<H`<<H`OOQO<<He<<HeP$wQPO'#EPPJVQPO'#EROOQO<<Hj<<HjO$wQPO,5:ROJ[QPO<<HqOJfQPO<<HqOOQO<<Hq<<HqOJnQPO1G/sOOQO1G/m1G/mOOQO,5:n,5:nOOQOAN>]AN>]OJuQPOAN>]OOQO-E8Q-E8QOOQOG23wG23wP<nQPO'#ESO$wQPO,59XO$wQPO,59WO$wQPO,59VO$wQPO,59TO$wQPO,59SO$wQPO,59ROKPQPO1G.rOKgQPO'#ClOKzQPO'#CkOLnQPO'#ChOMnQPO'#CgONqQPO'#CfO$wQPO,59WO$wQPO,59UO$wQPO,59QO! zQPO1G.qO!!bQPO'#CkO!#oQPO'#CiO$wQPO,59VO$wQPO,59UO$wQPO,59TO$wQPO,59SO$wQPO,59RO$wQPO,59QO$wQPO,58zO!$lQPO1G.oO!%SQPO1G.nO!%jQPO1G.mO!&[QPO'#CiO!'SQPO'#ChO!'wQPO'#CgO!(iQPO'#CfO!)WQPO'#C`",
stateData: "!)j~O!yOSPOS~OUPOgQOhQOiQOjQOkQOlQOnROoROpROqROsSOwTO}UO!RVO!YWO!_XO!b]O!c]O~OneXoeXpeXqeX!ReX!ceX!deX!eeX!feX!geX!ieX!jeX!keX!leX!meX!neX!oeX~OVlOUeXWTXgeXheXieXjeXkeXleXseXteXweX}eX!YeX!_eX!beX!weX~P!gOUYOgQOhQOiQOjQOkQOlQOnROoROpROqROsSOwTO}UO!RVO!YWO!_XO!b]O!c]O~OUoOupO~OxrOzsO~P$wOUoO!P!OP~OUxO!VzO~P]Og!OO~OV!RO!R!POUcXgcXhcXicXjcXkcXlcXncXocXpcXqcXscXtcXwcX}cX!YcX!_cX!bcX!ccX!dcX!ecX!fcX!gcX!icX!jcX!kcX!lcX!mcX!ncX!ocX!wcXycXzcX!VcXucX!UcX~O!d!TO!e!TOUaXgaXhaXiaXjaXkaXlaXnaXoaXpaXqaXsaXtaXwaX}aX!RaX!YaX!_aX!baX!caX!faX!gaX!iaX!jaX!kaX!laX!maX!naX!oaX!waXVaXyaXzaX!VaXuaX!UaX~OU`Xg`Xh`Xi`Xj`Xk`Xl`Xn`Xo`Xp`Xq`Xs`Xt`Xw`X}`X!R`X!Y`X!_`X!b`X!g`X!i`X!j`X!k`X!l`X!m`X!n`X!o`X!w`Xy`Xz`X!V`Xu`X!U`X~O!c!UO!f!UO~P,ROn!WOo!WOp!WOq!WO!i!WO!j!WOU_Xg_Xh_Xi_Xj_Xk_Xl_Xs_Xt_Xw_X}_X!R_X!Y_X!__X!b_X!c_X!k_X!l_X!m_X!n_X!o_X!w_X~O!g!VO~P.fOU]Xg]Xh]Xi]Xj]Xk]Xl]Xn]Xo]Xp]Xq]Xs]Xt]Xw]X}]X!R]X!Y]X!_]X!b]X!c]X!l]X!m]X!n]X!o]X!w]X~O!k!YO~P0gOU[Xg[Xh[Xi[Xj[Xk[Xl[Xn[Xo[Xp[Xq[Xs[Xt[Xw[X}[X!R[X!Y[X!_[X!b[X!c[X!m[X!n[X!o[X!w[X~O!l!ZO~P2_OUZXgZXhZXiZXjZXkZXlZXnZXoZXpZXqZXsZXtZXwZX}ZX!RZX!YZX!_ZX!bZX!cZX!nZX!oZX!wZX~O!m![O~P4SOUYXgYXhYXiYXjYXkYXlYXnYXoYXpYXqYXsYXtYXwYX}YX!RYX!YYX!_YX!bYX!cYX!wYX~O!n!]O!o!^O~P5tOW!_O~Ot!`OURXgRXhRXiRXjRXkRXlRXnRXoRXpRXqRXsRXwRX}RX!RRX!YRX!_RX!bRX!cRX!wRX~OU!bO~OVlOWTa~OVlOWTX~Ot!dOu!fO~Oy!hOz!jO~Ot!kO~OUoO!P!OX~O!P!mO~O!U!nOVeX!VeX~P!gOy!oO!V!SX~O!W!qO~O!V!rO~O!V!sO~Os!tO~O!V!vO~P$wOU!xO~OUoOu#[O~Ot#]Ou#[O~Oy#`Oz#_O~Oz#bO~P$wOy#cOz#bO~OU#gO!V!Sa~Oy#iO!V!Sa~O!W#lO~Ou#oO!]#mO~P$wOy#qO!V!aX~O!V#sO~O!d!TO!e!TOUaigaihaiiaijaikailainaioaipaiqaisaitaiwai}ai!Rai!Yai!_ai!bai!cai!fai!gai!iai!jai!kai!lai!mai!nai!oai!waiyaizai!Vaiuai!Uai~O!f!UOn`io`ip`iq`is`it`i!g`i!i`i!j`i!k`i!l`i!m`i!n`i!o`i~OU`ig`ih`ii`ij`ik`il`iw`i}`i!R`i!Y`i!_`i!b`i!c`i!w`i~P?iOs_it_i!k_i!l_i!m_i!n_i!o_i~O!g!VOU_ig_ih_ii_ij_ik_il_in_io_ip_iq_iw_i}_i!R_i!Y_i!__i!b_i!c_i!w_i~PAmOs]it]i!l]i!m]i!n]i!o]i~O!k!YOU]ig]ih]ii]ij]ik]il]in]io]ip]iq]iw]i}]i!R]i!Y]i!_]i!b]i!c]i!w]i~PChOs[it[i!m[i!n[i!o[i~O!l!ZOU[ig[ih[ii[ij[ik[il[in[io[ip[iq[iw[i}[i!R[i!Y[i!_[i!b[i!c[i!w[i~PE`O!m![OUZigZihZiiZijZikZilZinZioZipZiqZisZitZiwZi}Zi!RZi!YZi!_Zi!bZi!cZi!nZi!oZi!wZi~OUoOu#tO~Oz#vO~Oz#wO~P$wO!U!nO~OU#gO!V!Si~O!U#{O~Ot#|Ou$OO~O!V!aa~P$wOy$PO!V!aa~OUoO~OU#gO~Ou$SO!]#mO~P$wOt$TOu$SO~O!V!ai~P$wOu$VO!]#mO~P$wO!c!UOy`iz`i!V`iu`i!U`i~P?iO!c$XO!f$XOV`X!d`X!e`X~P,RO!g$YOV_X!d_X!e_X!f_Xy_Xz_X!V_Xu_X!U_X~P.fO!l$[OV[X!d[X!e[X!f[X!g[X!i[X!j[X!k[Xy[Xz[X!V[Xu[X!U[X~P2_O!m$]OVZX!dZX!eZX!fZX!gZX!iZX!jZX!kZX!lZXyZXzZX!VZXuZX!UZX~P4SO!n$^O!o$gOVYX!dYX!eYX!fYX!gYX!iYX!jYX!kYX!lYX!mYXyYXzYX!VYXuYX!UYX~P5tO!g$eOy_iz_i!V_iu_i!U_i~PAmOn!WOo!WOp!WOq!WO!g$eO!i!WO!j!WOy_Xz_X!k_X!l_X!m_X!n_X!o_X!V_Xs_Xt_Xu_X!U_X~O!k$fOV]X!d]X!e]X!f]X!g]X!i]X!j]Xy]Xz]X!V]Xu]X!U]X~P0gO!k$lOy]iz]i!V]iu]i!U]i~PChO!l$mOy[iz[i!V[iu[i!U[i~PE`O!m$nOyZizZi!nZi!oZi!VZisZitZiuZi!UZi~O!k$lOy]Xz]X!l]X!m]X!n]X!o]X!V]Xs]Xt]Xu]X!U]X~O!l$mOy[Xz[X!m[X!n[X!o[X!V[Xs[Xt[Xu[X!U[X~O!m$nOyZXzZX!nZX!oZX!VZXsZXtZXuZX!UZX~O!n$oO!o$pOyYXzYX!VYXsYXtYXuYX!UYX~OW$qO~O}!P!Y!_!ojkih!]Uj~",
goto: "4O!wPPP!x!|#^PPP#j$r%c&U'Q(P)R*O+W,a-h.q/v0{PPPPPP0{PPPP0{PPP0{PPPP0{0{P2QP0{P2T2WPPP0{P2`2hP0{P2n2qPPPPPP2tPPPPPPP2}3T3[3b3l3r3xTjOkSiOkQqSSuUvV#Z!d#]#uShOk]$ySUv!d#]#uSiOkQnRQtTQ|VQ}WQ!grQ!u!PS#S!_$qY#a!h#c#q#x$PQ#e!mQ#f!nQ#k!qW#m!t#|$T$WQ#z#lR$Q#{!QgORTVWkr!P!_!h!m!n!q!t#c#l#q#x#{#|$P$T$W$qV#R!^$g$pWfOk!^!_Y$dR!m!q#l$gu$xTVWr!P!h!n!t#c#q#x#{#|$P$T$W$p$qWeOk!^!_Q#Q!]Q#Y$^Y$cR!m!q#l$gQ$t$ou$wTVWr!P!h!n!t#c#q#x#{#|$P$T$W$p$qYdOk!]!^!_Q#P![Q#X$][$bR!m!q#l$^$gQ$s$nw$vTVWr!P!h!n!t#c#q#x#{#|$P$T$W$o$p$q[cOk![!]!^!_Q#O!ZQ#W$[^$jR!m!q#l$]$^$gQ$r$my$uTVWr!P!h!n!t#c#q#x#{#|$P$T$W$n$o$p$q!jbORTVWkr!P!Z![!]!^!_!h!m!n!q!t#c#l#q#x#{#|$P$T$W$[$]$^$g$m$n$o$p$qV!}!Y$f$l`aOk!Y!Z![!]!^!_Q!|!XQ#V$Zb$aR!m!q#l$[$]$^$f$gQ$h$k}$iTVWr!P!h!n!t#c#q#x#{#|$P$T$W$l$m$n$o$p$q!b`OTVWkr!P!X!Y!Z![!]!^!_!h!n!t#c#q#x#{#|$P$T$W$k$l$m$n$o$p$qQ!{!VQ#U$YQ$_$ee$`R!m!q#l$Z$[$]$^$f$g!|_ORTVWkr!P!V!X!Y!Z![!]!^!_!h!m!n!q!t#c#l#q#x#{#|$P$T$W$Y$Z$[$]$^$e$f$g$k$l$m$n$o$p$qQ!z!UR#T$X#Q^ORTVWkr!P!U!V!X!Y!Z![!]!^!_!h!m!n!q!t#c#l#q#x#{#|$P$T$W$X$Y$Z$[$]$^$e$f$g$k$l$m$n$o$p$qQ!S]R!y!T#V[ORTVW]kr!P!T!U!V!X!Y!Z![!]!^!_!h!m!n!q!t#c#l#q#x#{#|$P$T$W$X$Y$Z$[$]$^$e$f$g$k$l$m$n$o$p$q#VZORTVW]kr!P!T!U!V!X!Y!Z![!]!^!_!h!m!n!q!t#c#l#q#x#{#|$P$T$W$X$Y$Z$[$]$^$e$f$g$k$l$m$n$o$p$q#VYORTVW]kr!P!T!U!V!X!Y!Z![!]!^!_!h!m!n!q!t#c#l#q#x#{#|$P$T$W$X$Y$Z$[$]$^$e$f$g$k$l$m$n$o$p$qRwUR{VQyVV#h!o#i#yQ#p!tV$R#|$T$WX#n!t#|$T$WR!Q[R!w!PQ!XaQ$Z$aR$k$iQkOR!akSmPoR!cmQ!eqR#^!eQ!itS#d!i#rR#r!uQvUR!lvQ!pyR#j!pQ#}#pR$U#}",
nodeNames: "⚠ Comment Source Statement FieldDefinition FieldPath Identifier Dot Equal Expression DefaultExpression PatchExpression ComposeExpression LogicalOrExpression LogicalAndExpression ComparisonExpression ConcatExpression AdditiveExpression MultiplicativeExpression UnaryExpression PostfixExpression PrimaryExpression Literal String Integer Float True False Regex ComparisonConstraint Lt Lte Gt Gte Object LBrace Semicolon RBrace ArrayConstraint LBracket Ellipsis Comma RBracket Array LetExpression Let FieldDefinitionList In FunctionExpression LParen ParameterList Parameter Colon RParen Arrow MatchExpression Match MatchArm Pattern Underscore ImportExpression Import CallSuffix ArgumentList Bang Minus Star Slash Plus PlusPlus CompareOperator EqualEqual BangEqual AmpAmp PipePipe Amp SlashSlash Default",
maxTerm: 87,
skippedNodes: [0,1],
repeatNodeCount: 7,
tokenData: "=T~R!PX^$Upq$Uqr$yrs%Wst&zvw'cxy'pyz'uz{'z{|(P|}(^}!O(c!O!P(h!P!Q(m!Q![+T![!]+n!]!^+s!^!_+x!_!`,V!`!a,l!c!},y!}#O-[#P#Q-a#R#S-f#T#W,y#W#X-y#X#Y,y#Y#Z1Z#Z#],y#]#^3k#^#`,y#`#a6}#a#b8b#b#h,y#h#i:r#i#o,y#o#p<n#p#q<s#q#r=O#y#z$U$f$g$U#BY#BZ$U$IS$I_$U$I|$JO$U$JT$JU$U$KV$KW$U&FU&FV$U~$ZY!w~X^$Upq$U#y#z$U$f$g$U#BY#BZ$U$IS$I_$U$I|$JO$U$JT$JU$U$KV$KW$U&FU&FV$U~%OP!`~!_!`%R~%WO!h~~%ZWOY%WZr%Wrs%ss#O%W#O#P%x#P;'S%W;'S;=`&t<%lO%W~%xOg~~%{RO;'S%W;'S;=`&U;=`O%W~&XXOY%WZr%Wrs%ss#O%W#O#P%x#P;'S%W;'S;=`&t;=`<%l%W<%lO%W~&wP;=`<%l%W~'PSP~OY&zZ;'S&z;'S;=`']<%lO&z~'`P;=`<%l&z~'hP!k~vw'k~'pO!i~~'uO!P~~'zO!T~~(PO!b~~(UP!d~{|(X~(^O!e~~(cOx~~(hO!a~~(mOV~~(rW!c~OY)[Z!P)[!P!Q+O!Q#O)[#O#P)|#P;'S)[;'S;=`*x<%lO)[~)_WOY)[Z!P)[!P!Q)w!Q#O)[#O#P)|#P;'S)[;'S;=`*x<%lO)[~)|Ol~~*PRO;'S)[;'S;=`*Y;=`O)[~*]XOY)[Z!P)[!P!Q)w!Q#O)[#O#P)|#P;'S)[;'S;=`*x;=`<%l)[<%lO)[~*{P;=`<%l)[~+TO!l~~+YQh~!O!P+`!Q![+T~+cP!Q![+f~+kPi~!Q![+f~+sO!S~~+xOt~~+}Pn~!_!`,Q~,VOo~~,[QWP!_!`,b!`!a,g~,gO!g~Q,lO!UQ~,qPp~!_!`,t~,yOq~~-OSU~!Q![,y!c!},y#R#S,y#T#o,y~-aOw~~-fOy~~-mS!Z~U~!Q![,y!c!},y#R#S,y#T#o,y~.OUU~!Q![,y!c!},y#R#S,y#T#X,y#X#Y.b#Y#o,y~.gUU~!Q![,y!c!},y#R#S,y#T#Y,y#Y#Z.y#Z#o,y~/OTU~!Q![,y!c!},y#R#S,y#T#U/_#U#o,y~/dUU~!Q![,y!c!},y#R#S,y#T#i,y#i#j/v#j#o,y~/{UU~!Q![,y!c!},y#R#S,y#T#`,y#`#a0_#a#o,y~0dUU~!Q![,y!c!},y#R#S,y#T#h,y#h#i0v#i#o,y~0}S!m~U~!Q![,y!c!},y#R#S,y#T#o,y~1`TU~!Q![,y!c!},y#R#S,y#T#U1o#U#o,y~1tUU~!Q![,y!c!},y#R#S,y#T#`,y#`#a2W#a#o,y~2]UU~!Q![,y!c!},y#R#S,y#T#g,y#g#h2o#h#o,y~2tUU~!Q![,y!c!},y#R#S,y#T#X,y#X#Y3W#Y#o,y~3_Sk~U~!Q![,y!c!},y#R#S,y#T#o,y~3pVU~!Q![,y!c!},y#R#S,y#T#a,y#a#b4V#b#c6j#c#o,y~4[UU~!Q![,y!c!},y#R#S,y#T#d,y#d#e4n#e#o,y~4sUU~!Q![,y!c!},y#R#S,y#T#c,y#c#d5V#d#o,y~5[UU~!Q![,y!c!},y#R#S,y#T#f,y#f#g5n#g#o,y~5sUU~!Q![,y!c!},y#R#S,y#T#h,y#h#i6V#i#o,y~6^S!]~U~!Q![,y!c!},y#R#S,y#T#o,y~6qS}~U~!Q![,y!c!},y#R#S,y#T#o,y~7SUU~!Q![,y!c!},y#R#S,y#T#X,y#X#Y7f#Y#o,y~7kUU~!Q![,y!c!},y#R#S,y#T#h,y#h#i7}#i#o,y~8US{~U~!Q![,y!c!},y#R#S,y#T#o,y~8gTU~!Q![,y!c!},y#R#S,y#T#U8v#U#o,y~8{UU~!Q![,y!c!},y#R#S,y#T#h,y#h#i9_#i#o,y~9dUU~!Q![,y!c!},y#R#S,y#T#V,y#V#W9v#W#o,y~9{UU~!Q![,y!c!},y#R#S,y#T#[,y#[#]:_#]#o,y~:fS!W~U~!Q![,y!c!},y#R#S,y#T#o,y~:wUU~!Q![,y!c!},y#R#S,y#T#f,y#f#g;Z#g#o,y~;`UU~!Q![,y!c!},y#R#S,y#T#i,y#i#j;r#j#o,y~;wUU~!Q![,y!c!},y#R#S,y#T#X,y#X#Y<Z#Y#o,y~<bSj~U~!Q![,y!c!},y#R#S,y#T#o,y~<sOs~~<vP#p#q<y~=OO!j~~=TOu~",
tokenData: "=c~R!PX^$Upq$Uqr$yrs%Wst&zvw'cxy'pyz'uz{'z{|(P|}(^}!O(c!O!P(h!P!Q({!Q![+c![!]+|!]!^,R!^!_,W!_!`,e!`!a,z!c!}-X!}#O-j#P#Q-o#R#S-t#T#W-X#W#X.X#X#Y-X#Y#Z1i#Z#]-X#]#^3y#^#`-X#`#a7]#a#b8p#b#h-X#h#i;Q#i#o-X#o#p<|#p#q=R#q#r=^#y#z$U$f$g$U#BY#BZ$U$IS$I_$U$I|$JO$U$JT$JU$U$KV$KW$U&FU&FV$U~$ZY!y~X^$Upq$U#y#z$U$f$g$U#BY#BZ$U$IS$I_$U$I|$JO$U$JT$JU$U$KV$KW$U&FU&FV$U~%OP!b~!_!`%R~%WO!j~~%ZWOY%WZr%Wrs%ss#O%W#O#P%x#P;'S%W;'S;=`&t<%lO%W~%xOg~~%{RO;'S%W;'S;=`&U;=`O%W~&XXOY%WZr%Wrs%ss#O%W#O#P%x#P;'S%W;'S;=`&t;=`<%l%W<%lO%W~&wP;=`<%l%W~'PSP~OY&zZ;'S&z;'S;=`']<%lO&z~'`P;=`<%l&z~'hP!m~vw'k~'pO!k~~'uO!R~~'zO!V~~(PO!d~~(UP!f~{|(X~(^O!g~~(cOy~~(hO!c~R(mPVP!O!P(pQ(sP!O!P(vQ({OxQ~)QW!e~OY)jZ!P)j!P!Q+^!Q#O)j#O#P*[#P;'S)j;'S;=`+W<%lO)j~)mWOY)jZ!P)j!P!Q*V!Q#O)j#O#P*[#P;'S)j;'S;=`+W<%lO)j~*[Ol~~*_RO;'S)j;'S;=`*h;=`O)j~*kXOY)jZ!P)j!P!Q*V!Q#O)j#O#P*[#P;'S)j;'S;=`+W;=`<%l)j<%lO)j~+ZP;=`<%l)j~+cO!n~~+hQh~!O!P+n!Q![+c~+qP!Q![+t~+yPi~!Q![+t~,RO!U~~,WOt~~,]Pn~!_!`,`~,eOo~~,jQWP!_!`,p!`!a,u~,uO!i~Q,zO!WQ~-PPp~!_!`-S~-XOq~~-^SU~!Q![-X!c!}-X#R#S-X#T#o-X~-oOw~~-tOz~~-{S!]~U~!Q![-X!c!}-X#R#S-X#T#o-X~.^UU~!Q![-X!c!}-X#R#S-X#T#X-X#X#Y.p#Y#o-X~.uUU~!Q![-X!c!}-X#R#S-X#T#Y-X#Y#Z/X#Z#o-X~/^TU~!Q![-X!c!}-X#R#S-X#T#U/m#U#o-X~/rUU~!Q![-X!c!}-X#R#S-X#T#i-X#i#j0U#j#o-X~0ZUU~!Q![-X!c!}-X#R#S-X#T#`-X#`#a0m#a#o-X~0rUU~!Q![-X!c!}-X#R#S-X#T#h-X#h#i1U#i#o-X~1]S!o~U~!Q![-X!c!}-X#R#S-X#T#o-X~1nTU~!Q![-X!c!}-X#R#S-X#T#U1}#U#o-X~2SUU~!Q![-X!c!}-X#R#S-X#T#`-X#`#a2f#a#o-X~2kUU~!Q![-X!c!}-X#R#S-X#T#g-X#g#h2}#h#o-X~3SUU~!Q![-X!c!}-X#R#S-X#T#X-X#X#Y3f#Y#o-X~3mSk~U~!Q![-X!c!}-X#R#S-X#T#o-X~4OVU~!Q![-X!c!}-X#R#S-X#T#a-X#a#b4e#b#c6x#c#o-X~4jUU~!Q![-X!c!}-X#R#S-X#T#d-X#d#e4|#e#o-X~5RUU~!Q![-X!c!}-X#R#S-X#T#c-X#c#d5e#d#o-X~5jUU~!Q![-X!c!}-X#R#S-X#T#f-X#f#g5|#g#o-X~6RUU~!Q![-X!c!}-X#R#S-X#T#h-X#h#i6e#i#o-X~6lS!_~U~!Q![-X!c!}-X#R#S-X#T#o-X~7PS!P~U~!Q![-X!c!}-X#R#S-X#T#o-X~7bUU~!Q![-X!c!}-X#R#S-X#T#X-X#X#Y7t#Y#o-X~7yUU~!Q![-X!c!}-X#R#S-X#T#h-X#h#i8]#i#o-X~8dS}~U~!Q![-X!c!}-X#R#S-X#T#o-X~8uTU~!Q![-X!c!}-X#R#S-X#T#U9U#U#o-X~9ZUU~!Q![-X!c!}-X#R#S-X#T#h-X#h#i9m#i#o-X~9rUU~!Q![-X!c!}-X#R#S-X#T#V-X#V#W:U#W#o-X~:ZUU~!Q![-X!c!}-X#R#S-X#T#[-X#[#]:m#]#o-X~:tS!Y~U~!Q![-X!c!}-X#R#S-X#T#o-X~;VUU~!Q![-X!c!}-X#R#S-X#T#f-X#f#g;i#g#o-X~;nUU~!Q![-X!c!}-X#R#S-X#T#i-X#i#j<Q#j#o-X~<VUU~!Q![-X!c!}-X#R#S-X#T#X-X#X#Y<i#Y#o-X~<pSj~U~!Q![-X!c!}-X#R#S-X#T#o-X~=ROs~~=UP#p#q=X~=^O!l~~=cOu~",
tokenizers: [0, 1],
topRules: {"Source":[0,2]},
tokenPrec: 2481
tokenPrec: 2497
})
@@ -2,15 +2,15 @@
import {LRParser} from "@lezer/lr"
export const parser = LRParser.deserialize({
version: 14,
states: "7vQYQPOOO#nQPO'#CaOOQO'#Cr'#CrO$wQPO'#CyO&UQPO'#DOO&^QPO'#DSO&eQPO'#DWO&mQPO'#CqO$wQPO'#DcO&wQPO'#DhOOQO'#Cq'#CqOOQO'#Cp'#CpO&|QPO'#CoO$wQPO'#CoOOQO'#Cn'#CnO)fQPO'#CmO.XQPO'#ClO0]QPO'#CkOOQO'#Cj'#CjO2TQPO'#CiO3xQPO'#ChO5jQPO'#CgO7UQPO'#CfOOQO'#Ce'#CeO7`QPO'#C`O7eQPO'#C_OOQO'#Dz'#DzQYQPOOO8xQPO'#D{O8}QPO,58{OOQO,59e,59eO9VQPO'#CaOOQO,59j,59jO9_QPO,59jOOQO,59n,59nO9gQPO,59nO9oQPO'#EOO9tQPO'#DYO9|QPO,59rO:RQPO'#CqO:`QPO'#D^O:hQQO,59vO:mQPO,59vO:rQPO,59]O:wQPO,59}OOQO,5:S,5:SO:|QPO'#DjOOQO,59[,59[O;TQPO,59[OOQO,59Z,59ZO$wQPO,59YO$wQPO,59XO$wQPO,59WOOQO'#Dr'#DrO$wQPO,59VO$wQPO,59UO$wQPO,59TO$wQPO,59SO$wQPO,59RO$wQPO,59QO$wQPO,58zOOQO,58y,58yOOQO-E7x-E7xOOQO,5:g,5:gOOQO-E7y-E7yO;YQPO1G/UO;bQPO1G/UOOQO1G/U1G/UO;jQPO1G/YO;qQPO1G/YOOQO1G/Y1G/YOOQO,5:j,5:jOOQO-E7|-E7|O$wQPO1G/^O$wQPO,59yO;yQPO,59xO<RQPO,59xO$wQPO1G/bO<ZQQO1G/bOOQO1G.w1G.wO<`QPO1G/iO<jQPO'#DkOOQO,5:U,5:UO<rQPO,5:UOOQO1G.v1G.vOOQO1G.t1G.tO<wQPO1G.sO@_QPO1G.rOAyQPO1G.qOOQO1G.p1G.pOCqQPO1G.oOEfQPO1G.nOFxQPO1G.mOOQO1G.l1G.lOOQO1G.f1G.fOOQO1G.s1G.sOOQO1G.r1G.rOOQO1G.q1G.qOOQO1G.o1G.oOOQO1G.n1G.nOOQO1G.m1G.mOOQO,5:h,5:hOOQO7+$p7+$pOHfQPO7+$pOOQO-E7z-E7zOOQO,5:i,5:iOOQO7+$t7+$tOHnQPO7+$tOOQO-E7{-E7{OOQO7+$x7+$xOOQO1G/e1G/eOHuQPO'#D_OOQO,5:k,5:kOHzQPO1G/dOOQO-E7}-E7}OOQO7+$|7+$|O$wQPO7+$|OOQO'#Df'#DfOISQPO'#DeOOQO7+%T7+%TOIXQPO7+%TOIaQPO,5:VOIhQPO,5:VOOQO1G/p1G/pOOQO<<H[<<H[PIpQPO'#D|OOQO<<H`<<H`P$wQPO'#D}PIuQPO'#EPOOQO<<Hh<<HhO$wQPO,5:POIzQPO<<HoOJUQPO<<HoOOQO<<Ho<<HoOJ^QPO1G/qOOQO1G/k1G/kOOQO,5:l,5:lOOQOAN>ZAN>ZOJeQPOAN>ZOOQO-E8O-E8OOOQOG23uG23uP<cQPO'#EQO$wQPO,59XO$wQPO,59WO$wQPO,59VO$wQPO,59TO$wQPO,59SO$wQPO,59ROJoQPO1G.rOKVQPO'#ClOKjQPO'#CkOL^QPO'#ChOM^QPO'#CgONaQPO'#CfO$wQPO,59WO$wQPO,59UO$wQPO,59QO! jQPO1G.qO!!QQPO'#CkO!#_QPO'#CiO$wQPO,59VO$wQPO,59UO$wQPO,59TO$wQPO,59SO$wQPO,59RO$wQPO,59QO$wQPO,58zO!$[QPO1G.oO!$rQPO1G.nO!%YQPO1G.mO!%zQPO'#CiO!&rQPO'#ChO!'gQPO'#CgO!(XQPO'#CfO!(vQPO'#C`",
stateData: "!)Y~O!wOSPOS~OUPOgQOhQOiQOjQOkQOlQOnROoROpROqROsSOwTO{UO!PVO!WWO!]XO!`]O!a]O~OneXoeXpeXqeX!PeX!aeX!beX!ceX!deX!eeX!geX!heX!ieX!jeX!keX!leX!meX~OVlOUeXWTXgeXheXieXjeXkeXleXseXteXweX{eX!WeX!]eX!`eX!ueX~P!gOUYOgQOhQOiQOjQOkQOlQOnROoROpROqROsSOwTO{UO!PVO!WWO!]XO!`]O!a]O~OUoOupO~OyrO~P$wOUoO}|P~OUwO!TyO~P]Og}O~OV!QO!P!OOUcXgcXhcXicXjcXkcXlcXncXocXpcXqcXscXtcXwcX{cX!WcX!]cX!`cX!acX!bcX!ccX!dcX!ecX!gcX!hcX!icX!jcX!kcX!lcX!mcX!ucXxcXycX!TcXucX!ScX~O!b!SO!c!SOUaXgaXhaXiaXjaXkaXlaXnaXoaXpaXqaXsaXtaXwaX{aX!PaX!WaX!]aX!`aX!aaX!daX!eaX!gaX!haX!iaX!jaX!kaX!laX!maX!uaXVaXxaXyaX!TaXuaX!SaX~OU`Xg`Xh`Xi`Xj`Xk`Xl`Xn`Xo`Xp`Xq`Xs`Xt`Xw`X{`X!P`X!W`X!]`X!``X!e`X!g`X!h`X!i`X!j`X!k`X!l`X!m`X!u`Xx`Xy`X!T`Xu`X!S`X~O!a!TO!d!TO~P,OOn!VOo!VOp!VOq!VO!g!VO!h!VOU_Xg_Xh_Xi_Xj_Xk_Xl_Xs_Xt_Xw_X{_X!P_X!W_X!]_X!`_X!a_X!i_X!j_X!k_X!l_X!m_X!u_X~O!e!UO~P.cOU]Xg]Xh]Xi]Xj]Xk]Xl]Xn]Xo]Xp]Xq]Xs]Xt]Xw]X{]X!P]X!W]X!]]X!`]X!a]X!j]X!k]X!l]X!m]X!u]X~O!i!XO~P0dOU[Xg[Xh[Xi[Xj[Xk[Xl[Xn[Xo[Xp[Xq[Xs[Xt[Xw[X{[X!P[X!W[X!][X!`[X!a[X!k[X!l[X!m[X!u[X~O!j!YO~P2[OUZXgZXhZXiZXjZXkZXlZXnZXoZXpZXqZXsZXtZXwZX{ZX!PZX!WZX!]ZX!`ZX!aZX!lZX!mZX!uZX~O!k!ZO~P4POUYXgYXhYXiYXjYXkYXlYXnYXoYXpYXqYXsYXtYXwYX{YX!PYX!WYX!]YX!`YX!aYX!uYX~O!l![O!m!]O~P5qOW!^O~Ot!_OURXgRXhRXiRXjRXkRXlRXnRXoRXpRXqRXsRXwRX{RX!PRX!WRX!]RX!`RX!aRX!uRX~OU!aO~OVlOWTa~OVlOWTX~Ot!cOu!eO~Ox!fOy!hO~Ot!iO~OUoO}|X~O}!kO~O!S!lOVeX!TeX~P!gOx!mO!T!QX~O!U!oO~O!T!pO~O!T!qO~Os!rO~O!T!tO~P$wOU!vO~OUoOu#YO~Ot#ZOu#YO~Oy#^O~P$wOx#_Oy#^O~OU#cO!T!Qa~Ox#eO!T!Qa~O!U#hO~Ou#kO!Z#iO~P$wOx#mO!T!_X~O!T#oO~O!b!SO!c!SOUaigaihaiiaijaikailainaioaipaiqaisaitaiwai{ai!Pai!Wai!]ai!`ai!aai!dai!eai!gai!hai!iai!jai!kai!lai!mai!uaixaiyai!Taiuai!Sai~O!d!TOn`io`ip`iq`is`it`i!e`i!g`i!h`i!i`i!j`i!k`i!l`i!m`i~OU`ig`ih`ii`ij`ik`il`iw`i{`i!P`i!W`i!]`i!``i!a`i!u`i~P?^Os_it_i!i_i!j_i!k_i!l_i!m_i~O!e!UOU_ig_ih_ii_ij_ik_il_in_io_ip_iq_iw_i{_i!P_i!W_i!]_i!`_i!a_i!u_i~PAbOs]it]i!j]i!k]i!l]i!m]i~O!i!XOU]ig]ih]ii]ij]ik]il]in]io]ip]iq]iw]i{]i!P]i!W]i!]]i!`]i!a]i!u]i~PC]Os[it[i!k[i!l[i!m[i~O!j!YOU[ig[ih[ii[ij[ik[il[in[io[ip[iq[iw[i{[i!P[i!W[i!][i!`[i!a[i!u[i~PETO!k!ZOUZigZihZiiZijZikZilZinZioZipZiqZisZitZiwZi{Zi!PZi!WZi!]Zi!`Zi!aZi!lZi!mZi!uZi~OUoOu#pO~Oy#rO~P$wO!S!lO~OU#cO!T!Qi~O!S#vO~Ot#wOu#yO~O!T!_a~P$wOx#zO!T!_a~OUoO~OU#cO~Ou#}O!Z#iO~P$wOt$OOu#}O~O!T!_i~P$wOu$QO!Z#iO~P$wO!a!TOx`iy`i!T`iu`i!S`i~P?^O!a$SO!d$SOV`X!b`X!c`X~P,OO!e$TOV_X!b_X!c_X!d_Xx_Xy_X!T_Xu_X!S_X~P.cO!j$VOV[X!b[X!c[X!d[X!e[X!g[X!h[X!i[Xx[Xy[X!T[Xu[X!S[X~P2[O!k$WOVZX!bZX!cZX!dZX!eZX!gZX!hZX!iZX!jZXxZXyZX!TZXuZX!SZX~P4PO!l$XO!m$bOVYX!bYX!cYX!dYX!eYX!gYX!hYX!iYX!jYX!kYXxYXyYX!TYXuYX!SYX~P5qO!e$`Ox_iy_i!T_iu_i!S_i~PAbOn!VOo!VOp!VOq!VO!e$`O!g!VO!h!VOx_Xy_X!i_X!j_X!k_X!l_X!m_X!T_Xs_Xt_Xu_X!S_X~O!i$aOV]X!b]X!c]X!d]X!e]X!g]X!h]Xx]Xy]X!T]Xu]X!S]X~P0dO!i$gOx]iy]i!T]iu]i!S]i~PC]O!j$hOx[iy[i!T[iu[i!S[i~PETO!k$iOxZiyZi!lZi!mZi!TZisZitZiuZi!SZi~O!i$gOx]Xy]X!j]X!k]X!l]X!m]X!T]Xs]Xt]Xu]X!S]X~O!j$hOx[Xy[X!k[X!l[X!m[X!T[Xs[Xt[Xu[X!S[X~O!k$iOxZXyZX!lZX!mZX!TZXsZXtZXuZX!SZX~O!l$jO!m$kOxYXyYX!TYXsYXtYXuYX!SYX~OW$lO~O{}!W!]!mjkih!ZUj~",
goto: "3l!uPPP!v!z#[PPP#h$m%]%}&x'v(w)s*z,S-Y.b/f0jPPPPPP0jPPPP0jPPP0jPPP0jP1nP0jP1q1tPPP0jP1|2UP0jP2[2_PPPPPP2bPPPPPPP2k2q2x3O3Y3`3fTjOkSiOkQqSStUuV#X!c#Z#qShOk]$tSUu!c#Z#qSiOkQnRQsTQ{VQ|WQ!s!OS#Q!^$lY#]!f#_#m#s#zQ#a!kQ#b!lQ#g!oW#i!r#w$O$RQ#u#hR#{#v!OgORTVWk!O!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$lV#P!]$b$kWfOk!]!^Y$_R!k!o#h$bs$sTVW!O!f!l!r#_#m#s#v#w#z$O$R$k$lWeOk!]!^Q#O![Q#W$XY$^R!k!o#h$bQ$o$js$rTVW!O!f!l!r#_#m#s#v#w#z$O$R$k$lYdOk![!]!^Q!}!ZQ#V$W[$]R!k!o#h$X$bQ$n$iu$qTVW!O!f!l!r#_#m#s#v#w#z$O$R$j$k$l[cOk!Z![!]!^Q!|!YQ#U$V^$eR!k!o#h$W$X$bQ$m$hw$pTVW!O!f!l!r#_#m#s#v#w#z$O$R$i$j$k$l!hbORTVWk!O!Y!Z![!]!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$V$W$X$b$h$i$j$k$lV!{!X$a$g`aOk!X!Y!Z![!]!^Q!z!WQ#T$Ub$[R!k!o#h$V$W$X$a$bQ$c$f{$dTVW!O!f!l!r#_#m#s#v#w#z$O$R$g$h$i$j$k$l!``OTVWk!O!W!X!Y!Z![!]!^!f!l!r#_#m#s#v#w#z$O$R$f$g$h$i$j$k$lQ!y!UQ#S$TQ$Y$`e$ZR!k!o#h$U$V$W$X$a$b!z_ORTVWk!O!U!W!X!Y!Z![!]!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$T$U$V$W$X$`$a$b$f$g$h$i$j$k$lQ!x!TR#R$S#O^ORTVWk!O!T!U!W!X!Y!Z![!]!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$S$T$U$V$W$X$`$a$b$f$g$h$i$j$k$lQ!R]R!w!S#T[ORTVW]k!O!S!T!U!W!X!Y!Z![!]!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$S$T$U$V$W$X$`$a$b$f$g$h$i$j$k$l#TZORTVW]k!O!S!T!U!W!X!Y!Z![!]!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$S$T$U$V$W$X$`$a$b$f$g$h$i$j$k$l#TYORTVW]k!O!S!T!U!W!X!Y!Z![!]!^!f!k!l!o!r#_#h#m#s#v#w#z$O$R$S$T$U$V$W$X$`$a$b$f$g$h$i$j$k$lRvURzVQxVV#d!m#e#tQ#l!rV#|#w$O$RX#j!r#w$O$RR!P[R!u!OQ!WaQ$U$[R$f$dQkOR!`kSmPoR!bmQ!dqR#[!dQ!gsS#`!g#nR#n!sQuUR!juQ!nxR#f!nQ#x#lR$P#x",
nodeNames: "⚠ Comment Source Statement FieldDefinition FieldPath Identifier Dot Equal Expression DefaultExpression PatchExpression ComposeExpression LogicalOrExpression LogicalAndExpression ComparisonExpression ConcatExpression AdditiveExpression MultiplicativeExpression UnaryExpression PostfixExpression PrimaryExpression Literal String Integer Float True False Regex ComparisonConstraint Lt Lte Gt Gte Object LBrace Semicolon RBrace Array LBracket Comma RBracket LetExpression Let FieldDefinitionList In FunctionExpression LParen ParameterList Parameter Colon RParen Arrow MatchExpression Match MatchArm Pattern Underscore ImportExpression Import CallSuffix ArgumentList Bang Minus Star Slash Plus PlusPlus CompareOperator EqualEqual BangEqual AmpAmp PipePipe Amp SlashSlash Default",
maxTerm: 85,
states: "8fQYQPOOO#nQPO'#CaOOQO'#Cr'#CrO$wQPO'#CyO&UQPO'#DOO&^QQO'#DXO&hQPO'#DYO&pQPO'#CqO$wQPO'#DeO&zQPO'#DjOOQO'#Cq'#CqOOQO'#Cp'#CpO'PQPO'#CoO$wQPO'#CoOOQO'#Cn'#CnO)iQPO'#CmO.[QPO'#ClO0`QPO'#CkOOQO'#Cj'#CjO2WQPO'#CiO3{QPO'#ChO5mQPO'#CgO7XQPO'#CfOOQO'#Ce'#CeO7cQPO'#C`O7hQPO'#C_OOQO'#D|'#D|QYQPOOO8{QPO'#D}O9QQPO,58{OOQO,59e,59eO9YQPO'#CaOOQO,59j,59jO9bQPO,59jO$wQPO,59nOOQO,59s,59sO9jQPO,59sO9rQPO'#EQO9wQPO'#D[O:PQPO,59tO:UQPO'#CqO:cQPO'#D`O:kQQO,59xO:pQPO,59xO:uQPO,59]O:zQPO,5:POOQO,5:U,5:UO;PQPO'#DlOOQO,59[,59[O;WQPO,59[OOQO,59Z,59ZO$wQPO,59YO$wQPO,59XO$wQPO,59WOOQO'#Dt'#DtO$wQPO,59VO$wQPO,59UO$wQPO,59TO$wQPO,59SO$wQPO,59RO$wQPO,59QO$wQPO,58zOOQO,58y,58yOOQO-E7z-E7zOOQO,5:i,5:iOOQO-E7{-E7{O;]QPO1G/UO;eQPO1G/UOOQO1G/U1G/UO;mQPO1G/YO;uQPO1G/_O;|QPO1G/_OOQO1G/_1G/_OOQO,5:l,5:lOOQO-E8O-E8OO$wQPO1G/`O$wQPO,59{O<UQPO,59zO<^QPO,59zO$wQPO1G/dO<fQQO1G/dOOQO1G.w1G.wO<kQPO1G/kO<uQPO'#DmOOQO,5:W,5:WO<}QPO,5:WOOQO1G.v1G.vOOQO1G.t1G.tO=SQPO1G.sO@jQPO1G.rOBUQPO1G.qOOQO1G.p1G.pOC|QPO1G.oOEqQPO1G.nOGTQPO1G.mOOQO1G.l1G.lOOQO1G.f1G.fOOQO1G.s1G.sOOQO1G.r1G.rOOQO1G.q1G.qOOQO1G.o1G.oOOQO1G.n1G.nOOQO1G.m1G.mOOQO,5:j,5:jOOQO7+$p7+$pOHqQPO7+$pOOQO-E7|-E7|OOQO7+$t7+$tOHyQPO7+$tOOQO,5:k,5:kOOQO7+$y7+$yOIOQPO7+$yOOQO-E7}-E7}OOQO7+$z7+$zOOQO1G/g1G/gOIVQPO'#DaOOQO,5:m,5:mOI[QPO1G/fOOQO-E8P-E8POOQO7+%O7+%OO$wQPO7+%OOOQO'#Dh'#DhOIdQPO'#DgOOQO7+%V7+%VOIiQPO7+%VOIqQPO,5:XOIxQPO,5:XOOQO1G/r1G/rOOQO<<H[<<H[PJQQPO'#EOOOQO<<H`<<H`OOQO<<He<<HeP$wQPO'#EPPJVQPO'#EROOQO<<Hj<<HjO$wQPO,5:ROJ[QPO<<HqOJfQPO<<HqOOQO<<Hq<<HqOJnQPO1G/sOOQO1G/m1G/mOOQO,5:n,5:nOOQOAN>]AN>]OJuQPOAN>]OOQO-E8Q-E8QOOQOG23wG23wP<nQPO'#ESO$wQPO,59XO$wQPO,59WO$wQPO,59VO$wQPO,59TO$wQPO,59SO$wQPO,59ROKPQPO1G.rOKgQPO'#ClOKzQPO'#CkOLnQPO'#ChOMnQPO'#CgONqQPO'#CfO$wQPO,59WO$wQPO,59UO$wQPO,59QO! zQPO1G.qO!!bQPO'#CkO!#oQPO'#CiO$wQPO,59VO$wQPO,59UO$wQPO,59TO$wQPO,59SO$wQPO,59RO$wQPO,59QO$wQPO,58zO!$lQPO1G.oO!%SQPO1G.nO!%jQPO1G.mO!&[QPO'#CiO!'SQPO'#ChO!'wQPO'#CgO!(iQPO'#CfO!)WQPO'#C`",
stateData: "!)j~O!yOSPOS~OUPOgQOhQOiQOjQOkQOlQOnROoROpROqROsSOwTO}UO!RVO!YWO!_XO!b]O!c]O~OneXoeXpeXqeX!ReX!ceX!deX!eeX!feX!geX!ieX!jeX!keX!leX!meX!neX!oeX~OVlOUeXWTXgeXheXieXjeXkeXleXseXteXweX}eX!YeX!_eX!beX!weX~P!gOUYOgQOhQOiQOjQOkQOlQOnROoROpROqROsSOwTO}UO!RVO!YWO!_XO!b]O!c]O~OUoOupO~OxrOzsO~P$wOUoO!P!OP~OUxO!VzO~P]Og!OO~OV!RO!R!POUcXgcXhcXicXjcXkcXlcXncXocXpcXqcXscXtcXwcX}cX!YcX!_cX!bcX!ccX!dcX!ecX!fcX!gcX!icX!jcX!kcX!lcX!mcX!ncX!ocX!wcXycXzcX!VcXucX!UcX~O!d!TO!e!TOUaXgaXhaXiaXjaXkaXlaXnaXoaXpaXqaXsaXtaXwaX}aX!RaX!YaX!_aX!baX!caX!faX!gaX!iaX!jaX!kaX!laX!maX!naX!oaX!waXVaXyaXzaX!VaXuaX!UaX~OU`Xg`Xh`Xi`Xj`Xk`Xl`Xn`Xo`Xp`Xq`Xs`Xt`Xw`X}`X!R`X!Y`X!_`X!b`X!g`X!i`X!j`X!k`X!l`X!m`X!n`X!o`X!w`Xy`Xz`X!V`Xu`X!U`X~O!c!UO!f!UO~P,ROn!WOo!WOp!WOq!WO!i!WO!j!WOU_Xg_Xh_Xi_Xj_Xk_Xl_Xs_Xt_Xw_X}_X!R_X!Y_X!__X!b_X!c_X!k_X!l_X!m_X!n_X!o_X!w_X~O!g!VO~P.fOU]Xg]Xh]Xi]Xj]Xk]Xl]Xn]Xo]Xp]Xq]Xs]Xt]Xw]X}]X!R]X!Y]X!_]X!b]X!c]X!l]X!m]X!n]X!o]X!w]X~O!k!YO~P0gOU[Xg[Xh[Xi[Xj[Xk[Xl[Xn[Xo[Xp[Xq[Xs[Xt[Xw[X}[X!R[X!Y[X!_[X!b[X!c[X!m[X!n[X!o[X!w[X~O!l!ZO~P2_OUZXgZXhZXiZXjZXkZXlZXnZXoZXpZXqZXsZXtZXwZX}ZX!RZX!YZX!_ZX!bZX!cZX!nZX!oZX!wZX~O!m![O~P4SOUYXgYXhYXiYXjYXkYXlYXnYXoYXpYXqYXsYXtYXwYX}YX!RYX!YYX!_YX!bYX!cYX!wYX~O!n!]O!o!^O~P5tOW!_O~Ot!`OURXgRXhRXiRXjRXkRXlRXnRXoRXpRXqRXsRXwRX}RX!RRX!YRX!_RX!bRX!cRX!wRX~OU!bO~OVlOWTa~OVlOWTX~Ot!dOu!fO~Oy!hOz!jO~Ot!kO~OUoO!P!OX~O!P!mO~O!U!nOVeX!VeX~P!gOy!oO!V!SX~O!W!qO~O!V!rO~O!V!sO~Os!tO~O!V!vO~P$wOU!xO~OUoOu#[O~Ot#]Ou#[O~Oy#`Oz#_O~Oz#bO~P$wOy#cOz#bO~OU#gO!V!Sa~Oy#iO!V!Sa~O!W#lO~Ou#oO!]#mO~P$wOy#qO!V!aX~O!V#sO~O!d!TO!e!TOUaigaihaiiaijaikailainaioaipaiqaisaitaiwai}ai!Rai!Yai!_ai!bai!cai!fai!gai!iai!jai!kai!lai!mai!nai!oai!waiyaizai!Vaiuai!Uai~O!f!UOn`io`ip`iq`is`it`i!g`i!i`i!j`i!k`i!l`i!m`i!n`i!o`i~OU`ig`ih`ii`ij`ik`il`iw`i}`i!R`i!Y`i!_`i!b`i!c`i!w`i~P?iOs_it_i!k_i!l_i!m_i!n_i!o_i~O!g!VOU_ig_ih_ii_ij_ik_il_in_io_ip_iq_iw_i}_i!R_i!Y_i!__i!b_i!c_i!w_i~PAmOs]it]i!l]i!m]i!n]i!o]i~O!k!YOU]ig]ih]ii]ij]ik]il]in]io]ip]iq]iw]i}]i!R]i!Y]i!_]i!b]i!c]i!w]i~PChOs[it[i!m[i!n[i!o[i~O!l!ZOU[ig[ih[ii[ij[ik[il[in[io[ip[iq[iw[i}[i!R[i!Y[i!_[i!b[i!c[i!w[i~PE`O!m![OUZigZihZiiZijZikZilZinZioZipZiqZisZitZiwZi}Zi!RZi!YZi!_Zi!bZi!cZi!nZi!oZi!wZi~OUoOu#tO~Oz#vO~Oz#wO~P$wO!U!nO~OU#gO!V!Si~O!U#{O~Ot#|Ou$OO~O!V!aa~P$wOy$PO!V!aa~OUoO~OU#gO~Ou$SO!]#mO~P$wOt$TOu$SO~O!V!ai~P$wOu$VO!]#mO~P$wO!c!UOy`iz`i!V`iu`i!U`i~P?iO!c$XO!f$XOV`X!d`X!e`X~P,RO!g$YOV_X!d_X!e_X!f_Xy_Xz_X!V_Xu_X!U_X~P.fO!l$[OV[X!d[X!e[X!f[X!g[X!i[X!j[X!k[Xy[Xz[X!V[Xu[X!U[X~P2_O!m$]OVZX!dZX!eZX!fZX!gZX!iZX!jZX!kZX!lZXyZXzZX!VZXuZX!UZX~P4SO!n$^O!o$gOVYX!dYX!eYX!fYX!gYX!iYX!jYX!kYX!lYX!mYXyYXzYX!VYXuYX!UYX~P5tO!g$eOy_iz_i!V_iu_i!U_i~PAmOn!WOo!WOp!WOq!WO!g$eO!i!WO!j!WOy_Xz_X!k_X!l_X!m_X!n_X!o_X!V_Xs_Xt_Xu_X!U_X~O!k$fOV]X!d]X!e]X!f]X!g]X!i]X!j]Xy]Xz]X!V]Xu]X!U]X~P0gO!k$lOy]iz]i!V]iu]i!U]i~PChO!l$mOy[iz[i!V[iu[i!U[i~PE`O!m$nOyZizZi!nZi!oZi!VZisZitZiuZi!UZi~O!k$lOy]Xz]X!l]X!m]X!n]X!o]X!V]Xs]Xt]Xu]X!U]X~O!l$mOy[Xz[X!m[X!n[X!o[X!V[Xs[Xt[Xu[X!U[X~O!m$nOyZXzZX!nZX!oZX!VZXsZXtZXuZX!UZX~O!n$oO!o$pOyYXzYX!VYXsYXtYXuYX!UYX~OW$qO~O}!P!Y!_!ojkih!]Uj~",
goto: "4O!wPPP!x!|#^PPP#j$r%c&U'Q(P)R*O+W,a-h.q/v0{PPPPPP0{PPPP0{PPP0{PPPP0{0{P2QP0{P2T2WPPP0{P2`2hP0{P2n2qPPPPPP2tPPPPPPP2}3T3[3b3l3r3xTjOkSiOkQqSSuUvV#Z!d#]#uShOk]$ySUv!d#]#uSiOkQnRQtTQ|VQ}WQ!grQ!u!PS#S!_$qY#a!h#c#q#x$PQ#e!mQ#f!nQ#k!qW#m!t#|$T$WQ#z#lR$Q#{!QgORTVWkr!P!_!h!m!n!q!t#c#l#q#x#{#|$P$T$W$qV#R!^$g$pWfOk!^!_Y$dR!m!q#l$gu$xTVWr!P!h!n!t#c#q#x#{#|$P$T$W$p$qWeOk!^!_Q#Q!]Q#Y$^Y$cR!m!q#l$gQ$t$ou$wTVWr!P!h!n!t#c#q#x#{#|$P$T$W$p$qYdOk!]!^!_Q#P![Q#X$][$bR!m!q#l$^$gQ$s$nw$vTVWr!P!h!n!t#c#q#x#{#|$P$T$W$o$p$q[cOk![!]!^!_Q#O!ZQ#W$[^$jR!m!q#l$]$^$gQ$r$my$uTVWr!P!h!n!t#c#q#x#{#|$P$T$W$n$o$p$q!jbORTVWkr!P!Z![!]!^!_!h!m!n!q!t#c#l#q#x#{#|$P$T$W$[$]$^$g$m$n$o$p$qV!}!Y$f$l`aOk!Y!Z![!]!^!_Q!|!XQ#V$Zb$aR!m!q#l$[$]$^$f$gQ$h$k}$iTVWr!P!h!n!t#c#q#x#{#|$P$T$W$l$m$n$o$p$q!b`OTVWkr!P!X!Y!Z![!]!^!_!h!n!t#c#q#x#{#|$P$T$W$k$l$m$n$o$p$qQ!{!VQ#U$YQ$_$ee$`R!m!q#l$Z$[$]$^$f$g!|_ORTVWkr!P!V!X!Y!Z![!]!^!_!h!m!n!q!t#c#l#q#x#{#|$P$T$W$Y$Z$[$]$^$e$f$g$k$l$m$n$o$p$qQ!z!UR#T$X#Q^ORTVWkr!P!U!V!X!Y!Z![!]!^!_!h!m!n!q!t#c#l#q#x#{#|$P$T$W$X$Y$Z$[$]$^$e$f$g$k$l$m$n$o$p$qQ!S]R!y!T#V[ORTVW]kr!P!T!U!V!X!Y!Z![!]!^!_!h!m!n!q!t#c#l#q#x#{#|$P$T$W$X$Y$Z$[$]$^$e$f$g$k$l$m$n$o$p$q#VZORTVW]kr!P!T!U!V!X!Y!Z![!]!^!_!h!m!n!q!t#c#l#q#x#{#|$P$T$W$X$Y$Z$[$]$^$e$f$g$k$l$m$n$o$p$q#VYORTVW]kr!P!T!U!V!X!Y!Z![!]!^!_!h!m!n!q!t#c#l#q#x#{#|$P$T$W$X$Y$Z$[$]$^$e$f$g$k$l$m$n$o$p$qRwUR{VQyVV#h!o#i#yQ#p!tV$R#|$T$WX#n!t#|$T$WR!Q[R!w!PQ!XaQ$Z$aR$k$iQkOR!akSmPoR!cmQ!eqR#^!eQ!itS#d!i#rR#r!uQvUR!lvQ!pyR#j!pQ#}#pR$U#}",
nodeNames: "⚠ Comment Source Statement FieldDefinition FieldPath Identifier Dot Equal Expression DefaultExpression PatchExpression ComposeExpression LogicalOrExpression LogicalAndExpression ComparisonExpression ConcatExpression AdditiveExpression MultiplicativeExpression UnaryExpression PostfixExpression PrimaryExpression Literal String Integer Float True False Regex ComparisonConstraint Lt Lte Gt Gte Object LBrace Semicolon RBrace ArrayConstraint LBracket Ellipsis Comma RBracket Array LetExpression Let FieldDefinitionList In FunctionExpression LParen ParameterList Parameter Colon RParen Arrow MatchExpression Match MatchArm Pattern Underscore ImportExpression Import CallSuffix ArgumentList Bang Minus Star Slash Plus PlusPlus CompareOperator EqualEqual BangEqual AmpAmp PipePipe Amp SlashSlash Default",
maxTerm: 87,
skippedNodes: [0,1],
repeatNodeCount: 7,
tokenData: "=T~R!PX^$Upq$Uqr$yrs%Wst&zvw'cxy'pyz'uz{'z{|(P|}(^}!O(c!O!P(h!P!Q(m!Q![+T![!]+n!]!^+s!^!_+x!_!`,V!`!a,l!c!},y!}#O-[#P#Q-a#R#S-f#T#W,y#W#X-y#X#Y,y#Y#Z1Z#Z#],y#]#^3k#^#`,y#`#a6}#a#b8b#b#h,y#h#i:r#i#o,y#o#p<n#p#q<s#q#r=O#y#z$U$f$g$U#BY#BZ$U$IS$I_$U$I|$JO$U$JT$JU$U$KV$KW$U&FU&FV$U~$ZY!w~X^$Upq$U#y#z$U$f$g$U#BY#BZ$U$IS$I_$U$I|$JO$U$JT$JU$U$KV$KW$U&FU&FV$U~%OP!`~!_!`%R~%WO!h~~%ZWOY%WZr%Wrs%ss#O%W#O#P%x#P;'S%W;'S;=`&t<%lO%W~%xOg~~%{RO;'S%W;'S;=`&U;=`O%W~&XXOY%WZr%Wrs%ss#O%W#O#P%x#P;'S%W;'S;=`&t;=`<%l%W<%lO%W~&wP;=`<%l%W~'PSP~OY&zZ;'S&z;'S;=`']<%lO&z~'`P;=`<%l&z~'hP!k~vw'k~'pO!i~~'uO!P~~'zO!T~~(PO!b~~(UP!d~{|(X~(^O!e~~(cOx~~(hO!a~~(mOV~~(rW!c~OY)[Z!P)[!P!Q+O!Q#O)[#O#P)|#P;'S)[;'S;=`*x<%lO)[~)_WOY)[Z!P)[!P!Q)w!Q#O)[#O#P)|#P;'S)[;'S;=`*x<%lO)[~)|Ol~~*PRO;'S)[;'S;=`*Y;=`O)[~*]XOY)[Z!P)[!P!Q)w!Q#O)[#O#P)|#P;'S)[;'S;=`*x;=`<%l)[<%lO)[~*{P;=`<%l)[~+TO!l~~+YQh~!O!P+`!Q![+T~+cP!Q![+f~+kPi~!Q![+f~+sO!S~~+xOt~~+}Pn~!_!`,Q~,VOo~~,[QWP!_!`,b!`!a,g~,gO!g~Q,lO!UQ~,qPp~!_!`,t~,yOq~~-OSU~!Q![,y!c!},y#R#S,y#T#o,y~-aOw~~-fOy~~-mS!Z~U~!Q![,y!c!},y#R#S,y#T#o,y~.OUU~!Q![,y!c!},y#R#S,y#T#X,y#X#Y.b#Y#o,y~.gUU~!Q![,y!c!},y#R#S,y#T#Y,y#Y#Z.y#Z#o,y~/OTU~!Q![,y!c!},y#R#S,y#T#U/_#U#o,y~/dUU~!Q![,y!c!},y#R#S,y#T#i,y#i#j/v#j#o,y~/{UU~!Q![,y!c!},y#R#S,y#T#`,y#`#a0_#a#o,y~0dUU~!Q![,y!c!},y#R#S,y#T#h,y#h#i0v#i#o,y~0}S!m~U~!Q![,y!c!},y#R#S,y#T#o,y~1`TU~!Q![,y!c!},y#R#S,y#T#U1o#U#o,y~1tUU~!Q![,y!c!},y#R#S,y#T#`,y#`#a2W#a#o,y~2]UU~!Q![,y!c!},y#R#S,y#T#g,y#g#h2o#h#o,y~2tUU~!Q![,y!c!},y#R#S,y#T#X,y#X#Y3W#Y#o,y~3_Sk~U~!Q![,y!c!},y#R#S,y#T#o,y~3pVU~!Q![,y!c!},y#R#S,y#T#a,y#a#b4V#b#c6j#c#o,y~4[UU~!Q![,y!c!},y#R#S,y#T#d,y#d#e4n#e#o,y~4sUU~!Q![,y!c!},y#R#S,y#T#c,y#c#d5V#d#o,y~5[UU~!Q![,y!c!},y#R#S,y#T#f,y#f#g5n#g#o,y~5sUU~!Q![,y!c!},y#R#S,y#T#h,y#h#i6V#i#o,y~6^S!]~U~!Q![,y!c!},y#R#S,y#T#o,y~6qS}~U~!Q![,y!c!},y#R#S,y#T#o,y~7SUU~!Q![,y!c!},y#R#S,y#T#X,y#X#Y7f#Y#o,y~7kUU~!Q![,y!c!},y#R#S,y#T#h,y#h#i7}#i#o,y~8US{~U~!Q![,y!c!},y#R#S,y#T#o,y~8gTU~!Q![,y!c!},y#R#S,y#T#U8v#U#o,y~8{UU~!Q![,y!c!},y#R#S,y#T#h,y#h#i9_#i#o,y~9dUU~!Q![,y!c!},y#R#S,y#T#V,y#V#W9v#W#o,y~9{UU~!Q![,y!c!},y#R#S,y#T#[,y#[#]:_#]#o,y~:fS!W~U~!Q![,y!c!},y#R#S,y#T#o,y~:wUU~!Q![,y!c!},y#R#S,y#T#f,y#f#g;Z#g#o,y~;`UU~!Q![,y!c!},y#R#S,y#T#i,y#i#j;r#j#o,y~;wUU~!Q![,y!c!},y#R#S,y#T#X,y#X#Y<Z#Y#o,y~<bSj~U~!Q![,y!c!},y#R#S,y#T#o,y~<sOs~~<vP#p#q<y~=OO!j~~=TOu~",
tokenData: "=c~R!PX^$Upq$Uqr$yrs%Wst&zvw'cxy'pyz'uz{'z{|(P|}(^}!O(c!O!P(h!P!Q({!Q![+c![!]+|!]!^,R!^!_,W!_!`,e!`!a,z!c!}-X!}#O-j#P#Q-o#R#S-t#T#W-X#W#X.X#X#Y-X#Y#Z1i#Z#]-X#]#^3y#^#`-X#`#a7]#a#b8p#b#h-X#h#i;Q#i#o-X#o#p<|#p#q=R#q#r=^#y#z$U$f$g$U#BY#BZ$U$IS$I_$U$I|$JO$U$JT$JU$U$KV$KW$U&FU&FV$U~$ZY!y~X^$Upq$U#y#z$U$f$g$U#BY#BZ$U$IS$I_$U$I|$JO$U$JT$JU$U$KV$KW$U&FU&FV$U~%OP!b~!_!`%R~%WO!j~~%ZWOY%WZr%Wrs%ss#O%W#O#P%x#P;'S%W;'S;=`&t<%lO%W~%xOg~~%{RO;'S%W;'S;=`&U;=`O%W~&XXOY%WZr%Wrs%ss#O%W#O#P%x#P;'S%W;'S;=`&t;=`<%l%W<%lO%W~&wP;=`<%l%W~'PSP~OY&zZ;'S&z;'S;=`']<%lO&z~'`P;=`<%l&z~'hP!m~vw'k~'pO!k~~'uO!R~~'zO!V~~(PO!d~~(UP!f~{|(X~(^O!g~~(cOy~~(hO!c~R(mPVP!O!P(pQ(sP!O!P(vQ({OxQ~)QW!e~OY)jZ!P)j!P!Q+^!Q#O)j#O#P*[#P;'S)j;'S;=`+W<%lO)j~)mWOY)jZ!P)j!P!Q*V!Q#O)j#O#P*[#P;'S)j;'S;=`+W<%lO)j~*[Ol~~*_RO;'S)j;'S;=`*h;=`O)j~*kXOY)jZ!P)j!P!Q*V!Q#O)j#O#P*[#P;'S)j;'S;=`+W;=`<%l)j<%lO)j~+ZP;=`<%l)j~+cO!n~~+hQh~!O!P+n!Q![+c~+qP!Q![+t~+yPi~!Q![+t~,RO!U~~,WOt~~,]Pn~!_!`,`~,eOo~~,jQWP!_!`,p!`!a,u~,uO!i~Q,zO!WQ~-PPp~!_!`-S~-XOq~~-^SU~!Q![-X!c!}-X#R#S-X#T#o-X~-oOw~~-tOz~~-{S!]~U~!Q![-X!c!}-X#R#S-X#T#o-X~.^UU~!Q![-X!c!}-X#R#S-X#T#X-X#X#Y.p#Y#o-X~.uUU~!Q![-X!c!}-X#R#S-X#T#Y-X#Y#Z/X#Z#o-X~/^TU~!Q![-X!c!}-X#R#S-X#T#U/m#U#o-X~/rUU~!Q![-X!c!}-X#R#S-X#T#i-X#i#j0U#j#o-X~0ZUU~!Q![-X!c!}-X#R#S-X#T#`-X#`#a0m#a#o-X~0rUU~!Q![-X!c!}-X#R#S-X#T#h-X#h#i1U#i#o-X~1]S!o~U~!Q![-X!c!}-X#R#S-X#T#o-X~1nTU~!Q![-X!c!}-X#R#S-X#T#U1}#U#o-X~2SUU~!Q![-X!c!}-X#R#S-X#T#`-X#`#a2f#a#o-X~2kUU~!Q![-X!c!}-X#R#S-X#T#g-X#g#h2}#h#o-X~3SUU~!Q![-X!c!}-X#R#S-X#T#X-X#X#Y3f#Y#o-X~3mSk~U~!Q![-X!c!}-X#R#S-X#T#o-X~4OVU~!Q![-X!c!}-X#R#S-X#T#a-X#a#b4e#b#c6x#c#o-X~4jUU~!Q![-X!c!}-X#R#S-X#T#d-X#d#e4|#e#o-X~5RUU~!Q![-X!c!}-X#R#S-X#T#c-X#c#d5e#d#o-X~5jUU~!Q![-X!c!}-X#R#S-X#T#f-X#f#g5|#g#o-X~6RUU~!Q![-X!c!}-X#R#S-X#T#h-X#h#i6e#i#o-X~6lS!_~U~!Q![-X!c!}-X#R#S-X#T#o-X~7PS!P~U~!Q![-X!c!}-X#R#S-X#T#o-X~7bUU~!Q![-X!c!}-X#R#S-X#T#X-X#X#Y7t#Y#o-X~7yUU~!Q![-X!c!}-X#R#S-X#T#h-X#h#i8]#i#o-X~8dS}~U~!Q![-X!c!}-X#R#S-X#T#o-X~8uTU~!Q![-X!c!}-X#R#S-X#T#U9U#U#o-X~9ZUU~!Q![-X!c!}-X#R#S-X#T#h-X#h#i9m#i#o-X~9rUU~!Q![-X!c!}-X#R#S-X#T#V-X#V#W:U#W#o-X~:ZUU~!Q![-X!c!}-X#R#S-X#T#[-X#[#]:m#]#o-X~:tS!Y~U~!Q![-X!c!}-X#R#S-X#T#o-X~;VUU~!Q![-X!c!}-X#R#S-X#T#f-X#f#g;i#g#o-X~;nUU~!Q![-X!c!}-X#R#S-X#T#i-X#i#j<Q#j#o-X~<VUU~!Q![-X!c!}-X#R#S-X#T#X-X#X#Y<i#Y#o-X~<pSj~U~!Q![-X!c!}-X#R#S-X#T#o-X~=ROs~~=UP#p#q=X~=^O!l~~=cOu~",
tokenizers: [0, 1],
topRules: {"Source":[0,2]},
tokenPrec: 2481
tokenPrec: 2497
})
@@ -37,41 +37,43 @@ export const
LBrace = 35,
Semicolon = 36,
RBrace = 37,
Array = 38,
ArrayConstraint = 38,
LBracket = 39,
Comma = 40,
RBracket = 41,
LetExpression = 42,
Let = 43,
FieldDefinitionList = 44,
In = 45,
FunctionExpression = 46,
LParen = 47,
ParameterList = 48,
Parameter = 49,
Colon = 50,
RParen = 51,
Arrow = 52,
MatchExpression = 53,
Match = 54,
MatchArm = 55,
Pattern = 56,
Underscore = 57,
ImportExpression = 58,
Import = 59,
CallSuffix = 60,
ArgumentList = 61,
Bang = 62,
Minus = 63,
Star = 64,
Slash = 65,
Plus = 66,
PlusPlus = 67,
CompareOperator = 68,
EqualEqual = 69,
BangEqual = 70,
AmpAmp = 71,
PipePipe = 72,
Amp = 73,
SlashSlash = 74,
Default = 75
Ellipsis = 40,
Comma = 41,
RBracket = 42,
Array = 43,
LetExpression = 44,
Let = 45,
FieldDefinitionList = 46,
In = 47,
FunctionExpression = 48,
LParen = 49,
ParameterList = 50,
Parameter = 51,
Colon = 52,
RParen = 53,
Arrow = 54,
MatchExpression = 55,
Match = 56,
MatchArm = 57,
Pattern = 58,
Underscore = 59,
ImportExpression = 60,
Import = 61,
CallSuffix = 62,
ArgumentList = 63,
Bang = 64,
Minus = 65,
Star = 66,
Slash = 67,
Plus = 68,
PlusPlus = 69,
CompareOperator = 70,
EqualEqual = 71,
BangEqual = 72,
AmpAmp = 73,
PipePipe = 74,
Amp = 75,
SlashSlash = 76,
Default = 77
+7 -5
View File
@@ -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 { parser } from './decodal-parser.js';
@@ -12,21 +12,23 @@ const parserWithMetadata = parser.configure({
Regex: t.regexp,
'Integer Float': t.number,
Comment: t.lineComment,
'Plus Minus Star Slash PlusPlus Equal EqualEqual Bang BangEqual Amp AmpAmp PipePipe SlashSlash Gt Gte Lt Lte Arrow': t.operator,
'Plus Minus Star Slash PlusPlus Ellipsis Equal EqualEqual Bang BangEqual Amp AmpAmp PipePipe SlashSlash Gt Gte Lt Lte Arrow': t.operator,
'LBrace RBrace': t.brace,
'LBracket RBracket': t.squareBracket,
'LParen RParen': t.paren,
'Semicolon Comma Dot Colon': t.punctuation,
}),
indentNodeProp.add({
Object: context => context.column(context.node.from) + context.unit,
Array: context => context.column(context.node.from) + context.unit,
MatchExpression: context => context.column(context.node.from) + context.unit,
Object: delimitedIndent({ closing: '}', align: false }),
Array: delimitedIndent({ closing: ']', align: false }),
ArrayConstraint: delimitedIndent({ closing: ']', align: false }),
MatchExpression: delimitedIndent({ closing: '}', align: false }),
LetExpression: context => context.column(context.node.from) + context.unit,
}),
foldNodeProp.add({
Object: foldDelimited('{', '}'),
Array: foldDelimited('[', ']'),
ArrayConstraint: foldDelimited('[', ']'),
MatchExpression: foldDelimited('{', '}'),
}),
],
@@ -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);
});
+17
View File
@@ -0,0 +1,17 @@
import type { EditorView } from '@codemirror/view';
export interface FormatSuccess {
ok: true;
source: string;
}
export interface FormatFailure {
ok: false;
error: string;
}
export type FormatResult = FormatSuccess | FormatFailure;
export declare function initDecodalFormatter(moduleOrPath?: RequestInfo | URL | Response | BufferSource | WebAssembly.Module): Promise<void>;
export declare function formatDecodal(source: string): FormatResult;
export declare function formatDecodalCommand(view: EditorView): boolean;
+28
View File
@@ -0,0 +1,28 @@
import initLanguageTools, { formatSource } from '../wasm/decodal_language_tools.js';
let initialized = false;
export async function initDecodalFormatter(moduleOrPath) {
await initLanguageTools(moduleOrPath);
initialized = true;
}
export function formatDecodal(source) {
if (!initialized) {
return { ok: false, error: 'Decodal formatter is not initialized' };
}
return JSON.parse(formatSource(source));
}
export function formatDecodalCommand(view) {
const result = formatDecodal(view.state.doc.toString());
if (!result.ok) return false;
view.dispatch({
changes: {
from: 0,
to: view.state.doc.length,
insert: result.source,
},
});
return true;
}
+17
View File
@@ -0,0 +1,17 @@
import type { EditorView } from '@codemirror/view';
export interface FormatSuccess {
ok: true;
source: string;
}
export interface FormatFailure {
ok: false;
error: string;
}
export type FormatResult = FormatSuccess | FormatFailure;
export declare function initDecodalFormatter(moduleOrPath?: RequestInfo | URL | Response | BufferSource | WebAssembly.Module): Promise<void>;
export declare function formatDecodal(source: string): FormatResult;
export declare function formatDecodalCommand(view: EditorView): boolean;
@@ -0,0 +1,16 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import { formatDecodal, initDecodalFormatter } from './format.js';
const wasmPath = new URL('../wasm/decodal_language_tools_bg.wasm', import.meta.url);
test('WASM uses the canonical formatter and is idempotent', async () => {
await initDecodalFormatter({ module_or_path: await readFile(wasmPath) });
const source = 'Server={\n# host\nhost=String default "localhost"; # trailing\n\nport=Int&>0;\n};';
const expected = 'Server = {\n # host\n host = String default "localhost"; # trailing\n\n port = Int & > 0;\n};\n';
assert.deepEqual(formatDecodal(source), { ok: true, source: expected });
assert.deepEqual(formatDecodal(expected), { ok: true, source: expected });
});
+84
View File
@@ -0,0 +1,84 @@
/**
* Decodal source formatting helpers for CodeMirror integrations.
*
* The formatter is backed by WebAssembly generated from the canonical Rust
* `decodal-language-tools` formatter. Call {@link initDecodalFormatter} once before
* using {@link formatDecodal} or {@link formatDecodalCommand}.
*
* @module
*/
import type { EditorView } from 'npm:@codemirror/view@^6.43.6';
import initLanguageTools, { formatSource } from '../wasm/decodal_language_tools.js';
let initialized = false;
/** Successful formatter result. */
export interface FormatSuccess {
/** Indicates that formatting succeeded. */
ok: true;
/** The formatted Decodal source text. */
source: string;
}
/** Failed formatter result. */
export interface FormatFailure {
/** Indicates that formatting failed. */
ok: false;
/** Human-readable formatter error. */
error: string;
}
/** Result returned by {@link formatDecodal}. */
export type FormatResult = FormatSuccess | FormatFailure;
/**
* Initialize the bundled Decodal formatter WebAssembly module.
*
* Bundlers such as Vite should pass the package wasm asset URL explicitly:
*
* ```ts
* import wasmUrl from 'decodal-codemirror/wasm/decodal_language_tools_bg.wasm?url';
* await initDecodalFormatter(wasmUrl);
* ```
*
* @param moduleOrPath - Optional WebAssembly module, bytes, response, URL, or
* request accepted by wasm-bindgen's generated initializer.
*/
export async function initDecodalFormatter(
moduleOrPath?: RequestInfo | URL | Response | BufferSource | WebAssembly.Module,
): Promise<void> {
await initLanguageTools(moduleOrPath);
initialized = true;
}
/**
* Format a complete Decodal source document.
*
* @param source - Source text to format.
* @returns Either formatted source text or an error message.
*/
export function formatDecodal(source: string): FormatResult {
if (!initialized) {
return { ok: false, error: 'Decodal formatter is not initialized' };
}
return JSON.parse(formatSource(source)) as FormatResult;
}
/**
* CodeMirror command that formats the whole document in-place.
*
* @param view - The editor view whose document should be formatted.
* @returns `true` when the document was formatted, otherwise `false`.
*/
export function formatDecodalCommand(view: EditorView): boolean {
const result = formatDecodal(view.state.doc.toString());
if (!result.ok) return false;
view.dispatch({
changes: {
from: 0,
to: view.state.doc.length,
insert: result.source,
},
});
return true;
}
+28 -5
View File
@@ -1,4 +1,14 @@
import { HighlightStyle, LRLanguage, LanguageSupport, foldNodeProp, indentNodeProp, syntaxHighlighting } from 'npm:@codemirror/language@^6.12.4';
/**
* CodeMirror 6 language support for Decodal.
*
* This module exports the Decodal language extension, parser metadata, and the
* bundled highlight style used by the official Decodal playground. Add
* {@link decodal} to a CodeMirror extension set to enable highlighting,
* indentation, and folding for Decodal documents.
*
* @module
*/
import { HighlightStyle, LRLanguage, LanguageSupport, delimitedIndent, foldNodeProp, indentNodeProp, syntaxHighlighting } from 'npm:@codemirror/language@^6.12.4';
import { styleTags, tags as t } from 'npm:@lezer/highlight@^1.2.3';
import { parser } from './decodal-parser-jsr.js';
@@ -12,21 +22,23 @@ const parserWithMetadata = parser.configure({
Regex: t.regexp,
'Integer Float': t.number,
Comment: t.lineComment,
'Plus Minus Star Slash PlusPlus Equal EqualEqual Bang BangEqual Amp AmpAmp PipePipe SlashSlash Gt Gte Lt Lte Arrow': t.operator,
'Plus Minus Star Slash PlusPlus Ellipsis Equal EqualEqual Bang BangEqual Amp AmpAmp PipePipe SlashSlash Gt Gte Lt Lte Arrow': t.operator,
'LBrace RBrace': t.brace,
'LBracket RBracket': t.squareBracket,
'LParen RParen': t.paren,
'Semicolon Comma Dot Colon': t.punctuation,
}),
indentNodeProp.add({
Object: context => context.column(context.node.from) + context.unit,
Array: context => context.column(context.node.from) + context.unit,
MatchExpression: context => context.column(context.node.from) + context.unit,
Object: delimitedIndent({ closing: '}', align: false }),
Array: delimitedIndent({ closing: ']', align: false }),
ArrayConstraint: delimitedIndent({ closing: ']', align: false }),
MatchExpression: delimitedIndent({ closing: '}', align: false }),
LetExpression: context => context.column(context.node.from) + context.unit,
}),
foldNodeProp.add({
Object: foldDelimited('{', '}'),
Array: foldDelimited('[', ']'),
ArrayConstraint: foldDelimited('[', ']'),
MatchExpression: foldDelimited('{', '}'),
}),
],
@@ -44,6 +56,7 @@ function foldDelimited(open: string, close: string) {
};
}
/** Decodal LR language definition for CodeMirror 6. */
export const decodalLanguage: LRLanguage = LRLanguage.define({
parser: parserWithMetadata,
languageData: {
@@ -52,6 +65,7 @@ export const decodalLanguage: LRLanguage = LRLanguage.define({
},
});
/** Default Decodal highlight style used by the playground. */
export const decodalHighlightStyle: HighlightStyle = HighlightStyle.define([
{ tag: t.keyword, color: '#93c5fd', fontWeight: '700' },
{ tag: t.variableName, color: 'inherit' },
@@ -64,10 +78,19 @@ export const decodalHighlightStyle: HighlightStyle = HighlightStyle.define([
{ tag: [t.brace, t.squareBracket, t.paren, t.punctuation], color: '#f9a8d4' },
]);
/** Options for {@link decodal}. */
export interface DecodalOptions {
/** Include the bundled {@link decodalHighlightStyle}. Defaults to `true`. */
highlight?: boolean;
}
/**
* Create CodeMirror language support for Decodal.
*
* @param options - Optional language support options.
* @returns A CodeMirror extension bundle containing the Decodal language and,
* unless disabled, the default highlight style.
*/
export function decodal(options: DecodalOptions = {}): LanguageSupport {
const { highlight = true } = options;
return new LanguageSupport(
@@ -0,0 +1 @@
# wasm-pack output is committed as part of decodal-codemirror.
@@ -1,16 +1,13 @@
/* tslint:disable */
/* eslint-disable */
export function evaluate(source: string): string;
export function evaluateProject(entry: string, files_json: string): string;
export function formatSource(source: string): string;
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
export interface InitOutput {
readonly memory: WebAssembly.Memory;
readonly evaluate: (a: number, b: number) => [number, number];
readonly evaluateProject: (a: number, b: number, c: number, d: number) => [number, number];
readonly formatSource: (a: number, b: number) => [number, number];
readonly __wbindgen_externrefs: WebAssembly.Table;
readonly __wbindgen_malloc: (a: number, b: number) => number;
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
@@ -1,16 +1,16 @@
/* @ts-self-types="./decodal_wasm.d.ts" */
/* @ts-self-types="./decodal_language_tools.d.ts" */
/**
* @param {string} source
* @returns {string}
*/
export function evaluate(source) {
export function formatSource(source) {
let deferred2_0;
let deferred2_1;
try {
const ptr0 = passStringToWasm0(source, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.evaluate(ptr0, len0);
const ret = wasm.formatSource(ptr0, len0);
deferred2_0 = ret[0];
deferred2_1 = ret[1];
return getStringFromWasm0(ret[0], ret[1]);
@@ -18,28 +18,6 @@ export function evaluate(source) {
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() {
const import0 = {
__proto__: null,
@@ -55,7 +33,7 @@ function __wbg_get_imports() {
};
return {
__proto__: null,
"./decodal_wasm_bg.js": import0,
"./decodal_language_tools_bg.js": import0,
};
}
@@ -215,7 +193,7 @@ async function __wbg_init(module_or_path) {
}
if (module_or_path === undefined) {
module_or_path = new URL('decodal_wasm_bg.wasm', import.meta.url);
module_or_path = new URL('decodal_language_tools_bg.wasm', import.meta.url);
}
const imports = __wbg_get_imports();
@@ -1,8 +1,7 @@
/* tslint:disable */
/* eslint-disable */
export const memory: WebAssembly.Memory;
export const evaluate: (a: number, b: number) => [number, number];
export const evaluateProject: (a: number, b: number, c: number, d: number) => [number, number];
export const formatSource: (a: number, b: number) => [number, number];
export const __wbindgen_externrefs: WebAssembly.Table;
export const __wbindgen_malloc: (a: number, b: number) => number;
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
+1
View File
@@ -0,0 +1 @@
# wasm-pack output is committed for the browser runtime package.
+173
View File
@@ -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
+21
View File
@@ -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.
+69
View File
@@ -0,0 +1,69 @@
# decodal-wasm
WebAssembly runtime package for Decodal.
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.
Use `decodal-codemirror` separately when you need CodeMirror 6 language support.
## Install
```sh
npm install decodal-wasm
```
JSR:
```sh
deno add jsr:@hare/decodal-wasm
```
## Usage
```js
import init, { DecodalLanguageService } from 'decodal-wasm';
await init();
const files = {
'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));
const completion = service.complete(
'main.dcdl',
'App.Server.po',
'App.Server.po'.length,
false,
);
console.log(JSON.parse(completion));
```
`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.
+126
View File
@@ -0,0 +1,126 @@
/* tslint:disable */
/* eslint-disable */
/** 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[];
}
/**
* 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 interface InitOutput {
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 __wbindgen_malloc: (a: number, b: 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 __externref_table_dealloc: (a: number) => void;
readonly __wbindgen_start: () => void;
}
export type SyncInitInput = BufferSource | WebAssembly.Module;
/**
* Instantiates the given `module`, which can either be bytes or
* a precompiled `WebAssembly.Module`.
*
* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
*
* @returns {InitOutput}
*/
export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
/**
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
* for everything else, calls `WebAssembly.instantiate` directly.
*
* @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
*
* @returns {Promise<InitOutput>}
*/
export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;
+597
View File
@@ -0,0 +1,597 @@
/* @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
* @returns {string}
*/
export function evaluate(source) {
let deferred2_0;
let deferred2_1;
try {
const ptr0 = passStringToWasm0(source, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.evaluate(ptr0, len0);
deferred2_0 = ret[0];
deferred2_1 = ret[1];
return getStringFromWasm0(ret[0], ret[1]);
} finally {
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
}
}
function __wbg_get_imports() {
const import0 = {
__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() {
const table = wasm.__wbindgen_externrefs;
const offset = table.grow(4);
table.set(0, undefined);
table.set(offset + 0, undefined);
table.set(offset + 1, null);
table.set(offset + 2, true);
table.set(offset + 3, false);
},
};
return {
__proto__: null,
"./decodal_wasm_bg.js": import0,
};
}
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) {
return decodeText(ptr >>> 0, len);
}
let cachedUint8ArrayMemory0 = null;
function getUint8ArrayMemory0() {
if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
}
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) {
if (realloc === undefined) {
const buf = cachedTextEncoder.encode(arg);
const ptr = malloc(buf.length, 1) >>> 0;
getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
WASM_VECTOR_LEN = buf.length;
return ptr;
}
let len = arg.length;
let ptr = malloc(len, 1) >>> 0;
const mem = getUint8ArrayMemory0();
let offset = 0;
for (; offset < len; offset++) {
const code = arg.charCodeAt(offset);
if (code > 0x7F) break;
mem[ptr + offset] = code;
}
if (offset !== len) {
if (offset !== 0) {
arg = arg.slice(offset);
}
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
const ret = cachedTextEncoder.encodeInto(arg, view);
offset += ret.written;
ptr = realloc(ptr, len, offset, 1) >>> 0;
}
WASM_VECTOR_LEN = offset;
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 });
cachedTextDecoder.decode();
const MAX_SAFARI_DECODE_BYTES = 2146435072;
let numBytesDecoded = 0;
function decodeText(ptr, len) {
numBytesDecoded += len;
if (numBytesDecoded >= MAX_SAFARI_DECODE_BYTES) {
cachedTextDecoder = new TextDecoder('utf-8', { ignoreBOM: true, fatal: true });
cachedTextDecoder.decode();
numBytesDecoded = len;
}
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
}
const cachedTextEncoder = new TextEncoder();
if (!('encodeInto' in cachedTextEncoder)) {
cachedTextEncoder.encodeInto = function (arg, view) {
const buf = cachedTextEncoder.encode(arg);
view.set(buf);
return {
read: arg.length,
written: buf.length
};
};
}
let WASM_VECTOR_LEN = 0;
let wasmModule, wasmInstance, wasm;
function __wbg_finalize_init(instance, module) {
wasmInstance = instance;
wasm = instance.exports;
wasmModule = module;
cachedDataViewMemory0 = null;
cachedUint8ArrayMemory0 = null;
wasm.__wbindgen_start();
return wasm;
}
async function __wbg_load(module, imports) {
if (typeof Response === 'function' && module instanceof Response) {
if (typeof WebAssembly.instantiateStreaming === 'function') {
try {
return await WebAssembly.instantiateStreaming(module, imports);
} catch (e) {
const validResponse = module.ok && expectedResponseType(module.type);
if (validResponse && module.headers.get('Content-Type') !== 'application/wasm') {
console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
} else { throw e; }
}
}
const bytes = await module.arrayBuffer();
return await WebAssembly.instantiate(bytes, imports);
} else {
const instance = await WebAssembly.instantiate(module, imports);
if (instance instanceof WebAssembly.Instance) {
return { instance, module };
} else {
return instance;
}
}
function expectedResponseType(type) {
switch (type) {
case 'basic': case 'cors': case 'default': return true;
}
return false;
}
}
function initSync(module) {
if (wasm !== undefined) return wasm;
if (module !== undefined) {
if (Object.getPrototypeOf(module) === Object.prototype) {
({module} = module)
} else {
console.warn('using deprecated parameters for `initSync()`; pass a single object instead')
}
}
const imports = __wbg_get_imports();
if (!(module instanceof WebAssembly.Module)) {
module = new WebAssembly.Module(module);
}
const instance = new WebAssembly.Instance(module, imports);
return __wbg_finalize_init(instance, module);
}
async function __wbg_init(module_or_path) {
if (wasm !== undefined) return wasm;
if (module_or_path !== undefined) {
if (Object.getPrototypeOf(module_or_path) === Object.prototype) {
({module_or_path} = module_or_path)
} else {
console.warn('using deprecated parameters for the initialization function; pass a single object instead')
}
}
if (module_or_path === undefined) {
module_or_path = new URL('decodal_wasm_bg.wasm', import.meta.url);
}
const imports = __wbg_get_imports();
if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) {
module_or_path = fetch(module_or_path);
}
const { instance, module } = await __wbg_load(await module_or_path, imports);
return __wbg_finalize_init(instance, module);
}
export { initSync, __wbg_init as default };
Binary file not shown.
+16
View File
@@ -0,0 +1,16 @@
/* tslint:disable */
/* eslint-disable */
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 __wbindgen_malloc: (a: number, b: 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 __externref_table_dealloc: (a: number) => void;
export const __wbindgen_start: () => void;
@@ -1,11 +1,14 @@
{
"name": "@hare/decodal-wasm",
"version": "0.1.2",
"version": "0.1.4",
"license": "MIT",
"exports": "./decodal_wasm.js",
"exports": "./mod.ts",
"publish": {
"include": [
"README.md",
"LICENSE-MIT",
"LICENSE-APACHE",
"mod.ts",
"decodal_wasm.js",
"decodal_wasm.d.ts",
"decodal_wasm_bg.wasm",
+26
View File
@@ -0,0 +1,26 @@
/**
* Host-configurable Decodal evaluator and language service for JavaScript.
*
* @module
*/
export {
DecodalLanguageService,
default,
evaluate,
initSync,
} from './decodal_wasm.js';
export type {
DecodalAbstractDescriptor,
DecodalArrayDescriptor,
DecodalConstraint,
DecodalEnvironment,
DecodalHostValue,
DecodalImportCandidate,
DecodalLoadedImport,
DecodalPrimitiveDescriptor,
DecodalPrimitiveType,
InitInput,
InitOutput,
SyncInitInput,
} from './decodal_wasm.js';
@@ -1,14 +1,17 @@
{
"name": "decodal-wasm",
"type": "module",
"description": "WebAssembly wrapper for evaluating Decodal in browser playgrounds.",
"version": "0.1.2",
"description": "Host-configurable Decodal evaluator and language service for JavaScript runtimes.",
"version": "0.1.4",
"license": "MIT OR Apache-2.0",
"repository": {
"type": "git",
"url": "https://gitea.hareworks.net/Hare/Decodal"
},
"files": [
"README.md",
"LICENSE-MIT",
"LICENSE-APACHE",
"decodal_wasm_bg.wasm",
"decodal_wasm.js",
"decodal_wasm.d.ts"
@@ -23,5 +26,13 @@
"wasm",
"dsl",
"config"
]
],
"exports": {
".": {
"types": "./decodal_wasm.d.ts",
"import": "./decodal_wasm.js"
},
"./decodal_wasm_bg.wasm": "./decodal_wasm_bg.wasm",
"./package.json": "./package.json"
}
}
+10
View File
@@ -2,8 +2,18 @@ import { defineConfig } from 'astro/config';
export default defineConfig({
output: 'static',
build: {
assets: 'assets',
},
vite: {
resolve: {
dedupe: [
'@codemirror/language',
'@codemirror/state',
'@codemirror/view',
'@lezer/highlight',
'@lezer/lr',
],
preserveSymlinks: true,
},
},
+19 -2
View File
@@ -16,6 +16,7 @@
"astro": "^4.16.18",
"codemirror": "^6.0.2",
"decodal-codemirror": "file:../../packages/decodal-codemirror",
"decodal-wasm": "file:../../packages/decodal-wasm",
"marked": "^12.0.2"
},
"devDependencies": {
@@ -24,14 +25,26 @@
}
},
"../../packages/decodal-codemirror": {
"version": "0.1.2",
"version": "0.1.6",
"license": "MIT OR Apache-2.0",
"dependencies": {
"devDependencies": {
"@codemirror/language": "^6.12.4",
"@codemirror/state": "^6.7.1",
"@codemirror/view": "^6.43.6",
"@lezer/highlight": "^1.2.3",
"@lezer/lr": "^1.4.10"
},
"peerDependencies": {
"@codemirror/language": "^6.12.4",
"@codemirror/view": "^6.43.6",
"@lezer/highlight": "^1.2.3",
"@lezer/lr": "^1.4.10"
}
},
"../../packages/decodal-wasm": {
"version": "0.1.4",
"license": "MIT OR Apache-2.0"
},
"node_modules/@astrojs/check": {
"version": "0.9.9",
"resolved": "https://registry.npmjs.org/@astrojs/check/-/check-0.9.9.tgz",
@@ -3250,6 +3263,10 @@
"resolved": "../../packages/decodal-codemirror",
"link": true
},
"node_modules/decodal-wasm": {
"resolved": "../../packages/decodal-wasm",
"link": true
},
"node_modules/decode-named-character-reference": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz",
+7 -2
View File
@@ -6,10 +6,14 @@
"scripts": {
"dev": "astro dev --host 0.0.0.0",
"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",
"build:wasm": "wasm-pack build ../../crates/decodal-wasm --target web --out-dir ../../site/decodal-site/src/wasm --release",
"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": {
"@astrojs/check": "^0.9.4",
@@ -20,6 +24,7 @@
"astro": "^4.16.18",
"codemirror": "^6.0.2",
"decodal-codemirror": "file:../../packages/decodal-codemirror",
"decodal-wasm": "file:../../packages/decodal-wasm",
"marked": "^12.0.2"
},
"devDependencies": {
@@ -0,0 +1,16 @@
import { copyFileSync, rmSync, writeFileSync } from 'node:fs';
import { resolve } from 'node:path';
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, 'package.json'), { force: true });
writeFileSync(
resolve(wasmDir, '.gitignore'),
'# wasm-pack output is committed as part of decodal-codemirror.\n',
);
@@ -0,0 +1,202 @@
import { copyFileSync, readFileSync, writeFileSync } from 'node:fs';
import { resolve } from 'node:path';
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(
resolve(packageDir, '.gitignore'),
'# wasm-pack output is committed for the browser runtime package.\n',
);
const packageJsonPath = resolve(packageDir, 'package.json');
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
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(
resolve(packageDir, 'README.md'),
`# decodal-wasm
WebAssembly runtime package for Decodal.
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.
Use \`decodal-codemirror\` separately when you need CodeMirror 6 language support.
## Install
\`\`\`sh
npm install decodal-wasm
\`\`\`
JSR:
\`\`\`sh
deno add jsr:@hare/decodal-wasm
\`\`\`
## Usage
\`\`\`js
import init, { DecodalLanguageService } from 'decodal-wasm';
await init();
const files = {
'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));
const completion = service.complete(
'main.dcdl',
'App.Server.po',
'App.Server.po'.length,
false,
);
console.log(JSON.parse(completion));
\`\`\`
\`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.
`,
);
writeFileSync(
resolve(packageDir, 'jsr.json'),
JSON.stringify(
{
name: '@hare/decodal-wasm',
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',
exports: './mod.ts',
publish: {
include: [
'README.md',
'LICENSE-MIT',
'LICENSE-APACHE',
'mod.ts',
'decodal_wasm.js',
'decodal_wasm.d.ts',
'decodal_wasm_bg.wasm',
'package.json',
],
},
},
null,
2,
) + '\n',
);
+3 -1
View File
@@ -41,7 +41,7 @@ export function highlightDecodalTokens(source, tokens) {
function tokenClass(kind, text) {
if (['let', 'in', 'fn', 'match', 'import', 'default'].includes(kind)) return 'tok-keyword';
if (kind === 'ident' && ['String', 'Int', 'Float', 'Bool', 'Array'].includes(text)) return 'tok-type';
if (kind === 'ident' && ['String', 'Int', 'Float', 'Bool'].includes(text)) return 'tok-type';
if (kind === 'ident') return '';
if (['true', 'false'].includes(kind)) return 'tok-literal';
if (['int', 'float'].includes(kind)) return 'tok-number';
@@ -60,6 +60,7 @@ function tokenClass(kind, text) {
'pipe_pipe',
'plus',
'plus_plus',
'ellipsis',
'minus',
'star',
'slash',
@@ -255,6 +256,7 @@ function isOperatorStart(char) {
function readOperator(source, start) {
let index = start + 1;
if (source.startsWith('...', start)) return start + 3;
if ('&|/+'.includes(source[start]) && source[index] === source[start]) return index + 1;
if ((source[start] === '<' || source[start] === '>' || source[start] === '=' || source[start] === '!') && source[index] === '=') {
index += 1;
+10 -24
View File
@@ -27,28 +27,14 @@ Production = Server & {
<section class="home-packages" aria-label="Official packages">
<h2>Official packages</h2>
<div class="package-links">
<a href="https://crates.io/crates/decodal">
<span>Rust crate</span>
<strong>crates.io/crates/decodal</strong>
</a>
<a href="https://www.npmjs.com/package/decodal-wasm">
<span>npm package</span>
<strong>npmjs.com/package/decodal-wasm</strong>
</a>
<a href="https://jsr.io/@hare/decodal-wasm@0.1.2">
<span>JSR package</span>
<strong>jsr.io/@hare/decodal-wasm</strong>
</a>
<a href="https://www.npmjs.com/package/decodal-codemirror">
<span>CodeMirror package</span>
<strong>npmjs.com/package/decodal-codemirror</strong>
</a>
<a href="https://jsr.io/@hare/decodal-codemirror@0.1.2">
<span>CodeMirror on JSR</span>
<strong>jsr.io/@hare/decodal-codemirror</strong>
</a>
</div>
<a class="primary-package" href="https://crates.io/crates/decodal">
<span>Rust crate</span>
<strong>crates.io/crates/decodal</strong>
</a>
<ul class="package-list">
<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.6">jsr</a> / <a href="https://www.npmjs.com/package/decodal-codemirror">npm</a></li>
</ul>
</section>
<section class="home-grid" aria-label="Decodal overview">
@@ -62,8 +48,8 @@ Production = Server & {
<article class="home-card">
<h2>Where it runs</h2>
<p>
Use the Rust crate for runtime embedding, the WASM package in browsers, CodeMirror/Lezer for the Web editor,
and Tree-sitter for general editor integration.
Use the Rust crate for runtime embedding, the WASM package in browsers, CodeMirror/Lezer for Web editing
and formatting, and Tree-sitter for general editor integration.
</p>
</article>
<article class="home-card">
+16 -7
View File
@@ -6,16 +6,25 @@ import ManualLayout from '../layouts/ManualLayout.astro';
<div class="playground-header">
<div>
<h1>Playground</h1>
<p>Virtual files are evaluated in the browser through WebAssembly. Use import paths such as <code>./schemas/service.dcdl</code>.</p>
</div>
<div class="playground-actions">
<label class="example-picker">
<span>Example</span>
<select id="example-select" aria-label="Playground example"></select>
</label>
<button id="load-example" type="button">Load</button>
<div class="action-group example-actions">
<label class="example-picker">
<span>Example</span>
<select id="example-select" aria-label="Playground example"></select>
</label>
<button id="load-example" type="button">Load</button>
</div>
<span class="action-separator" aria-hidden="true">|</span>
<div class="action-group run-actions">
<button id="format" disabled>Format</button>
<label class="entry-picker">
<span>Entry</span>
<select id="entry-select" aria-label="Entry point file"></select>
</label>
<button id="run" disabled>Run</button>
</div>
<p id="status" class="status">Loading WASM...</p>
<button id="run" disabled>Run</button>
</div>
</div>

Some files were not shown because too many files have changed in this diff Show More