Compare commits

..

11 Commits

41 changed files with 1672 additions and 83 deletions

17
Cargo.lock generated
View File

@ -25,21 +25,32 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "decodal"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"decodal-derive",
"regex",
]
[[package]]
name = "decodal-cli"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"decodal",
]
[[package]]
name = "decodal-derive"
version = "0.1.1"
dependencies = [
"decodal",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "decodal-wasm"
version = "0.1.0"
version = "0.1.1"
dependencies = [
"decodal",
"serde_json",

View File

@ -3,11 +3,12 @@ members = [
"crates/decodal-core",
"crates/decodal-cli",
"crates/decodal-wasm",
"crates/decodal-derive",
]
resolver = "2"
[workspace.package]
version = "0.1.0"
version = "0.1.1"
edition = "2024"
rust-version = "1.85"
license = "MIT OR Apache-2.0"

View File

@ -20,6 +20,33 @@ Embedded hosts should depend on `decodal` and provide imports with a `SourceLoad
decodal = "0.1"
```
## Derive support
For embedded Rust applications, Decodal can generate a schema and typed decoder from a Rust struct with the `derive` feature.
```toml
[dependencies]
decodal = { version = "0.1", features = ["derive"] }
```
```rust
use decodal::{Decodal, DecodalDecode, DecodalSchema, Engine};
#[derive(Decodal)]
struct Service {
name: String,
#[decodal(gt = 443, default = 8443)]
port: i64,
#[decodal(rename = "feature.enable", default = true)]
feature_enabled: bool,
}
```
The derive implements:
- `DecodalSchema`, which produces a host schema for `Engine::bind_global`
- `DecodalDecode`, which converts materialized `Data` into the Rust struct
## CLI
A standalone CLI is kept in this repository as the `decodal-cli` workspace package.

View File

@ -20,4 +20,4 @@ default = []
regex = ["decodal/regex"]
[dependencies]
decodal = { version = "0.1.0", path = "../decodal-core" }
decodal = { version = "0.1.1", path = "../decodal-core" }

View File

@ -13,7 +13,9 @@ categories = ["config", "parser-implementations"]
[features]
default = ["std"]
std = []
derive = ["dep:decodal-derive"]
regex = ["std", "dep:regex"]
[dependencies]
decodal-derive = { version = "0.1.1", path = "../decodal-derive", optional = true }
regex = { version = "1.10", default-features = false, features = ["std", "unicode-perl"], optional = true }

View File

@ -70,7 +70,10 @@ pub fn normalize_constraints(
if matches!(
primitive,
Some((PrimitiveType::String | PrimitiveType::Bool, _))
Some((
PrimitiveType::String | PrimitiveType::Bool | PrimitiveType::Array,
_
))
) && (lower.is_some() || upper.is_some())
{
let mut diagnostic = Diagnostic::new(

View File

@ -63,6 +63,18 @@ impl HostValue {
)
}
pub fn object_from_paths<I, N>(fields: I) -> Self
where
I: IntoIterator<Item = (N, HostValue)>,
N: Into<String>,
{
let mut root = Vec::new();
for (path, value) in fields {
insert_path(&mut root, &path.into(), value);
}
Self::Object(root)
}
pub fn string_type() -> Self {
Self::abstract_with_constraint(Constraint::Type(PrimitiveType::String))
}
@ -79,6 +91,10 @@ 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 builtin_predicate(name: impl Into<String>) -> Self {
Self::abstract_with_constraint(Constraint::BuiltinPredicate(name.into()))
}
@ -162,3 +178,38 @@ impl HostValue {
self.default(Self::bool(value))
}
}
impl HostField {
pub fn new(name: impl Into<String>, value: HostValue) -> Self {
Self {
name: name.into(),
value,
}
}
}
fn insert_path(fields: &mut Vec<HostField>, path: &str, value: HostValue) {
let mut parts = path.splitn(2, '.');
let Some(head) = parts.next().filter(|part| !part.is_empty()) else {
return;
};
if let Some(tail) = parts.next() {
if let Some(field) = fields.iter_mut().find(|field| field.name == head) {
if let HostValue::Object(children) = &mut field.value {
insert_path(children, tail, value);
} else {
let mut children = Vec::new();
insert_path(&mut children, tail, value);
field.value = HostValue::Object(children);
}
} else {
let mut children = Vec::new();
insert_path(&mut children, tail, value);
fields.push(HostField::new(head, HostValue::Object(children)));
}
} else if let Some(field) = fields.iter_mut().find(|field| field.name == head) {
field.value = value;
} else {
fields.push(HostField::new(head, value));
}
}

View File

@ -1170,6 +1170,7 @@ fn primitive_type(name: &str) -> Option<PrimitiveType> {
"Int" => Some(PrimitiveType::Int),
"Float" => Some(PrimitiveType::Float),
"Bool" => Some(PrimitiveType::Bool),
"Array" => Some(PrimitiveType::Array),
_ => None,
}
}
@ -1250,6 +1251,9 @@ fn value_matches_primitive(value: &RuntimeValue, primitive: PrimitiveType) -> bo
) | (
RuntimeValue::Concrete(ConcreteValue::Bool(_)),
PrimitiveType::Bool
) | (
RuntimeValue::Concrete(ConcreteValue::Array(_)),
PrimitiveType::Array
)
)
}

View File

@ -62,6 +62,7 @@ pub struct Lexer<'a> {
}
impl<'a> Lexer<'a> {
#[cfg(test)]
pub fn new(source: &'a str) -> Self {
Self::with_source_id(SourceId(0), source)
}
@ -93,6 +94,10 @@ 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 {
return Ok(Token {

View File

@ -7,22 +7,28 @@ pub mod constraints;
pub mod diagnostic;
pub mod embedding;
pub mod eval;
pub mod lexer;
mod lexer;
pub mod module;
pub mod parser;
pub mod runtime;
pub mod span;
pub mod typed;
pub use ast::{Ast, BinaryOp, CompareOp, Expr, ExprId, Field, Literal, Param};
pub use constraints::normalize_constraints;
#[cfg(feature = "derive")]
pub use decodal_derive::Decodal;
pub use diagnostic::{Diagnostic, DiagnosticKind, Result};
pub use embedding::{HostField, HostValue};
pub use eval::{Engine, format_diagnostic_with};
pub use lexer::{Lexer, Token, TokenKind};
pub use module::{EmptyLoader, LoadedSource, Module, SourceLoader};
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};
pub use typed::{
DecodalDecode, DecodalSchema, DecodeError, DecodeResult, IntoHostValue, data_at_path,
decode_path, prefix_decode_error,
};
pub fn version() -> &'static str {
env!("CARGO_PKG_VERSION")

View File

@ -79,6 +79,7 @@ pub enum PrimitiveType {
Int,
Float,
Bool,
Array,
}
#[derive(Debug, Clone, PartialEq)]

View File

@ -0,0 +1,227 @@
use alloc::{format, string::String, vec::Vec};
use crate::{Data, HostValue};
pub trait DecodalSchema {
fn decodal_schema() -> HostValue;
}
pub trait DecodalDecode: Sized {
fn decodal_decode(data: &Data) -> DecodeResult<Self>;
}
pub trait IntoHostValue {
fn into_host_value(self) -> HostValue;
}
pub type DecodeResult<T> = core::result::Result<T, DecodeError>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DecodeError {
pub path: String,
pub message: String,
}
impl DecodeError {
pub fn new(path: impl Into<String>, message: impl Into<String>) -> Self {
Self {
path: path.into(),
message: message.into(),
}
}
pub fn at_type(path: &str, expected: &'static str) -> Self {
Self::new(path, format!("expected {expected}"))
}
}
impl core::fmt::Display for DecodeError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
if self.path.is_empty() {
write!(f, "{}", self.message)
} else {
write!(f, "{}: {}", self.path, self.message)
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for DecodeError {}
pub fn decode_path<T: DecodalDecode>(data: &Data, path: &str) -> DecodeResult<T> {
let value = data_at_path(data, path).ok_or_else(|| DecodeError::new(path, "missing field"))?;
T::decodal_decode(value).map_err(|error| prefix_error(path, error))
}
pub fn data_at_path<'a>(data: &'a Data, path: &str) -> Option<&'a Data> {
let mut current = data;
for part in path.split('.') {
if part.is_empty() {
return None;
}
let Data::Object(fields) = current else {
return None;
};
current = &fields.iter().find(|field| field.name == part)?.value;
}
Some(current)
}
pub fn prefix_decode_error(path: &str, error: DecodeError) -> DecodeError {
prefix_error(path, error)
}
fn prefix_error(path: &str, mut error: DecodeError) -> DecodeError {
if error.path.is_empty() {
error.path = String::from(path);
} else if !path.is_empty() {
error.path = format!("{path}.{}", error.path);
}
error
}
impl DecodalSchema for String {
fn decodal_schema() -> HostValue {
HostValue::string_type()
}
}
impl DecodalDecode for String {
fn decodal_decode(data: &Data) -> DecodeResult<Self> {
match data {
Data::String(value) => Ok(value.clone()),
_ => Err(DecodeError::at_type("", "String")),
}
}
}
impl IntoHostValue for String {
fn into_host_value(self) -> HostValue {
HostValue::string(self)
}
}
impl IntoHostValue for &str {
fn into_host_value(self) -> HostValue {
HostValue::string(self)
}
}
macro_rules! impl_int_decode {
($($ty:ty),* $(,)?) => {
$(
impl DecodalSchema for $ty {
fn decodal_schema() -> HostValue {
HostValue::int_type()
}
}
impl DecodalDecode for $ty {
fn decodal_decode(data: &Data) -> DecodeResult<Self> {
match data {
Data::Int(value) => <$ty>::try_from(*value)
.map_err(|_| DecodeError::new("", "integer value is out of range")),
_ => Err(DecodeError::at_type("", "Int")),
}
}
}
impl IntoHostValue for $ty {
fn into_host_value(self) -> HostValue {
HostValue::int(self as i64)
}
}
)*
};
}
impl_int_decode!(i8, i16, i32, i64, u8, u16, u32);
impl DecodalSchema for f64 {
fn decodal_schema() -> HostValue {
HostValue::float_type()
}
}
impl DecodalDecode for f64 {
fn decodal_decode(data: &Data) -> DecodeResult<Self> {
match data {
Data::Float(value) => Ok(*value),
Data::Int(value) => Ok(*value as f64),
_ => Err(DecodeError::at_type("", "Float")),
}
}
}
impl IntoHostValue for f64 {
fn into_host_value(self) -> HostValue {
HostValue::float(self)
}
}
impl DecodalSchema for f32 {
fn decodal_schema() -> HostValue {
HostValue::float_type()
}
}
impl DecodalDecode for f32 {
fn decodal_decode(data: &Data) -> DecodeResult<Self> {
f64::decodal_decode(data).map(|value| value as f32)
}
}
impl IntoHostValue for f32 {
fn into_host_value(self) -> HostValue {
HostValue::float(f64::from(self))
}
}
impl DecodalSchema for bool {
fn decodal_schema() -> HostValue {
HostValue::bool_type()
}
}
impl DecodalDecode for bool {
fn decodal_decode(data: &Data) -> DecodeResult<Self> {
match data {
Data::Bool(value) => Ok(*value),
_ => Err(DecodeError::at_type("", "Bool")),
}
}
}
impl IntoHostValue for bool {
fn into_host_value(self) -> HostValue {
HostValue::bool(self)
}
}
impl<T> DecodalSchema for Vec<T> {
fn decodal_schema() -> HostValue {
HostValue::array_type()
}
}
impl<T: DecodalDecode> DecodalDecode for Vec<T> {
fn decodal_decode(data: &Data) -> DecodeResult<Self> {
match data {
Data::Array(items) => items
.iter()
.enumerate()
.map(|(index, item)| {
T::decodal_decode(item)
.map_err(|error| prefix_error(&format!("[{index}]"), error))
})
.collect(),
_ => Err(DecodeError::at_type("", "Array")),
}
}
}
impl<T: IntoHostValue> IntoHostValue for Vec<T> {
fn into_host_value(self) -> HostValue {
HostValue::array(self.into_iter().map(IntoHostValue::into_host_value))
}
}

View File

@ -0,0 +1,22 @@
[package]
name = "decodal-derive"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
readme.workspace = true
description = "Derive macro for generating Decodal schemas and typed decoders from Rust structs."
keywords = ["decodal", "derive", "proc-macro", "schema"]
categories = ["config", "development-tools::procedural-macro-helpers"]
[lib]
proc-macro = true
[dependencies]
proc-macro2 = "1"
quote = "1"
syn = { version = "2", features = ["full"] }
[dev-dependencies]
decodal = { version = "0.1", path = "../decodal-core" }

View File

@ -0,0 +1,212 @@
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use syn::{
Data, DeriveInput, Expr, Fields, LitStr, Result, Type, parse_macro_input, spanned::Spanned,
};
#[proc_macro_derive(Decodal, attributes(decodal))]
pub fn derive_decodal(input: TokenStream) -> TokenStream {
match expand_decodal(parse_macro_input!(input as DeriveInput)) {
Ok(tokens) => tokens.into(),
Err(error) => error.to_compile_error().into(),
}
}
fn expand_decodal(input: DeriveInput) -> Result<TokenStream2> {
let name = input.ident;
let fields = match input.data {
Data::Struct(data) => match data.fields {
Fields::Named(fields) => fields.named,
_ => {
return Err(syn::Error::new(
data.struct_token.span(),
"Decodal derive supports structs with named fields only",
));
}
},
_ => {
return Err(syn::Error::new(
name.span(),
"Decodal derive supports structs only",
));
}
};
let mut schema_fields = Vec::new();
let mut decode_fields = Vec::new();
for field in fields {
let ident = field.ident.expect("named field");
let ty = field.ty;
let attrs = FieldAttrs::from_attrs(&field.attrs, &ident.to_string())?;
let path = attrs.rename.clone();
let schema_value = schema_expr(&ty, &attrs)?;
let decode_value = decode_expr(&ty, &path, &attrs);
schema_fields.push(quote! {
(#path, { #schema_value })
});
decode_fields.push(quote! {
#ident: #decode_value
});
}
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
Ok(quote! {
impl #impl_generics ::decodal::DecodalSchema for #name #ty_generics #where_clause {
fn decodal_schema() -> ::decodal::HostValue {
::decodal::HostValue::object_from_paths([
#(#schema_fields),*
])
}
}
impl #impl_generics ::decodal::DecodalDecode for #name #ty_generics #where_clause {
fn decodal_decode(data: &::decodal::Data) -> ::decodal::DecodeResult<Self> {
Ok(Self {
#(#decode_fields),*
})
}
}
})
}
#[derive(Default)]
struct FieldAttrs {
rename: String,
default: Option<DefaultAttr>,
constraints: Vec<ConstraintAttr>,
}
impl FieldAttrs {
fn from_attrs(attrs: &[syn::Attribute], fallback_name: &str) -> Result<Self> {
let mut output = Self {
rename: fallback_name.to_string(),
..Self::default()
};
for attr in attrs {
if !attr.path().is_ident("decodal") {
continue;
}
attr.parse_nested_meta(|meta| {
if meta.path.is_ident("rename") {
let value: LitStr = meta.value()?.parse()?;
output.rename = value.value();
return Ok(());
}
if meta.path.is_ident("default") {
if meta.input.peek(syn::Token![=]) {
let expr: Expr = meta.value()?.parse()?;
output.default = Some(DefaultAttr::Expr(expr));
} else {
output.default = Some(DefaultAttr::Default);
}
return Ok(());
}
if let Some(kind) = ConstraintKind::from_path(&meta.path) {
let expr: Expr = meta.value()?.parse()?;
output.constraints.push(ConstraintAttr { kind, expr });
return Ok(());
}
Err(meta.error("unsupported decodal attribute"))
})?;
}
Ok(output)
}
}
enum DefaultAttr {
Default,
Expr(Expr),
}
struct ConstraintAttr {
kind: ConstraintKind,
expr: Expr,
}
#[derive(Clone, Copy)]
enum ConstraintKind {
Gt,
Gte,
Lt,
Lte,
}
impl ConstraintKind {
fn from_path(path: &syn::Path) -> Option<Self> {
if path.is_ident("gt") {
Some(Self::Gt)
} else if path.is_ident("gte") {
Some(Self::Gte)
} else if path.is_ident("lt") {
Some(Self::Lt)
} else if path.is_ident("lte") {
Some(Self::Lte)
} else {
None
}
}
fn method(self) -> syn::Ident {
match self {
Self::Gt => format_ident!("gt"),
Self::Gte => format_ident!("gte"),
Self::Lt => format_ident!("lt"),
Self::Lte => format_ident!("lte"),
}
}
}
fn schema_expr(ty: &Type, attrs: &FieldAttrs) -> Result<TokenStream2> {
let mut tokens = quote! {
let mut schema = <#ty as ::decodal::DecodalSchema>::decodal_schema();
};
for constraint in &attrs.constraints {
let method = constraint.kind.method();
let expr = &constraint.expr;
tokens.extend(quote! {
schema = schema.#method(#expr);
});
}
if let Some(default) = &attrs.default {
let default_expr = match default {
DefaultAttr::Default => quote! { <#ty as ::core::default::Default>::default() },
DefaultAttr::Expr(expr) => quote! { (#expr) },
};
tokens.extend(quote! {
schema = schema
.default(::decodal::IntoHostValue::into_host_value(#default_expr))
.expect("Decodal derive generated a valid default");
});
}
tokens.extend(quote! { schema });
Ok(tokens)
}
fn decode_expr(ty: &Type, path: &str, attrs: &FieldAttrs) -> TokenStream2 {
let missing = if let Some(default) = &attrs.default {
match default {
DefaultAttr::Default => quote! { <#ty as ::core::default::Default>::default() },
DefaultAttr::Expr(expr) => quote! { ::core::convert::Into::into(#expr) },
}
} else {
quote! { return Err(::decodal::DecodeError::new(#path, "missing field")); }
};
quote! {
if let Some(value) = ::decodal::data_at_path(data, #path) {
<#ty as ::decodal::DecodalDecode>::decodal_decode(value)
.map_err(|error| ::decodal::prefix_decode_error(#path, error))?
} else {
#missing
}
}
}

View File

@ -0,0 +1,79 @@
use decodal::{Data, DecodalDecode, DecodalSchema, EmptyLoader, Engine};
use decodal_derive::Decodal;
#[derive(Debug, PartialEq, Decodal)]
struct Service {
name: String,
#[decodal(gt = 443, default = 8443)]
port: i64,
#[decodal(default = true, rename = "feature.enable")]
feature_enabled: bool,
#[decodal(default)]
tags: Vec<String>,
}
#[test]
fn derives_schema_and_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;
feature.enable = false;
tags = ["web", "prod"];
}
"#,
)
.unwrap();
let value = engine.eval_module(module).unwrap();
let data = engine.materialize(&value).unwrap();
let service = Service::decodal_decode(&data).unwrap();
assert_eq!(
service,
Service {
name: "api".into(),
port: 9443,
feature_enabled: false,
tags: vec!["web".into(), "prod".into()],
}
);
}
#[test]
fn defaults_are_available_in_schema() {
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";
}
"#,
)
.unwrap();
let value = engine.eval_module(module).unwrap();
let data = engine.materialize(&value).unwrap();
let service = Service::decodal_decode(&data).unwrap();
assert_eq!(service.port, 8443);
assert!(service.feature_enabled);
assert!(service.tags.is_empty());
}
#[test]
fn decode_reports_field_path() {
let data = Data::Object(vec![]);
let error = Service::decodal_decode(&data).unwrap_err();
assert_eq!(error.path, "name");
}

View File

@ -15,7 +15,7 @@ publish = false
crate-type = ["cdylib", "rlib"]
[dependencies]
decodal = { version = "0.1.0", path = "../decodal-core" }
decodal = { version = "0.1.1", path = "../decodal-core" }
serde_json.workspace = true
wasm-bindgen.workspace = true

View File

@ -0,0 +1,94 @@
# Components
Decodal is split into a small runtime core and separate syntax tooling components.
The public surface is intentionally organized by use case: execute Decodal with Rust or WebAssembly, edit Decodal on the Web with Lezer and CodeMirror, and integrate Decodal into general-purpose editors with Tree-sitter.
## Runtime components
### Rust crate
The `decodal` crate is the primary Rust runtime and embedding API.
It owns parsing, evaluation, materialization, diagnostics, host-provided values, and schema/decode traits.
Rust applications should use this crate when they want to load Decodal source, evaluate it, or embed Decodal into a host program.
Important paths:
```text
crates/decodal-core/
crates/decodal-derive/
```
`decodal-derive` provides optional derive macros for Rust struct integration.
It is a companion to the runtime crate rather than an editor or syntax-highlighting component.
### WebAssembly package
`decodal-wasm` exposes the runtime to browsers.
The documentation site playground uses it to evaluate Decodal entirely in the browser.
Important paths:
```text
crates/decodal-wasm/
site/decodal-site/src/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 WebAssembly package is for execution, not syntax highlighting.
## 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.
Important paths:
```text
editors/lezer-decodal/decodal.grammar
site/decodal-site/src/lib/codemirror/decodal-parser.js
site/decodal-site/src/lib/codemirror/decodal-parser.terms.js
site/decodal-site/src/lib/codemirror/decodal.js
```
The Lezer grammar is derived from the canonical grammar documentation, but it is not a literal copy of the EBNF.
Precedence and token conflict handling are represented in the Lezer grammar in the form CodeMirror needs.
## General editor components
Tree-sitter is the portable editor-integration grammar.
Editors such as Zed, Neovim, Helix, and Emacs should consume this component when they need Decodal parsing or highlighting outside the Web playground.
Important paths:
```text
editors/tree-sitter-decodal/grammar.js
editors/tree-sitter-decodal/queries/highlights.scm
editors/tree-sitter-decodal/queries/locals.scm
editors/tree-sitter-decodal/src/
```
The generated parser sources under `editors/tree-sitter-decodal/src/` are committed so editor integrations can consume the grammar without regenerating it first.
## Canonical grammar
The human-readable grammar lives in:
```text
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
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:
- Rust execution and embedding: `decodal`
- Browser execution: `decodal-wasm`
- Web editor syntax: Lezer / CodeMirror
- 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.

View File

@ -17,7 +17,7 @@ let / function env
```
Module top-level bindings shadow prelude 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.
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.
## Global bindings
@ -87,6 +87,46 @@ Concrete(Object {
This matches the runtime model used for Decodal source-defined schema objects.
## Typed Rust integration
Hosts can enable the `derive` feature on `decodal` to keep a Rust struct, the Decodal schema, and the decoded result in sync.
The derive implements two traits from the `decodal` crate:
- `DecodalSchema`: builds a `HostValue` schema that can be passed to `Engine::bind_global`.
- `DecodalDecode`: decodes materialized `Data` back into the Rust type.
```rust
use decodal::{Decodal, DecodalDecode, DecodalSchema, EmptyLoader, Engine};
#[derive(Decodal)]
struct Service {
name: String,
#[decodal(gt = 443, default = 8443)]
port: i64,
#[decodal(rename = "feature.enable", default = true)]
feature_enabled: bool,
}
let mut engine = Engine::new(EmptyLoader);
engine.bind_global("Service", Service::decodal_schema())?;
let value = engine.eval_module(module)?;
let data = engine.materialize(&value)?;
let service = Service::decodal_decode(&data)?;
```
Supported field attributes are intentionally small:
- `rename = "path.to.field"`
- `default`
- `default = value`
- numeric constraints: `gt`, `gte`, `lt`, `lte`
The derive does not add host callbacks or reflection.
It only generates schema construction and typed decoding code.
## SourceLoader and prelude together
`SourceLoader` and host prelude bindings are independent mechanisms.

View File

@ -23,10 +23,12 @@ 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.
Workspace support crates such as `decodal-cli` and `decodal-wasm` are not published for 0.1.0.
The project is dual licensed as `MIT OR Apache-2.0`.
```sh
cargo publish -p decodal-derive
cargo publish -p decodal
```
@ -36,6 +38,7 @@ Before publishing, run:
cargo fmt --check
cargo test
cargo check -p decodal --no-default-features
cargo publish -p decodal-derive --dry-run
cargo publish -p decodal --dry-run
```
@ -55,8 +58,13 @@ Important files:
```text
site/decodal-site/src/pages/docs/[...slug].astro
site/decodal-site/src/pages/playground.astro
site/decodal-site/src/scripts/playground.js
site/decodal-site/src/scripts/playground-examples.js
site/decodal-site/src/layouts/ManualLayout.astro
site/decodal-site/src/lib/codemirror/decodal.js
site/decodal-site/src/lib/codemirror/decodal-parser.js
site/decodal-site/src/lib/docs.js
site/decodal-site/src/lib/highlight.js
crates/decodal-wasm/src/lib.rs
```
@ -77,6 +85,10 @@ 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.
The playground editor uses CodeMirror 6 with the generated Lezer parser in `src/lib/codemirror/decodal-parser.js`.
The canonical grammar is documented in `doc/manual/souce/language/grammar.md`; regenerate the Lezer parser when that grammar or `editors/lezer-decodal/decodal.grammar` changes.
The documentation build still uses the lightweight JavaScript fallback highlighter so Astro can render Markdown without initializing WASM at build time.
To run the site locally:
```sh
@ -149,12 +161,18 @@ The generated parser files under `editors/tree-sitter-decodal/src/` are committe
When the Decodal syntax changes:
1. Update `grammar.js`.
2. Run `npx tree-sitter generate`.
3. Add or update corpus tests in `corpus/`.
4. Update highlight queries in `queries/highlights.scm` if token names changed.
5. Run `npx tree-sitter test`.
6. Run Rust checks from the repository root if the language parser or examples also changed.
1. Update the canonical EBNF in `doc/manual/souce/language/grammar.md`.
2. Update the Rust parser/lexer as needed.
3. Update `editors/tree-sitter-decodal/grammar.js` and run `npx tree-sitter generate` / `npx tree-sitter test`.
4. Update `editors/lezer-decodal/decodal.grammar` and regenerate the CodeMirror parser:
```sh
cd site/decodal-site
npx lezer-generator ../../editors/lezer-decodal/decodal.grammar -o src/lib/codemirror/decodal-parser.js
```
5. Add or update corpus/tests/examples.
6. Run Rust and site checks from the repository root.
## Development shell

View File

@ -6,7 +6,8 @@ Decodal は Deferred Constraint Data Language、略称 DCDL のプロジェク
## 目次
1. [Introduction](./introduction.md)
2. [Language Specification](./language/index.md)
2. [Components](./components.md)
3. [Language Specification](./language/index.md)
1. [Lexical Structure and Syntax](./language/syntax.md)
2. [Value](./language/value/index.md)
1. [String](./language/value/string.md)
@ -35,7 +36,7 @@ Decodal は Deferred Constraint Data Language、略称 DCDL のプロジェク
9. [Materialization and Errors](./language/materialization-and-errors.md)
10. [Naming Conventions](./language/naming.md)
11. [Examples](./language/examples.md)
3. [Implementation Design](./design/index.md)
4. [Implementation Design](./design/index.md)
1. [Execution Pipeline](./design/execution-pipeline.md)
2. [Runtime Model](./design/runtime-model.md)
3. [Thunk and Lazy Evaluation](./design/thunk-and-lazy-evaluation.md)
@ -43,5 +44,5 @@ Decodal は Deferred Constraint Data Language、略称 DCDL のプロジェク
5. [Diagnostics and Fallback](./design/diagnostics-and-fallback.md)
6. [Embedding API](./design/embedding-api.md)
7. [Features](./design/features.md)
4. [Development](./development.md)
5. [Open Issues](./open-issues.md)
5. [Development](./development.md)
6. [Open Issues](./open-issues.md)

View File

@ -0,0 +1,111 @@
# Grammar
This page is the canonical grammar reference for Decodal syntax.
Parser implementations such as the Rust parser, Tree-sitter grammar, and Lezer grammar should follow this grammar and may add implementation-specific precedence annotations where needed.
## Lexical grammar
```ebnf
source_character = ? any Unicode scalar value ? ;
newline = "\n" | "\r\n" | "\r" ;
space = " " | "\t" | newline ;
comment = "#" , { ? any character except newline ? } ;
digit = "0" … "9" ;
letter = "A" … "Z" | "a" … "z" ;
identifier = letter , { letter | digit | "_" } ;
integer = digit , { digit } ;
float = digit , { digit } , "." , digit , { digit } ;
string = '"' , { string_character | escape } , '"' ;
string_character = ? any character except '"', "\\", or newline ? ;
escape = "\\" , source_character ;
regex = "/" , regex_character , { regex_character } , "/" ;
regex_character = escape | ? any character except "/", "\\", or newline ? ;
```
Whitespace and comments separate tokens and are otherwise ignored by the parser.
## Syntactic grammar
```ebnf
module = { statement } ;
statement = field_definition , [ ";" ]
| expression , [ ";" ] ;
expression = default_expression ;
default_expression = patch_expression , [ "default" , default_expression ] ;
patch_expression = compose_expression , { "//" , compose_expression } ;
compose_expression = logical_or_expression , { "&" , logical_or_expression } ;
logical_or_expression = logical_and_expression , { "||" , logical_and_expression } ;
logical_and_expression = comparison_expression , { "&&" , comparison_expression } ;
comparison_expression = concat_expression , [ comparison_operator , concat_expression ] ;
concat_expression = additive_expression , { "++" , additive_expression } ;
additive_expression = multiplicative_expression , { ( "+" | "-" ) , multiplicative_expression } ;
multiplicative_expression = unary_expression , { ( "*" | "/" ) , unary_expression } ;
unary_expression = [ "!" | "-" ] , postfix_expression ;
postfix_expression = primary_expression , { call_suffix | path_suffix } ;
call_suffix = "(" , [ argument_list ] , ")" ;
path_suffix = "." , identifier ;
primary_expression = literal
| identifier
| comparison_constraint
| object
| array
| let_expression
| function_expression
| match_expression
| import_expression
| "(" , expression , ")" ;
literal = string | integer | float | "true" | "false" | regex ;
comparison_operator = "==" | "!=" | "<" | "<=" | ">" | ">=" ;
comparison_constraint = ( "<" | "<=" | ">" | ">=" ) , expression ;
object = "{" , [ field_definition , { ";" , field_definition } , [ ";" ] ] , "}" ;
field_definition = field_path , "=" , expression ;
field_path = identifier , { "." , identifier } ;
array = "[" , [ expression , { "," , expression } , [ "," ] ] , "]" ;
let_expression = "let" , { field_definition , ";" } , "in" , expression ;
function_expression = "(" , [ parameter_list ] , ")" , "=>" , expression ;
parameter_list = parameter , { "," , parameter } , [ "," ] ;
parameter = identifier , [ ":" , expression ] ;
match_expression = "match" , expression , "{" , [ match_arm , { ";" , match_arm } , [ ";" ] ] , "}" ;
match_arm = pattern , ":" , expression ;
pattern = "_" | expression ;
import_expression = "import" , string ;
argument_list = expression , { "," , expression } , [ "," ] ;
```
## Precedence
Precedence is highest first.
1. function call and field path reference
2. unary `!` and `-`
3. `*` and `/`
4. `+` and `-`
5. `++`
6. `==`, `!=`, `<`, `<=`, `>`, `>=`
7. `&&`
8. `||`
9. `&`
10. `//`
11. `default`
Binary operators are left-associative except `default`, which is right-associative.
## Tooling mapping
Syntax tooling should derive token categories from this grammar rather than making a tool-specific grammar canonical.
Tree-sitter and Lezer grammars are implementation artifacts that follow this page.

View File

@ -9,9 +9,10 @@ name = String;
retry = Int default 3;
ratio = Float;
enable = Bool default true;
tags = Array;
```
現在の primitive type は `String`、`Int`、`Float`、`Bool` である。
現在の primitive type は `String`、`Int`、`Float`、`Bool`、`Array` である。
各型の個別仕様へのリンクは [Manual Index](../../index.md) に集約する。
primitive type と制約合成の詳細は [制約と default](../constraints-and-defaults.md) も参照する。

View File

@ -0,0 +1,18 @@
# lezer-decodal
Lezer implementation of the Decodal grammar for CodeMirror 6.
The canonical grammar is documented in:
```text
../../doc/manual/souce/language/grammar.md
```
Regenerate the CodeMirror parser from the site directory:
```sh
cd ../../site/decodal-site
npx lezer-generator ../../editors/lezer-decodal/decodal.grammar -o src/lib/codemirror/decodal-parser.js
```
The generated parser is committed with the site because the playground imports it directly.

View File

@ -0,0 +1,172 @@
@top Source { Statement* }
Statement {
FieldDefinition Semicolon? |
Expression Semicolon?
}
Expression { DefaultExpression }
DefaultExpression {
PatchExpression |
PatchExpression !default Default DefaultExpression
}
PatchExpression {
ComposeExpression |
PatchExpression !patch SlashSlash ComposeExpression
}
ComposeExpression {
LogicalOrExpression |
ComposeExpression !compose Amp LogicalOrExpression
}
LogicalOrExpression {
LogicalAndExpression |
LogicalOrExpression !or PipePipe LogicalAndExpression
}
LogicalAndExpression {
ComparisonExpression |
LogicalAndExpression !and AmpAmp ComparisonExpression
}
ComparisonExpression {
ConcatExpression |
ConcatExpression !compare CompareOperator ConcatExpression
}
ConcatExpression {
AdditiveExpression |
ConcatExpression !concat PlusPlus AdditiveExpression
}
AdditiveExpression {
MultiplicativeExpression |
AdditiveExpression !add (Plus | Minus) MultiplicativeExpression
}
MultiplicativeExpression {
UnaryExpression |
MultiplicativeExpression !multiply (Star | Slash) UnaryExpression
}
UnaryExpression {
PostfixExpression |
(Bang | Minus) !unary UnaryExpression
}
PostfixExpression {
PrimaryExpression |
PostfixExpression !call CallSuffix |
PostfixExpression !path Dot Identifier
}
CallSuffix { LParen ArgumentList? RParen }
ArgumentList { Expression (Comma Expression)* Comma? }
PrimaryExpression {
Literal |
Identifier |
ComparisonConstraint |
Object |
Array |
LetExpression |
FunctionExpression |
MatchExpression |
ImportExpression |
LParen Expression RParen
}
Literal { String | Integer | Float | True | False | Regex }
CompareOperator { EqualEqual | BangEqual | Lt | Lte | Gt | Gte }
ComparisonConstraint { (Lt | Lte | Gt | Gte) Expression }
Object { LBrace (FieldDefinition (Semicolon FieldDefinition)* Semicolon?)? RBrace }
FieldDefinition { FieldPath Equal Expression }
FieldPath { Identifier !fieldPath (Dot Identifier)* }
Array { LBracket (Expression (Comma Expression)* Comma?)? RBracket }
LetExpression { Let FieldDefinitionList In Expression }
FieldDefinitionList { (FieldDefinition Semicolon)* }
FunctionExpression { LParen ParameterList? RParen Arrow Expression }
ParameterList { Parameter (Comma Parameter)* Comma? }
Parameter { Identifier Colon Expression }
MatchExpression { Match Expression LBrace (MatchArm (Semicolon MatchArm)* Semicolon?)? RBrace }
MatchArm { Pattern Colon Expression }
Pattern { Underscore | Expression }
ImportExpression { Import String }
@precedence {
fieldPath @left,
default @right,
patch @left,
compose @left,
or @left,
and @left,
compare @left,
concat @left,
add @left,
multiply @left,
unary,
call @left,
path @left
}
@tokens {
Let { "let" }
In { "in" }
Match { "match" }
Import { "import" }
Default { "default" }
True { "true" }
False { "false" }
Identifier { $[A-Za-z_] $[A-Za-z0-9_]* }
Integer { $[0-9]+ }
Float { $[0-9]+ "." $[0-9]+ }
String { '"' (!["\\\n] | "\\" _)* '"' }
Regex { "/" (![/\\\n] | "\\" _)+ "/" }
Comment { "#" ![\n]* }
LBrace { "{" }
RBrace { "}" }
LBracket { "[" }
RBracket { "]" }
LParen { "(" }
RParen { ")" }
Semicolon { ";" }
Comma { "," }
Dot { "." }
Colon { ":" }
Equal { "=" }
EqualEqual { "==" }
Bang { "!" }
BangEqual { "!=" }
Arrow { "=>" }
Amp { "&" }
AmpAmp { "&&" }
PipePipe { "||" }
Plus { "+" }
PlusPlus { "++" }
Minus { "-" }
Star { "*" }
Slash { "/" }
SlashSlash { "//" }
Gt { ">" }
Gte { ">=" }
Lt { "<" }
Lte { "<=" }
Underscore { "_" }
whitespace { @whitespace+ }
@precedence { Let, In, Match, Import, Default, True, False, Float, Integer, Underscore, Identifier }
}
@skip { whitespace | Comment }

View File

@ -0,0 +1,12 @@
# tree-sitter-decodal
Tree-sitter implementation of the Decodal grammar.
The canonical grammar is documented in:
```text
../../doc/manual/souce/language/grammar.md
```
This grammar is an editor/tooling implementation of that EBNF reference.
Generated parser files under `src/` are committed.

View File

@ -1,3 +1,6 @@
// Tree-sitter implementation of the canonical EBNF grammar in
// doc/manual/souce/language/grammar.md.
const PREC = {
DEFAULT: 1,
PATCH: 2,

View File

@ -24,7 +24,7 @@ let
in
rustPlatform.buildRustPackage {
pname = "decodal";
version = "0.1.0";
version = "0.1.1";
src = lib.cleanSourceWith {
src = srcRoot;

View File

@ -9,10 +9,17 @@
"version": "0.1.0",
"dependencies": {
"@astrojs/check": "^0.9.4",
"@codemirror/language": "^6.12.4",
"@codemirror/state": "^6.7.1",
"@codemirror/view": "^6.43.6",
"@lezer/highlight": "^1.2.3",
"@lezer/lr": "^1.4.10",
"astro": "^4.16.18",
"codemirror": "^6.0.2",
"marked": "^12.0.2"
},
"devDependencies": {
"@lezer/generator": "^1.8.0",
"wrangler": "^4.104.0"
}
},
@ -560,6 +567,87 @@
"node": ">=16"
}
},
"node_modules/@codemirror/autocomplete": {
"version": "6.20.3",
"resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.3.tgz",
"integrity": "sha512-tlosUqb+3BbxCxZdu4tKeRghPFC+QM7q4X5YhKV2eCmPG+1r2F3f4AaSz5sCrFqUtX4Jh20VFTKecl16MgiV9g==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.17.0",
"@lezer/common": "^1.0.0"
}
},
"node_modules/@codemirror/commands": {
"version": "6.10.4",
"resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.4.tgz",
"integrity": "sha512-Ryk9y9T0FFVF0cUGhAknveAyUOl/A1qReTFi+qPKtOh2Z9F4AUBz3XOrYD4ZEgZirdugVzHvd/2/Wcwy5OliTg==",
"license": "MIT",
"dependencies": {
"@codemirror/language": "^6.0.0",
"@codemirror/state": "^6.7.0",
"@codemirror/view": "^6.27.0",
"@lezer/common": "^1.1.0"
}
},
"node_modules/@codemirror/language": {
"version": "6.12.4",
"resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.4.tgz",
"integrity": "sha512-1q4PaT+o6PbgpkJt4Q8Fv5XJxTy4FUZ4MWETtyiDw3J0Pyr9E2vqcKL+k9wcvjNTIsauxvE7OfmWj3FRPHQ76A==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.23.0",
"@lezer/common": "^1.5.0",
"@lezer/highlight": "^1.0.0",
"@lezer/lr": "^1.0.0",
"style-mod": "^4.0.0"
}
},
"node_modules/@codemirror/lint": {
"version": "6.9.7",
"resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.7.tgz",
"integrity": "sha512-28/+iWLYxKxsvGYhSYL7zaCZqLz5+FFFDq9tVsvGv9kv8RY4fFAchJ5WX9M3YrrRlTIsECjsXPqeNgnSmNP2dg==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.42.0",
"crelt": "^1.0.5"
}
},
"node_modules/@codemirror/search": {
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.1.tgz",
"integrity": "sha512-uMe5UO6PamJtSHrXhhHOzSX3ReWtiJrva6GnPMwSOrZtiExb5X5eExhr2OUZQVvdxPsKpY3Ro2mFbQadpPWmHA==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.37.0",
"crelt": "^1.0.5"
}
},
"node_modules/@codemirror/state": {
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.7.1.tgz",
"integrity": "sha512-9QzNDgE4EYDnAHfrTlR2lwiPciiOymLtwKK+8yHQzCc7GXhAP9xdEbEJFy2IWB1j9UGUl9BsgMmTo/ImA02T7A==",
"license": "MIT",
"dependencies": {
"@marijn/find-cluster-break": "^1.0.0"
}
},
"node_modules/@codemirror/view": {
"version": "6.43.6",
"resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.43.6.tgz",
"integrity": "sha512-EVunGSYN1wz1p75WY1s3Xg7t3i8Yol0kGZGizNdX9BUFgMFILYVe8/u6EVpo7Ff5PwbZuILb4QAq7IZoKzIEQA==",
"license": "MIT",
"dependencies": {
"@codemirror/state": "^6.7.0",
"crelt": "^1.0.6",
"style-mod": "^4.1.0",
"w3c-keyname": "^2.2.4"
}
},
"node_modules/@cspotcode/source-map-support": {
"version": "0.8.1",
"resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz",
@ -1632,6 +1720,50 @@
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@lezer/common": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz",
"integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==",
"license": "MIT"
},
"node_modules/@lezer/generator": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@lezer/generator/-/generator-1.8.0.tgz",
"integrity": "sha512-/SF4EDWowPqV1jOgoGSGTIFsE7Ezdr7ZYxyihl5eMKVO5tlnpIhFcDavgm1hHY5GEonoOAEnJ0CU0x+tvuAuUg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.1.0",
"@lezer/lr": "^1.3.0"
},
"bin": {
"lezer-generator": "src/lezer-generator.cjs"
}
},
"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==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.3.0"
}
},
"node_modules/@lezer/lr": {
"version": "1.4.10",
"resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz",
"integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==",
"license": "MIT",
"dependencies": {
"@lezer/common": "^1.0.0"
}
},
"node_modules/@marijn/find-cluster-break": {
"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==",
"license": "MIT"
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -2981,6 +3113,21 @@
"node": ">=6"
}
},
"node_modules/codemirror": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz",
"integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==",
"license": "MIT",
"dependencies": {
"@codemirror/autocomplete": "^6.0.0",
"@codemirror/commands": "^6.0.0",
"@codemirror/language": "^6.0.0",
"@codemirror/lint": "^6.0.0",
"@codemirror/search": "^6.0.0",
"@codemirror/state": "^6.0.0",
"@codemirror/view": "^6.0.0"
}
},
"node_modules/color": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
@ -3055,6 +3202,12 @@
"node": ">= 0.6"
}
},
"node_modules/crelt": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.7.tgz",
"integrity": "sha512-aK6BbWfhf4U/wCcLHKPJl/xa6VkVstRaPywWtMKGwuOLc/wZTyQYuoxgvZnNsBvv7Kg3YTBQYYBCggcviQczuA==",
"license": "MIT"
},
"node_modules/cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@ -6429,6 +6582,12 @@
"node": ">=0.10.0"
}
},
"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==",
"license": "MIT"
},
"node_modules/supports-color": {
"version": "10.2.2",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz",
@ -7126,6 +7285,12 @@
"integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==",
"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==",
"license": "MIT"
},
"node_modules/web-namespaces": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz",

View File

@ -8,15 +8,22 @@
"build": "astro build",
"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 && wrangler pages deploy dist --project-name ${CLOUDFLARE_PROJECT_NAME:-decodal-site} --branch ${CLOUDFLARE_BRANCH:-master}",
"deploy": "npm run build && node scripts/deploy-pages.mjs",
"deploy:wasm": "npm run build:wasm && npm run deploy"
},
"dependencies": {
"@astrojs/check": "^0.9.4",
"@codemirror/language": "^6.12.4",
"@codemirror/state": "^6.7.1",
"@codemirror/view": "^6.43.6",
"@lezer/highlight": "^1.2.3",
"@lezer/lr": "^1.4.10",
"astro": "^4.16.18",
"codemirror": "^6.0.2",
"marked": "^12.0.2"
},
"devDependencies": {
"@lezer/generator": "^1.8.0",
"wrangler": "^4.104.0"
}
}

View File

@ -0,0 +1,12 @@
import { spawnSync } from 'node:child_process';
const projectName = process.env.CLOUDFLARE_PROJECT_NAME ?? 'decodal-site';
const branch = process.env.CLOUDFLARE_BRANCH ?? 'master';
const result = spawnSync(
'wrangler',
['pages', 'deploy', 'dist', '--project-name', projectName, '--branch', branch],
{ stdio: 'inherit', shell: process.platform === 'win32' },
);
process.exit(result.status ?? 1);

View File

@ -0,0 +1,16 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
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,
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~",
tokenizers: [0, 1],
topRules: {"Source":[0,2]},
tokenPrec: 2481
})

View File

@ -0,0 +1,77 @@
// This file was generated by lezer-generator. You probably shouldn't edit it.
export const
Comment = 1,
Source = 2,
Statement = 3,
FieldDefinition = 4,
FieldPath = 5,
Identifier = 6,
Dot = 7,
Equal = 8,
Expression = 9,
DefaultExpression = 10,
PatchExpression = 11,
ComposeExpression = 12,
LogicalOrExpression = 13,
LogicalAndExpression = 14,
ComparisonExpression = 15,
ConcatExpression = 16,
AdditiveExpression = 17,
MultiplicativeExpression = 18,
UnaryExpression = 19,
PostfixExpression = 20,
PrimaryExpression = 21,
Literal = 22,
String = 23,
Integer = 24,
Float = 25,
True = 26,
False = 27,
Regex = 28,
ComparisonConstraint = 29,
Lt = 30,
Lte = 31,
Gt = 32,
Gte = 33,
Object = 34,
LBrace = 35,
Semicolon = 36,
RBrace = 37,
Array = 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

View File

@ -0,0 +1,69 @@
import { LRLanguage, LanguageSupport, HighlightStyle, syntaxHighlighting, foldNodeProp, indentNodeProp } from '@codemirror/language';
import { styleTags, tags as t } from '@lezer/highlight';
import { parser } from './decodal-parser.js';
const parserWithMetadata = parser.configure({
props: [
styleTags({
'Let In Match Import Default': t.keyword,
'True False': t.bool,
Identifier: t.variableName,
String: t.string,
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,
'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,
LetExpression: context => context.column(context.node.from) + context.unit,
}),
foldNodeProp.add({
Object: foldDelimited('{', '}'),
Array: foldDelimited('[', ']'),
MatchExpression: foldDelimited('{', '}'),
}),
],
});
function foldDelimited(open, close) {
return (node, state) => {
const text = state.doc.sliceString(node.from, node.to);
const openIndex = text.indexOf(open);
const closeIndex = text.lastIndexOf(close);
if (openIndex < 0 || closeIndex <= openIndex) return null;
const from = node.from + openIndex + open.length;
const to = node.from + closeIndex;
return from < to ? { from, to } : null;
};
}
export const decodalLanguage = LRLanguage.define({
parser: parserWithMetadata,
languageData: {
commentTokens: { line: '#' },
closeBrackets: { brackets: ['(', '[', '{', '"'] },
},
});
export const decodalHighlightStyle = HighlightStyle.define([
{ tag: t.keyword, color: '#93c5fd', fontWeight: '700' },
{ tag: t.variableName, color: 'inherit' },
{ tag: t.bool, color: '#fbbf24' },
{ tag: t.number, color: '#fbbf24' },
{ tag: t.string, color: '#86efac' },
{ tag: t.regexp, color: '#86efac' },
{ tag: t.lineComment, color: '#94a3b8', fontStyle: 'italic' },
{ tag: t.operator, color: '#f9a8d4' },
{ tag: [t.brace, t.squareBracket, t.paren, t.punctuation], color: '#f9a8d4' },
]);
export function decodal() {
return new LanguageSupport(decodalLanguage, [syntaxHighlighting(decodalHighlightStyle)]);
}

View File

@ -19,11 +19,13 @@ export const docs = Object.fromEntries(
export const nav = [
{ title: 'Introduction', slug: 'introduction' },
{ title: 'Components', slug: 'components' },
{
title: 'Language Specification',
slug: 'language',
children: [
{ title: 'Syntax', slug: 'language/syntax' },
{ title: 'Grammar', slug: 'language/grammar' },
{
title: 'Value',
slug: 'language/value',

View File

@ -25,6 +25,66 @@ export function highlightCode(code, language = '') {
return escapeHtml(code);
}
export function highlightDecodalTokens(source, tokens) {
let html = '';
let cursor = 0;
for (const token of tokens) {
if (token.start > cursor) html += escapeHtml(source.slice(cursor, token.start));
const text = source.slice(token.start, token.end);
const kind = tokenClass(token.kind, text);
html += kind ? `<span class="${kind}">${escapeHtml(text)}</span>` : escapeHtml(text);
cursor = token.end;
}
if (cursor < source.length) html += escapeHtml(source.slice(cursor));
return html;
}
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') return '';
if (['true', 'false'].includes(kind)) return 'tok-literal';
if (['int', 'float'].includes(kind)) return 'tok-number';
if (kind === 'string') return 'tok-string';
if (kind === 'regex') return 'tok-regex';
if (kind === 'comment') return 'tok-comment';
if (
[
'equal',
'equal_equal',
'bang',
'bang_equal',
'arrow',
'amp',
'amp_amp',
'pipe_pipe',
'plus',
'plus_plus',
'minus',
'star',
'slash',
'slash_slash',
'gt',
'gte',
'lt',
'lte',
'dot',
'colon',
'semicolon',
'comma',
'l_brace',
'r_brace',
'l_bracket',
'r_bracket',
'l_paren',
'r_paren',
].includes(kind)
) {
return 'tok-operator';
}
return '';
}
export function highlightDecodal(source) {
let html = '';
let index = 0;

View File

@ -29,13 +29,10 @@ import ManualLayout from '../layouts/ManualLayout.astro';
<button id="delete-file" class="danger-button" type="button">Delete file</button>
</aside>
<label class="pane input-pane">
<section class="pane input-pane">
<span id="active-file">Input</span>
<div class="editor-wrap">
<pre id="source-highlight" aria-hidden="true"></pre>
<textarea id="source" spellcheck="false"></textarea>
</div>
</label>
<div id="editor" class="code-editor"></div>
</section>
<section class="pane output-pane">
<span>Output</span>

View File

@ -1,12 +1,13 @@
import init, { evaluateProject } from '../wasm/decodal_wasm.js';
import { highlightDecodal } from '../lib/highlight.js';
import { EditorView, basicSetup } from 'codemirror';
import { keymap } from '@codemirror/view';
import { decodal } from '../lib/codemirror/decodal.js';
import { playgroundExamples } from './playground-examples.js';
const STORAGE_KEY = 'decodal-playground-project-v1';
const starterProject = playgroundExamples[0];
const source = document.getElementById('source');
const sourceHighlight = document.getElementById('source-highlight');
const editorHost = document.getElementById('editor');
const output = document.getElementById('output');
const run = document.getElementById('run');
const status = document.getElementById('status');
@ -19,6 +20,63 @@ const loadExample = document.getElementById('load-example');
const project = loadProject();
const editorTheme = EditorView.theme({
'&': {
backgroundColor: '#0f172a',
color: '#e5e7eb',
height: '100%',
},
'.cm-scroller': {
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace',
fontSize: '14px',
lineHeight: '1.5',
overflow: 'auto',
},
'.cm-content': {
caretColor: '#e5e7eb',
minHeight: '100%',
padding: '12px',
},
'.cm-gutters': {
backgroundColor: '#111827',
borderRight: '1px solid #334155',
color: '#94a3b8',
},
'&.cm-focused': {
outline: 'none',
},
'&.cm-focused .cm-cursor': {
borderLeftColor: '#e5e7eb',
},
'&.cm-focused .cm-selectionBackground, .cm-selectionBackground, ::selection': {
backgroundColor: 'rgb(59 130 246 / 0.35)',
},
}, { dark: true });
const editor = new EditorView({
doc: project.files[project.activePath] ?? '',
parent: editorHost,
extensions: [
basicSetup,
decodal(),
editorTheme,
keymap.of([
{
key: 'Mod-Enter',
run() {
execute();
return true;
},
},
]),
EditorView.updateListener.of((update) => {
if (!update.docChanged) return;
project.files[project.activePath] = update.state.doc.toString();
saveProject();
}),
],
});
for (const example of playgroundExamples) {
const option = document.createElement('option');
option.value = example.id;
@ -29,7 +87,6 @@ exampleSelect.value = starterProject.id;
setActiveFile(project.activePath);
renderFileTree();
updateHighlight();
function loadProject() {
try {
@ -83,30 +140,36 @@ function normalizePath(path) {
return parts.join('/');
}
function getEditorText() {
return editor.state.doc.toString();
}
function setEditorText(value) {
editor.dispatch({
changes: {
from: 0,
to: editor.state.doc.length,
insert: value,
},
});
}
function setActiveFile(path) {
const normalized = normalizePath(path);
if (project.files[normalized] === undefined) return;
if (project.activePath && project.files[project.activePath] !== undefined) {
project.files[project.activePath] = getEditorText();
}
project.activePath = normalized;
source.value = project.files[normalized];
setEditorText(project.files[normalized]);
activeFile.textContent = normalized;
deleteFile.disabled = Object.keys(project.files).length <= 1;
updateHighlight();
syncHighlightScroll();
renderFileTree();
saveProject();
}
function updateHighlight() {
sourceHighlight.innerHTML = `${highlightDecodal(source.value)}\n`;
}
function syncHighlightScroll() {
sourceHighlight.scrollTop = source.scrollTop;
sourceHighlight.scrollLeft = source.scrollLeft;
}
function execute() {
project.files[project.activePath] = source.value;
project.files[project.activePath] = getEditorText();
saveProject();
const result = JSON.parse(evaluateProject(project.activePath, JSON.stringify(project.files)));
output.textContent = result.ok ? result.output : result.error;
@ -195,13 +258,3 @@ deleteFile.addEventListener('click', () => {
delete project.files[project.activePath];
setActiveFile(Object.keys(project.files).sort()[0]);
});
source.addEventListener('input', () => {
project.files[project.activePath] = source.value;
updateHighlight();
syncHighlightScroll();
saveProject();
});
source.addEventListener('scroll', syncHighlightScroll);
source.addEventListener('keydown', (event) => {
if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') execute();
});

View File

@ -419,43 +419,26 @@ button:disabled {
text-align: left;
}
.editor-wrap {
.code-editor {
background: var(--editor-bg);
flex: 1;
min-height: 0;
position: relative;
}
.editor-wrap pre,
.editor-wrap textarea {
border: 0;
box-sizing: border-box;
font: 14px/1.5 ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
inset: 0;
margin: 0;
overflow: auto;
padding: 12px;
position: absolute;
tab-size: 2;
white-space: pre;
.code-editor .cm-editor {
height: 100%;
}
.editor-wrap pre {
color: var(--editor-text);
pointer-events: none;
.code-editor .cm-line {
padding: 0;
}
.editor-wrap textarea {
background: transparent;
caret-color: var(--editor-text);
color: transparent;
outline: none;
resize: none;
width: 100%;
.code-editor .cm-activeLine {
background: rgb(59 130 246 / 0.08);
}
.editor-wrap textarea::selection {
background: var(--selection);
.code-editor .cm-activeLineGutter {
background: rgb(59 130 246 / 0.12);
}
.output-pane pre {

View File

@ -20,6 +20,33 @@ Embedded hosts should depend on `decodal` and provide imports with a `SourceLoad
decodal = "0.1"
```
## Derive support
For embedded Rust applications, Decodal can generate a schema and typed decoder from a Rust struct with the `derive` feature.
```toml
[dependencies]
decodal = { version = "0.1", features = ["derive"] }
```
```rust
use decodal::{Decodal, DecodalDecode, DecodalSchema, Engine};
#[derive(Decodal)]
struct Service {
name: String,
#[decodal(gt = 443, default = 8443)]
port: i64,
#[decodal(rename = "feature.enable", default = true)]
feature_enabled: bool,
}
```
The derive implements:
- `DecodalSchema`, which produces a host schema for `Engine::bind_global`
- `DecodalDecode`, which converts materialized `Data` into the Rust struct
## CLI
A standalone CLI is kept in this repository as the `decodal-cli` workspace package.

View File

@ -2,7 +2,7 @@
"name": "decodal-wasm",
"type": "module",
"description": "WebAssembly wrapper for evaluating Decodal in browser playgrounds.",
"version": "0.1.0",
"version": "0.1.1",
"license": "MIT OR Apache-2.0",
"repository": {
"type": "git",