Compare commits

..

12 Commits

44 changed files with 18273 additions and 3067 deletions

6
.gitignore vendored
View File

@ -3,3 +3,9 @@
/.env
/.yoi
/result
node_modules
/dist
site/decodal-site/dist
# Astro
site/decodal-site/.astro

167
Cargo.lock generated
View File

@ -11,6 +11,18 @@ dependencies = [
"memchr",
]
[[package]]
name = "bumpalo"
version = "3.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "decodal"
version = "0.1.0"
@ -25,12 +37,51 @@ dependencies = [
"regex",
]
[[package]]
name = "decodal-wasm"
version = "0.1.0"
dependencies = [
"decodal-core",
"serde_json",
"wasm-bindgen",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "memchr"
version = "2.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
[[package]]
name = "once_cell"
version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "regex"
version = "1.12.4"
@ -59,3 +110,119 @@ name = "regex-syntax"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.150"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "wasm-bindgen"
version = "0.2.125"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.125"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.125"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.125"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f"
dependencies = [
"unicode-ident",
]
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"

View File

@ -2,6 +2,7 @@
members = [
"crates/decodal-core",
"crates/decodal-cli",
"crates/decodal-wasm",
]
resolver = "3"

View File

@ -71,6 +71,10 @@ pub enum Expr {
scrutinee: ExprId,
arms: Vec<MatchArm>,
},
Unary {
op: UnaryOp,
expr: ExprId,
},
Binary {
op: BinaryOp,
lhs: ExprId,
@ -117,8 +121,27 @@ pub enum Literal {
Bool(bool),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UnaryOp {
Neg,
Not,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BinaryOp {
Add,
Sub,
Mul,
Div,
Concat,
Equal,
NotEqual,
Greater,
GreaterEqual,
Less,
LessEqual,
LogicalAnd,
LogicalOr,
And,
Patch,
}

View File

@ -2,7 +2,7 @@ use alloc::{format, string::String, vec, vec::Vec};
use crate::{
ExprId, SourceForm, SourceId, Span,
ast::{Ast, BinaryOp, CompareOp, Expr, Field, Literal},
ast::{Ast, BinaryOp, CompareOp, Expr, Field, Literal, UnaryOp},
constraints::normalize_constraints,
diagnostic::{Diagnostic, DiagnosticKind, Result},
embedding::HostValue,
@ -337,24 +337,106 @@ impl<L: SourceLoader> Engine<L> {
"no match arm matched",
))
}
Expr::Unary { op, expr } => {
let value = self.eval_expr(
ExprRef {
module: reference.module,
expr,
},
env,
)?;
match op {
UnaryOp::Neg => negate_number(value, span),
UnaryOp::Not => negate_bool(value, span),
}
}
Expr::Binary { op, lhs, rhs } => {
let lhs = self.eval_expr(
let lhs_value = self.eval_expr(
ExprRef {
module: reference.module,
expr: lhs,
},
env,
)?;
let rhs = self.eval_expr(
ExprRef {
module: reference.module,
expr: rhs,
},
env,
)?;
match op {
BinaryOp::And => self.compose_and(lhs, rhs, span),
BinaryOp::Patch => self.patch(lhs, rhs),
BinaryOp::LogicalAnd => {
if !bool_from_runtime(lhs_value, span)? {
return Ok(RuntimeValue::Concrete(ConcreteValue::Bool(false)));
}
let rhs_value = self.eval_expr(
ExprRef {
module: reference.module,
expr: rhs,
},
env,
)?;
Ok(RuntimeValue::Concrete(ConcreteValue::Bool(
bool_from_runtime(rhs_value, span)?,
)))
}
BinaryOp::LogicalOr => {
if bool_from_runtime(lhs_value, span)? {
return Ok(RuntimeValue::Concrete(ConcreteValue::Bool(true)));
}
let rhs_value = self.eval_expr(
ExprRef {
module: reference.module,
expr: rhs,
},
env,
)?;
Ok(RuntimeValue::Concrete(ConcreteValue::Bool(
bool_from_runtime(rhs_value, span)?,
)))
}
_ => {
let rhs_value = self.eval_expr(
ExprRef {
module: reference.module,
expr: rhs,
},
env,
)?;
match op {
BinaryOp::Add => {
arithmetic(lhs_value, rhs_value, ArithmeticOp::Add, span)
}
BinaryOp::Sub => {
arithmetic(lhs_value, rhs_value, ArithmeticOp::Sub, span)
}
BinaryOp::Mul => {
arithmetic(lhs_value, rhs_value, ArithmeticOp::Mul, span)
}
BinaryOp::Div => {
arithmetic(lhs_value, rhs_value, ArithmeticOp::Div, span)
}
BinaryOp::Concat => concat_arrays(lhs_value, rhs_value, span),
BinaryOp::Equal => {
compare_expr(lhs_value, rhs_value, CompareExprOp::Equal, span)
}
BinaryOp::NotEqual => {
compare_expr(lhs_value, rhs_value, CompareExprOp::NotEqual, span)
}
BinaryOp::Greater => {
compare_expr(lhs_value, rhs_value, CompareExprOp::Greater, span)
}
BinaryOp::GreaterEqual => compare_expr(
lhs_value,
rhs_value,
CompareExprOp::GreaterEqual,
span,
),
BinaryOp::Less => {
compare_expr(lhs_value, rhs_value, CompareExprOp::Less, span)
}
BinaryOp::LessEqual => {
compare_expr(lhs_value, rhs_value, CompareExprOp::LessEqual, span)
}
BinaryOp::And => self.compose_and(lhs_value, rhs_value, span),
BinaryOp::Patch => self.patch(lhs_value, rhs_value),
BinaryOp::LogicalAnd | BinaryOp::LogicalOr => unreachable!(),
}
}
}
}
Expr::Default { base, fallback } => {
@ -999,6 +1081,230 @@ fn satisfies_regex(_value: &RuntimeValue, _pattern: &str, span: Span) -> Result<
))
}
#[derive(Debug, Clone, Copy)]
enum ArithmeticOp {
Add,
Sub,
Mul,
Div,
}
#[derive(Debug, Clone, Copy)]
enum CompareExprOp {
Equal,
NotEqual,
Greater,
GreaterEqual,
Less,
LessEqual,
}
#[derive(Debug, Clone, Copy)]
enum Number {
Int(i64),
Float(f64),
}
fn negate_number(value: RuntimeValue, span: Span) -> Result<RuntimeValue> {
match value {
RuntimeValue::Concrete(ConcreteValue::Int(value)) => value
.checked_neg()
.map(|value| RuntimeValue::Concrete(ConcreteValue::Int(value)))
.ok_or_else(|| arithmetic_error(span, "integer negation overflow")),
RuntimeValue::Concrete(ConcreteValue::Float(value)) => {
Ok(RuntimeValue::Concrete(ConcreteValue::Float(-value)))
}
_ => Err(Diagnostic::new(
DiagnosticKind::TypeMismatch,
span,
"unary '-' expects a numeric value",
)),
}
}
fn negate_bool(value: RuntimeValue, span: Span) -> Result<RuntimeValue> {
Ok(RuntimeValue::Concrete(ConcreteValue::Bool(
!bool_from_runtime(value, span)?,
)))
}
fn bool_from_runtime(value: RuntimeValue, span: Span) -> Result<bool> {
match value {
RuntimeValue::Concrete(ConcreteValue::Bool(value)) => Ok(value),
_ => Err(Diagnostic::new(
DiagnosticKind::TypeMismatch,
span,
"logical operators expect boolean values",
)),
}
}
fn compare_expr(
lhs: RuntimeValue,
rhs: RuntimeValue,
op: CompareExprOp,
span: Span,
) -> Result<RuntimeValue> {
let result = match op {
CompareExprOp::Equal => scalar_equal(&lhs, &rhs, span)?,
CompareExprOp::NotEqual => !scalar_equal(&lhs, &rhs, span)?,
CompareExprOp::Greater => compare_numbers(lhs, rhs, |lhs, rhs| lhs > rhs, span)?,
CompareExprOp::GreaterEqual => compare_numbers(lhs, rhs, |lhs, rhs| lhs >= rhs, span)?,
CompareExprOp::Less => compare_numbers(lhs, rhs, |lhs, rhs| lhs < rhs, span)?,
CompareExprOp::LessEqual => compare_numbers(lhs, rhs, |lhs, rhs| lhs <= rhs, span)?,
};
Ok(RuntimeValue::Concrete(ConcreteValue::Bool(result)))
}
fn scalar_equal(lhs: &RuntimeValue, rhs: &RuntimeValue, span: Span) -> Result<bool> {
match (lhs, rhs) {
(
RuntimeValue::Concrete(ConcreteValue::String(lhs)),
RuntimeValue::Concrete(ConcreteValue::String(rhs)),
) => Ok(lhs == rhs),
(
RuntimeValue::Concrete(ConcreteValue::Bool(lhs)),
RuntimeValue::Concrete(ConcreteValue::Bool(rhs)),
) => Ok(lhs == rhs),
(
RuntimeValue::Concrete(ConcreteValue::Int(lhs)),
RuntimeValue::Concrete(ConcreteValue::Int(rhs)),
) => Ok(lhs == rhs),
(
RuntimeValue::Concrete(ConcreteValue::Float(lhs)),
RuntimeValue::Concrete(ConcreteValue::Float(rhs)),
) => Ok(lhs == rhs),
(
RuntimeValue::Concrete(ConcreteValue::Int(lhs)),
RuntimeValue::Concrete(ConcreteValue::Float(rhs)),
) => Ok(*lhs as f64 == *rhs),
(
RuntimeValue::Concrete(ConcreteValue::Float(lhs)),
RuntimeValue::Concrete(ConcreteValue::Int(rhs)),
) => Ok(*lhs == *rhs as f64),
_ => Err(Diagnostic::new(
DiagnosticKind::TypeMismatch,
span,
"equality operators expect comparable scalar values",
)),
}
}
fn compare_numbers(
lhs: RuntimeValue,
rhs: RuntimeValue,
compare: impl FnOnce(f64, f64) -> bool,
span: Span,
) -> Result<bool> {
let lhs = number_from_runtime(lhs).ok_or_else(|| comparison_type_error(span))?;
let rhs = number_from_runtime(rhs).ok_or_else(|| comparison_type_error(span))?;
Ok(compare(number_to_f64(lhs), number_to_f64(rhs)))
}
fn comparison_type_error(span: Span) -> Diagnostic {
Diagnostic::new(
DiagnosticKind::TypeMismatch,
span,
"ordering operators expect numeric values",
)
}
fn concat_arrays(lhs: RuntimeValue, rhs: RuntimeValue, span: Span) -> Result<RuntimeValue> {
match (lhs, rhs) {
(
RuntimeValue::Concrete(ConcreteValue::Array(mut lhs)),
RuntimeValue::Concrete(ConcreteValue::Array(rhs)),
) => {
lhs.extend(rhs);
Ok(RuntimeValue::Concrete(ConcreteValue::Array(lhs)))
}
_ => Err(Diagnostic::new(
DiagnosticKind::TypeMismatch,
span,
"'++' expects array values",
)),
}
}
fn arithmetic(
lhs: RuntimeValue,
rhs: RuntimeValue,
op: ArithmeticOp,
span: Span,
) -> Result<RuntimeValue> {
let lhs = number_from_runtime(lhs).ok_or_else(|| arithmetic_type_error(span))?;
let rhs = number_from_runtime(rhs).ok_or_else(|| arithmetic_type_error(span))?;
match (lhs, rhs) {
(Number::Int(lhs), Number::Int(rhs)) => arithmetic_int(lhs, rhs, op, span),
(lhs, rhs) => arithmetic_float(number_to_f64(lhs), number_to_f64(rhs), op, span),
}
}
fn arithmetic_int(lhs: i64, rhs: i64, op: ArithmeticOp, span: Span) -> Result<RuntimeValue> {
match op {
ArithmeticOp::Add => lhs
.checked_add(rhs)
.map(|value| RuntimeValue::Concrete(ConcreteValue::Int(value)))
.ok_or_else(|| arithmetic_error(span, "integer addition overflow")),
ArithmeticOp::Sub => lhs
.checked_sub(rhs)
.map(|value| RuntimeValue::Concrete(ConcreteValue::Int(value)))
.ok_or_else(|| arithmetic_error(span, "integer subtraction overflow")),
ArithmeticOp::Mul => lhs
.checked_mul(rhs)
.map(|value| RuntimeValue::Concrete(ConcreteValue::Int(value)))
.ok_or_else(|| arithmetic_error(span, "integer multiplication overflow")),
ArithmeticOp::Div => {
if rhs == 0 {
return Err(arithmetic_error(span, "division by zero"));
}
Ok(RuntimeValue::Concrete(ConcreteValue::Float(
lhs as f64 / rhs as f64,
)))
}
}
}
fn arithmetic_float(lhs: f64, rhs: f64, op: ArithmeticOp, span: Span) -> Result<RuntimeValue> {
if matches!(op, ArithmeticOp::Div) && rhs == 0.0 {
return Err(arithmetic_error(span, "division by zero"));
}
let value = match op {
ArithmeticOp::Add => lhs + rhs,
ArithmeticOp::Sub => lhs - rhs,
ArithmeticOp::Mul => lhs * rhs,
ArithmeticOp::Div => lhs / rhs,
};
Ok(RuntimeValue::Concrete(ConcreteValue::Float(value)))
}
fn number_from_runtime(value: RuntimeValue) -> Option<Number> {
match value {
RuntimeValue::Concrete(ConcreteValue::Int(value)) => Some(Number::Int(value)),
RuntimeValue::Concrete(ConcreteValue::Float(value)) => Some(Number::Float(value)),
_ => None,
}
}
fn number_to_f64(value: Number) -> f64 {
match value {
Number::Int(value) => value as f64,
Number::Float(value) => value,
}
}
fn arithmetic_type_error(span: Span) -> Diagnostic {
Diagnostic::new(
DiagnosticKind::TypeMismatch,
span,
"arithmetic operators expect numeric values",
)
}
fn arithmetic_error(span: Span, message: &'static str) -> Diagnostic {
Diagnostic::new(DiagnosticKind::Conflict, span, message)
}
fn compare_value(value: &RuntimeValue, op: CompareOp, expected: &LiteralValue) -> bool {
match (value, expected) {
(RuntimeValue::Concrete(ConcreteValue::Int(actual)), LiteralValue::Int(expected)) => {
@ -1072,6 +1378,111 @@ mod tests {
assert!(matches!(data, Data::Object(_)));
}
#[test]
fn evaluates_arithmetic_expressions() {
let data = eval_data(
r#"
{
sum = 1 + 2 * 3;
diff = 10 - 4;
product = (2 + 3) * 4;
quotient = 5 / 2;
negative = -3 + 1;
}
"#,
);
let Data::Object(fields) = data else { panic!() };
assert_eq!(fields[0].value, Data::Int(7));
assert_eq!(fields[1].value, Data::Int(6));
assert_eq!(fields[2].value, Data::Int(20));
assert_eq!(fields[3].value, Data::Float(2.5));
assert_eq!(fields[4].value, Data::Int(-2));
}
#[test]
fn arithmetic_can_feed_constraints() {
let data = eval_data("port = Int & > 4000 + 42 default 8080;");
let Data::Object(fields) = data else { panic!() };
assert_eq!(fields[0].value, Data::Int(8080));
}
#[test]
fn rejects_division_by_zero() {
let parsed = parse_source("1 / 0").unwrap();
let mut engine = Engine::from_parse(parsed.ast, parsed.root);
assert!(engine.eval_root().is_err());
}
#[test]
fn evaluates_array_concat() {
let data = eval_data(
r#"
[1, 2] ++ [3, 4]
"#,
);
assert_eq!(
data,
Data::Array(vec![Data::Int(1), Data::Int(2), Data::Int(3), Data::Int(4)])
);
}
#[test]
fn array_concat_has_lower_precedence_than_arithmetic() {
let data = eval_data("[1 + 1] ++ [2 * 2]");
assert_eq!(data, Data::Array(vec![Data::Int(2), Data::Int(4)]));
}
#[test]
fn rejects_invalid_array_concat_operands() {
let parsed = parse_source("[1] ++ 2").unwrap();
let mut engine = Engine::from_parse(parsed.ast, parsed.root);
assert!(engine.eval_root().is_err());
}
#[test]
fn evaluates_logical_and_comparison_expressions() {
let data = eval_data(
r#"
{
enabled = true && !false;
fallback = false || true;
compare = 9000 + 443 > 9442;
equality = 1 == 1.0;
inequality = "prod" != "dev";
}
"#,
);
let Data::Object(fields) = data else { panic!() };
assert_eq!(fields[0].value, Data::Bool(true));
assert_eq!(fields[1].value, Data::Bool(true));
assert_eq!(fields[2].value, Data::Bool(true));
assert_eq!(fields[3].value, Data::Bool(true));
assert_eq!(fields[4].value, Data::Bool(true));
}
#[test]
fn logical_operators_short_circuit() {
let false_and_missing = eval_data("false && missing_identifier");
assert_eq!(false_and_missing, Data::Bool(false));
let true_or_missing = eval_data("true || missing_identifier");
assert_eq!(true_or_missing, Data::Bool(true));
}
#[test]
fn rejects_invalid_logical_operands() {
let parsed = parse_source("true && 1").unwrap();
let mut engine = Engine::from_parse(parsed.ast, parsed.root);
assert!(engine.eval_root().is_err());
}
#[test]
fn rejects_invalid_comparison_operands() {
let parsed = parse_source("\"a\" < \"b\"").unwrap();
let mut engine = Engine::from_parse(parsed.ast, parsed.root);
assert!(engine.eval_root().is_err());
}
#[test]
fn composes_schema_and_value() {
let data = eval_data(

View File

@ -34,8 +34,18 @@ pub enum TokenKind {
Dot,
Colon,
Equal,
EqualEqual,
Bang,
BangEqual,
Arrow,
Amp,
AmpAmp,
PipePipe,
Plus,
PlusPlus,
Minus,
Star,
Slash,
SlashSlash,
Gt,
Gte,
@ -67,9 +77,13 @@ impl<'a> Lexer<'a> {
pub fn tokenize(mut self) -> Result<Vec<Token>> {
let mut tokens = Vec::new();
let mut previous = None;
loop {
let token = self.next_token()?;
let token = self.next_token(previous.as_ref())?;
let is_eof = token.kind == TokenKind::Eof;
if !is_eof {
previous = Some(token.kind.clone());
}
tokens.push(token);
if is_eof {
return Ok(tokens);
@ -77,7 +91,7 @@ impl<'a> Lexer<'a> {
}
}
fn next_token(&mut self) -> Result<Token> {
fn next_token(&mut self, previous: Option<&TokenKind>) -> Result<Token> {
self.skip_ws_and_comments();
let start = self.pos;
let Some(ch) = self.peek() else {
@ -134,16 +148,57 @@ impl<'a> Lexer<'a> {
}
b'&' => {
self.pos += 1;
TokenKind::Amp
if self.consume(b'&') {
TokenKind::AmpAmp
} else {
TokenKind::Amp
}
}
b'|' => {
self.pos += 1;
if self.consume(b'|') {
TokenKind::PipePipe
} else {
return Err(Diagnostic::syntax(
self.span(start, self.pos),
"expected '|' after '|'",
));
}
}
b'+' => {
self.pos += 1;
if self.consume(b'+') {
TokenKind::PlusPlus
} else {
TokenKind::Plus
}
}
b'-' => {
self.pos += 1;
TokenKind::Minus
}
b'*' => {
self.pos += 1;
TokenKind::Star
}
b'=' => {
self.pos += 1;
if self.consume(b'>') {
TokenKind::Arrow
} else if self.consume(b'=') {
TokenKind::EqualEqual
} else {
TokenKind::Equal
}
}
b'!' => {
self.pos += 1;
if self.consume(b'=') {
TokenKind::BangEqual
} else {
TokenKind::Bang
}
}
b'>' => {
self.pos += 1;
if self.consume(b'=') {
@ -164,6 +219,8 @@ impl<'a> Lexer<'a> {
self.pos += 1;
if self.consume(b'/') {
TokenKind::SlashSlash
} else if previous.is_some_and(token_can_end_expr) {
TokenKind::Slash
} else {
self.lex_regex(start)?
}
@ -341,6 +398,23 @@ fn is_ident_continue(c: u8) -> bool {
c.is_ascii_alphanumeric() || c == b'_'
}
fn token_can_end_expr(kind: &TokenKind) -> bool {
matches!(
kind,
TokenKind::Ident(_)
| TokenKind::Int(_)
| TokenKind::Float(_)
| TokenKind::String(_)
| TokenKind::Regex(_)
| TokenKind::True
| TokenKind::False
| TokenKind::Underscore
| TokenKind::RBrace
| TokenKind::RBracket
| TokenKind::RParen
)
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -2,7 +2,7 @@ use alloc::{string::String, vec::Vec};
use crate::{
SourceId, Span,
ast::{Ast, BinaryOp, CompareOp, Expr, ExprId, Field, Literal, MatchArm, Param},
ast::{Ast, BinaryOp, CompareOp, Expr, ExprId, Field, Literal, MatchArm, Param, UnaryOp},
diagnostic::{Diagnostic, Result},
lexer::{Lexer, Token, TokenKind},
};
@ -115,6 +115,110 @@ impl Parser {
let rhs = self.parse_expr(r_bp)?;
let span = self.ast.span(lhs).join(self.ast.span(rhs)).join(op_span);
lhs = match kind {
InfixKind::Add => self.ast.push(
Expr::Binary {
op: BinaryOp::Add,
lhs,
rhs,
},
span,
),
InfixKind::Sub => self.ast.push(
Expr::Binary {
op: BinaryOp::Sub,
lhs,
rhs,
},
span,
),
InfixKind::Mul => self.ast.push(
Expr::Binary {
op: BinaryOp::Mul,
lhs,
rhs,
},
span,
),
InfixKind::Div => self.ast.push(
Expr::Binary {
op: BinaryOp::Div,
lhs,
rhs,
},
span,
),
InfixKind::Concat => self.ast.push(
Expr::Binary {
op: BinaryOp::Concat,
lhs,
rhs,
},
span,
),
InfixKind::Equal => self.ast.push(
Expr::Binary {
op: BinaryOp::Equal,
lhs,
rhs,
},
span,
),
InfixKind::NotEqual => self.ast.push(
Expr::Binary {
op: BinaryOp::NotEqual,
lhs,
rhs,
},
span,
),
InfixKind::Greater => self.ast.push(
Expr::Binary {
op: BinaryOp::Greater,
lhs,
rhs,
},
span,
),
InfixKind::GreaterEqual => self.ast.push(
Expr::Binary {
op: BinaryOp::GreaterEqual,
lhs,
rhs,
},
span,
),
InfixKind::Less => self.ast.push(
Expr::Binary {
op: BinaryOp::Less,
lhs,
rhs,
},
span,
),
InfixKind::LessEqual => self.ast.push(
Expr::Binary {
op: BinaryOp::LessEqual,
lhs,
rhs,
},
span,
),
InfixKind::LogicalAnd => self.ast.push(
Expr::Binary {
op: BinaryOp::LogicalAnd,
lhs,
rhs,
},
span,
),
InfixKind::LogicalOr => self.ast.push(
Expr::Binary {
op: BinaryOp::LogicalOr,
lhs,
rhs,
},
span,
),
InfixKind::And => self.ast.push(
Expr::Binary {
op: BinaryOp::And,
@ -173,6 +277,28 @@ impl Parser {
TokenKind::Let => self.parse_let(token.span),
TokenKind::Match => self.parse_match(token.span),
TokenKind::Import => self.parse_import(token.span),
TokenKind::Minus => {
let expr = self.parse_expr(17)?;
let span = token.span.join(self.ast.span(expr));
Ok(self.ast.push(
Expr::Unary {
op: UnaryOp::Neg,
expr,
},
span,
))
}
TokenKind::Bang => {
let expr = self.parse_expr(17)?;
let span = token.span.join(self.ast.span(expr));
Ok(self.ast.push(
Expr::Unary {
op: UnaryOp::Not,
expr,
},
span,
))
}
TokenKind::Gt | TokenKind::Gte | TokenKind::Lt | TokenKind::Lte => {
let op = match token.kind {
TokenKind::Gt => CompareOp::Gt,
@ -181,7 +307,7 @@ impl Parser {
TokenKind::Lte => CompareOp::Lte,
_ => unreachable!(),
};
let value = self.parse_expr(8)?;
let value = self.parse_expr(12)?;
let span = token.span.join(self.ast.span(value));
Ok(self.ast.push(Expr::CompareConstraint { op, value }, span))
}
@ -406,6 +532,19 @@ impl Parser {
TokenKind::Default => Some((InfixKind::Default, 1, 2)),
TokenKind::SlashSlash => Some((InfixKind::Patch, 3, 4)),
TokenKind::Amp => Some((InfixKind::And, 5, 6)),
TokenKind::PipePipe => Some((InfixKind::LogicalOr, 7, 8)),
TokenKind::AmpAmp => Some((InfixKind::LogicalAnd, 9, 10)),
TokenKind::EqualEqual => Some((InfixKind::Equal, 11, 12)),
TokenKind::BangEqual => Some((InfixKind::NotEqual, 11, 12)),
TokenKind::Gt => Some((InfixKind::Greater, 11, 12)),
TokenKind::Gte => Some((InfixKind::GreaterEqual, 11, 12)),
TokenKind::Lt => Some((InfixKind::Less, 11, 12)),
TokenKind::Lte => Some((InfixKind::LessEqual, 11, 12)),
TokenKind::PlusPlus => Some((InfixKind::Concat, 12, 13)),
TokenKind::Plus => Some((InfixKind::Add, 13, 14)),
TokenKind::Minus => Some((InfixKind::Sub, 13, 14)),
TokenKind::Star => Some((InfixKind::Mul, 15, 16)),
TokenKind::Slash => Some((InfixKind::Div, 15, 16)),
_ => None,
}
}
@ -498,6 +637,19 @@ impl Parser {
#[derive(Debug, Clone, Copy)]
enum InfixKind {
Add,
Sub,
Mul,
Div,
Concat,
Equal,
NotEqual,
Greater,
GreaterEqual,
Less,
LessEqual,
LogicalAnd,
LogicalOr,
And,
Patch,
Default,

View File

@ -0,0 +1,15 @@
[package]
name = "decodal-wasm"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
decodal-core = { path = "../decodal-core" }
serde_json = "1"
wasm-bindgen = "0.2"
[package.metadata.wasm-pack.profile.release]
wasm-opt = false

View File

@ -0,0 +1,233 @@
use std::collections::BTreeMap;
use decodal_core::{
Data, Diagnostic, DiagnosticKind, EmptyLoader, Engine, LoadedSource, SourceLoader, Span,
};
use wasm_bindgen::prelude::*;
#[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))
}
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)),
}
}
fn evaluate_inner(source: &str) -> Result<String, String> {
let mut engine = Engine::new(EmptyLoader);
let module = engine
.add_root_source("playground", "playground", source)
.map_err(format_diagnostic)?;
let value = engine.eval_module(module).map_err(format_diagnostic)?;
let data = engine.materialize(&value).map_err(format_diagnostic)?;
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 = engine
.add_root_source(entry.clone(), entry.clone(), &source)
.map_err(format_diagnostic)?;
let value = engine.eval_module(module).map_err(format_diagnostic)?;
let data = engine.materialize(&value).map_err(format_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_core::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('/');
}
}
base.push_str(specifier);
normalize_path(&base)
}
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),
}
}
if parts.is_empty() {
None
} else {
Some(parts.join("/"))
}
}
fn format_diagnostic(diagnostic: decodal_core::Diagnostic) -> String {
format!(
"{:?} at {}:{}..{}: {}",
diagnostic.kind,
diagnostic.span.source.0,
diagnostic.span.start,
diagnostic.span.end,
diagnostic.message,
)
}
fn format_data(data: &Data, indent: usize) -> String {
match data {
Data::String(value) => json_string(value),
Data::Int(value) => value.to_string(),
Data::Float(value) => value.to_string(),
Data::Bool(value) => value.to_string(),
Data::Array(items) => {
if items.is_empty() {
return String::from("[]");
}
let mut out = String::from("[\n");
for (index, item) in items.iter().enumerate() {
out.push_str(&" ".repeat(indent + 2));
out.push_str(&format_data(item, indent + 2));
if index + 1 != items.len() {
out.push(',');
}
out.push('\n');
}
out.push_str(&" ".repeat(indent));
out.push(']');
out
}
Data::Object(fields) => {
if fields.is_empty() {
return String::from("{}");
}
let mut out = String::from("{\n");
for (index, field) in fields.iter().enumerate() {
out.push_str(&" ".repeat(indent + 2));
out.push_str(&json_string(&field.name));
out.push_str(": ");
out.push_str(&format_data(&field.value, indent + 2));
if index + 1 != fields.len() {
out.push(',');
}
out.push('\n');
}
out.push_str(&" ".repeat(indent));
out.push('}');
out
}
}
}
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
}
#[cfg(test)]
mod tests {
use super::{evaluate_project_inner, normalize_path, resolve_import};
#[test]
fn normalizes_virtual_paths() {
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())
);
}
#[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"));
}
}

View File

@ -4,11 +4,13 @@ pkgs.mkShell {
cargo
clippy
git
lld
nodejs
nixfmt
rustc
rustfmt
tree-sitter
wasm-pack
];
shellHook = ''

View File

@ -20,6 +20,51 @@ cargo test -p decodal-core --features regex
cargo run -q -p decodal --features regex -- examples/regex/main.dcdl
```
## Web site and playground
The Astro documentation site and browser playground are kept in:
```text
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.
Important files:
```text
site/decodal-site/src/pages/docs/[...slug].astro
site/decodal-site/src/pages/playground.astro
site/decodal-site/src/layouts/ManualLayout.astro
site/decodal-site/src/lib/docs.js
crates/decodal-wasm/src/lib.rs
```
Build the WebAssembly package before building the site:
```sh
cd site/decodal-site
npm install
npm run build:wasm
npm run build
```
`npm run build:wasm` writes generated files into:
```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.
To run the site locally:
```sh
cd site/decodal-site
npm run dev
```
## Tree-sitter grammar
The Tree-sitter grammar is kept in:
@ -73,7 +118,7 @@ When the Decodal syntax changes:
## Development shell
The Nix development shell includes Rust tooling, Node.js, and Tree-sitter CLI tooling.
The Nix development shell includes Rust tooling, Node.js, Tree-sitter CLI tooling, and wasm-pack tooling.
```sh
nix develop
@ -87,5 +132,7 @@ The shell provides:
- `clippy`
- `node`
- `npm`
- `wasm-pack`
- `lld`
- `tree-sitter`
- `nixfmt`

View File

@ -0,0 +1,43 @@
# Arithmetic Expression
Decodal supports arithmetic over concrete numeric values.
```dcdl
{
workers = 2 + 2;
timeout = 30.0 / 2;
port = 8000 + 80;
negative = -1;
}
```
## Operators
- `+` addition
- `-` subtraction
- `*` multiplication
- `/` division
- unary `-` negation
`*` and `/` bind tighter than `+` and `-`.
Parentheses can be used to make grouping explicit.
```dcdl
2 + 3 * 4 # 14
(2 + 3) * 4 # 20
```
## Numeric behavior
Arithmetic requires concrete `Int` or `Float` operands.
`Int + Int`, `Int - Int`, and `Int * Int` produce `Int` when no overflow occurs.
Mixed `Int` / `Float` arithmetic produces `Float`.
Division always produces `Float`.
Division by zero and integer overflow are evaluation errors.
Arithmetic expressions can be used anywhere a concrete numeric expression is expected, including defaults and numeric constraints.
```dcdl
port = Int & > 4000 + 42 default 8080;
```

View File

@ -7,9 +7,21 @@ array expression は、順序付きの値の列を表す。
["a", "b", "c"]
```
## 未確定事項
## Array concat
- 配列要素の制約表現。
- 異種配列を許可するか。
- `//` による patch を右辺置換だけにするか。
- append / prepend / remove などの操作を提供するか。
`++` は concrete array 同士を連結する。
```dcdl
base = ["read", "write"];
extra = ["admin"];
roles = base ++ extra;
```
`roles` は以下と同じ値になる。
```dcdl
["read", "write", "admin"]
```
`++` は配列要素を変換しない。
左辺の要素の後に右辺の要素が並ぶ。

View File

@ -0,0 +1,41 @@
# Logical and Comparison Expressions
Decodal supports boolean logic over concrete `Bool` values and comparison over concrete scalar values.
```dcdl
{
is_prod = env == "prod";
high_port = port > 9000;
enabled = is_prod && high_port;
disabled = !enabled;
}
```
## Logical operators
- `!expr` negates a concrete `Bool`.
- `lhs && rhs` returns boolean AND.
- `lhs || rhs` returns boolean OR.
`&&` and `||` short-circuit: the right-hand side is evaluated only when needed.
Logical operands must evaluate to concrete `Bool` values.
## Comparison operators
- `==`
- `!=`
- `<`
- `<=`
- `>`
- `>=`
`==` and `!=` compare concrete scalar values: `String`, `Bool`, `Int`, and `Float`.
`Int` and `Float` can be compared to each other numerically.
Ordering operators `<`, `<=`, `>`, and `>=` compare concrete numeric values only.
They are separate from prefix comparison constraints such as `> 443`.
```dcdl
port = Int & > 443 default 9443;
is_high = port > 9000;
```

View File

@ -1,6 +1,79 @@
# 合成演算子
# 演算子
この章では、`&` と `//` の意味を定義する。
この章では、Decodal の演算子の意味を定義する。
## 演算子一覧
| 演算子 | 形 | 種類 | 対象 | 結果 / 意味 |
|---|---|---|---|---|
| `.` | `object.field` | field reference | object / abstract object | field value |
| call | `fn(arg)` | function call | function | function result |
| `!` | `!expr` | unary logical | concrete `Bool` | concrete `Bool` |
| `-` | `-expr` | unary arithmetic | concrete `Int` / `Float` | negated number |
| `*` | `lhs * rhs` | arithmetic | concrete `Int` / `Float` | numeric product |
| `/` | `lhs / rhs` | arithmetic | concrete `Int` / `Float` | `Float` quotient |
| `+` | `lhs + rhs` | arithmetic | concrete `Int` / `Float` | numeric sum |
| `-` | `lhs - rhs` | arithmetic | concrete `Int` / `Float` | numeric difference |
| `++` | `lhs ++ rhs` | array concat | concrete arrays | concatenated array |
| `==` | `lhs == rhs` | equality | concrete scalar | concrete `Bool` |
| `!=` | `lhs != rhs` | equality | concrete scalar | concrete `Bool` |
| `<` | `lhs < rhs` | ordering | concrete `Int` / `Float` | concrete `Bool` |
| `<=` | `lhs <= rhs` | ordering | concrete `Int` / `Float` | concrete `Bool` |
| `>` | `lhs > rhs` | ordering | concrete `Int` / `Float` | concrete `Bool` |
| `>=` | `lhs >= rhs` | ordering | concrete `Int` / `Float` | concrete `Bool` |
| `>` | `> value` | comparison constraint | numeric constraint value | abstract constraint |
| `>=` | `>= value` | comparison constraint | numeric constraint value | abstract constraint |
| `<` | `< value` | comparison constraint | numeric constraint value | abstract constraint |
| `<=` | `<= value` | comparison constraint | numeric constraint value | abstract constraint |
| `&&` | `lhs && rhs` | logical | concrete `Bool` | short-circuit AND |
| `||` | `lhs || rhs` | logical | concrete `Bool` | short-circuit OR |
| `&` | `lhs & rhs` | composition | value / constraint / object | constraint-preserving composition |
| `//` | `lhs // rhs` | patch | object / value | right-biased structural patch |
| `default` | `base default fallback` | default | abstract value | materialization fallback |
`concrete scalar``String`、`Bool`、`Int`、`Float` を指す。
## 優先順位
優先順位は高い順に以下である。
1. 関数呼び出しとフィールド参照
2. unary `!` `-`
3. `*` `/`
4. `+` `-`
5. `++`
6. `==` `!=` `<` `<=` `>` `>=`
7. `&&`
8. `||`
9. `&`
10. `//`
11. `default`
同じ優先順位の二項演算子は左結合である。
`default` は右結合である。
## Arithmetic operators
`+` `-` `*` `/` は具体的な `Int` / `Float` に対する四則演算である。
詳しくは [Arithmetic Expression](./expression/arithmetic.md) を参照する。
## Array concat operator
`++` は concrete array 同士を連結する演算子である。
要素は変換されず、左辺の要素の後に右辺の要素が並ぶ。
```dcdl
["read", "write"] ++ ["admin"]
```
## Logical and comparison operators
`!` `&&` `||` は concrete `Bool` に対する論理演算である。
`&&``||` は短絡評価される。
`==` `!=` は concrete scalar value を比較する。
`<` `<=` `>` `>=` は concrete numeric value を比較する。
詳しくは [Logical and Comparison Expressions](./expression/logical-and-comparison.md) を参照する。
## `&`: 制約合成

View File

@ -115,12 +115,15 @@ rec
主要な演算子は以下である。
```text
& 制約合成
// patch 合成
default fallback 指定
=> 関数
. フィールド参照 / ドットパス定義
+ - * / 四則演算
++ 配列結合
! && || 論理演算
== != < <= > >= 比較式
& 制約合成
// patch 合成
default fallback 指定
=> 関数
. フィールド参照 / ドットパス定義
```
演算子の優先順位は未確定である。
詳細は [合成演算子](./operators.md) で定義する。
演算子の優先順位は [合成演算子](./operators.md) で定義する。

View File

@ -74,3 +74,87 @@ base // {
body: (literal (string)))
(match_arm
body: (literal (string))))))))
==================
Arithmetic
==================
{
value = 1 + 2 * 3;
grouped = (1 + 2) / -3;
}
---
(source_file
(object
(field_definition
path: (field_path (identifier))
value: (binary_expression
left: (literal (integer))
right: (binary_expression
left: (literal (integer))
right: (literal (integer)))))
(field_definition
path: (field_path (identifier))
value: (binary_expression
left: (parenthesized_expression
(binary_expression
left: (literal (integer))
right: (literal (integer))))
right: (unary_expression
operand: (literal (integer)))))))
==================
Logical and comparison
==================
{
enabled = env == "prod" && replicas > 1;
disabled = !enabled || false;
}
---
(source_file
(object
(field_definition
path: (field_path (identifier))
value: (binary_expression
left: (binary_expression
left: (identifier)
right: (literal (string)))
right: (binary_expression
left: (identifier)
right: (literal (integer)))))
(field_definition
path: (field_path (identifier))
value: (binary_expression
left: (unary_expression
operand: (identifier))
right: (literal (boolean))))))
==================
Array concat
==================
{
roles = ["read"] ++ ["write", "admin"];
ports = [8000 + 80] ++ [9443];
}
---
(source_file
(object
(field_definition
path: (field_path (identifier))
value: (binary_expression
left: (array
(literal (string)))
right: (array
(literal (string))
(literal (string)))))
(field_definition
path: (field_path (identifier))
value: (binary_expression
left: (array
(binary_expression
left: (literal (integer))
right: (literal (integer))))
right: (array
(literal (integer)))))))

View File

@ -1,9 +1,16 @@
const PREC = {
DEFAULT: 1,
PATCH: 2,
AND: 3,
CALL: 7,
PATH: 8,
COMPOSE: 3,
OR: 4,
LOGICAL_AND: 5,
COMPARE: 6,
CONCAT: 7,
ADD: 8,
MUL: 9,
UNARY: 10,
CALL: 11,
PATH: 12,
};
function commaSep(rule) {
@ -52,6 +59,7 @@ module.exports = grammar({
$.parenthesized_expression,
$.call_expression,
$.path_expression,
$.unary_expression,
$.binary_expression,
$.default_expression,
),
@ -159,13 +167,48 @@ module.exports = grammar({
field('field', $.identifier),
)),
comparison_constraint: $ => prec(6, seq(
comparison_constraint: $ => prec.right(PREC.UNARY + 1, seq(
field('operator', choice('>', '>=', '<', '<=')),
field('value', choice($.integer, $.float)),
field('value', $._expression),
)),
unary_expression: $ => prec(PREC.UNARY, seq(
field('operator', choice('-', '!')),
field('operand', $._expression),
)),
binary_expression: $ => choice(
prec.left(PREC.AND, seq(
prec.left(PREC.OR, seq(
field('left', $._expression),
field('operator', token(prec(2, '||'))),
field('right', $._expression),
)),
prec.left(PREC.LOGICAL_AND, seq(
field('left', $._expression),
field('operator', token(prec(2, '&&'))),
field('right', $._expression),
)),
prec.left(PREC.COMPARE, seq(
field('left', $._expression),
field('operator', choice('==', '!=', '>', '>=', '<', '<=')),
field('right', $._expression),
)),
prec.left(PREC.CONCAT, seq(
field('left', $._expression),
field('operator', token(prec(2, '++'))),
field('right', $._expression),
)),
prec.left(PREC.ADD, seq(
field('left', $._expression),
field('operator', choice('+', '-')),
field('right', $._expression),
)),
prec.left(PREC.MUL, seq(
field('left', $._expression),
field('operator', choice('*', '/')),
field('right', $._expression),
)),
prec.left(PREC.COMPOSE, seq(
field('left', $._expression),
field('operator', '&'),
field('right', $._expression),

View File

@ -17,6 +17,16 @@
[
"&"
"//"
"+"
"-"
"*"
"/"
"++"
"&&"
"||"
"!"
"=="
"!="
"=>"
"="
">"

View File

@ -127,6 +127,10 @@
"type": "SYMBOL",
"name": "path_expression"
},
{
"type": "SYMBOL",
"name": "unary_expression"
},
{
"type": "SYMBOL",
"name": "binary_expression"
@ -712,7 +716,7 @@
},
"call_expression": {
"type": "PREC_LEFT",
"value": 7,
"value": 11,
"content": {
"type": "SEQ",
"members": [
@ -782,7 +786,7 @@
},
"path_expression": {
"type": "PREC_LEFT",
"value": 8,
"value": 12,
"content": {
"type": "SEQ",
"members": [
@ -810,8 +814,8 @@
}
},
"comparison_constraint": {
"type": "PREC",
"value": 6,
"type": "PREC_RIGHT",
"value": 11,
"content": {
"type": "SEQ",
"members": [
@ -843,19 +847,44 @@
{
"type": "FIELD",
"name": "value",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
}
]
}
},
"unary_expression": {
"type": "PREC",
"value": 10,
"content": {
"type": "SEQ",
"members": [
{
"type": "FIELD",
"name": "operator",
"content": {
"type": "CHOICE",
"members": [
{
"type": "SYMBOL",
"name": "integer"
"type": "STRING",
"value": "-"
},
{
"type": "SYMBOL",
"name": "float"
"type": "STRING",
"value": "!"
}
]
}
},
{
"type": "FIELD",
"name": "operand",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
}
]
}
@ -863,6 +892,268 @@
"binary_expression": {
"type": "CHOICE",
"members": [
{
"type": "PREC_LEFT",
"value": 4,
"content": {
"type": "SEQ",
"members": [
{
"type": "FIELD",
"name": "left",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
},
{
"type": "FIELD",
"name": "operator",
"content": {
"type": "TOKEN",
"content": {
"type": "PREC",
"value": 2,
"content": {
"type": "STRING",
"value": "||"
}
}
}
},
{
"type": "FIELD",
"name": "right",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
}
]
}
},
{
"type": "PREC_LEFT",
"value": 5,
"content": {
"type": "SEQ",
"members": [
{
"type": "FIELD",
"name": "left",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
},
{
"type": "FIELD",
"name": "operator",
"content": {
"type": "TOKEN",
"content": {
"type": "PREC",
"value": 2,
"content": {
"type": "STRING",
"value": "&&"
}
}
}
},
{
"type": "FIELD",
"name": "right",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
}
]
}
},
{
"type": "PREC_LEFT",
"value": 6,
"content": {
"type": "SEQ",
"members": [
{
"type": "FIELD",
"name": "left",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
},
{
"type": "FIELD",
"name": "operator",
"content": {
"type": "CHOICE",
"members": [
{
"type": "STRING",
"value": "=="
},
{
"type": "STRING",
"value": "!="
},
{
"type": "STRING",
"value": ">"
},
{
"type": "STRING",
"value": ">="
},
{
"type": "STRING",
"value": "<"
},
{
"type": "STRING",
"value": "<="
}
]
}
},
{
"type": "FIELD",
"name": "right",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
}
]
}
},
{
"type": "PREC_LEFT",
"value": 7,
"content": {
"type": "SEQ",
"members": [
{
"type": "FIELD",
"name": "left",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
},
{
"type": "FIELD",
"name": "operator",
"content": {
"type": "TOKEN",
"content": {
"type": "PREC",
"value": 2,
"content": {
"type": "STRING",
"value": "++"
}
}
}
},
{
"type": "FIELD",
"name": "right",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
}
]
}
},
{
"type": "PREC_LEFT",
"value": 8,
"content": {
"type": "SEQ",
"members": [
{
"type": "FIELD",
"name": "left",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
},
{
"type": "FIELD",
"name": "operator",
"content": {
"type": "CHOICE",
"members": [
{
"type": "STRING",
"value": "+"
},
{
"type": "STRING",
"value": "-"
}
]
}
},
{
"type": "FIELD",
"name": "right",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
}
]
}
},
{
"type": "PREC_LEFT",
"value": 9,
"content": {
"type": "SEQ",
"members": [
{
"type": "FIELD",
"name": "left",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
},
{
"type": "FIELD",
"name": "operator",
"content": {
"type": "CHOICE",
"members": [
{
"type": "STRING",
"value": "*"
},
{
"type": "STRING",
"value": "/"
}
]
}
},
{
"type": "FIELD",
"name": "right",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
}
]
}
},
{
"type": "PREC_LEFT",
"value": 3,

View File

@ -66,6 +66,10 @@
{
"type": "regex_literal",
"named": true
},
{
"type": "unary_expression",
"named": true
}
]
}
@ -137,6 +141,10 @@
{
"type": "regex_literal",
"named": true
},
{
"type": "unary_expression",
"named": true
}
]
},
@ -144,13 +152,65 @@
"multiple": false,
"required": true,
"types": [
{
"type": "!=",
"named": false
},
{
"type": "&",
"named": false
},
{
"type": "&&",
"named": false
},
{
"type": "*",
"named": false
},
{
"type": "+",
"named": false
},
{
"type": "++",
"named": false
},
{
"type": "-",
"named": false
},
{
"type": "/",
"named": false
},
{
"type": "//",
"named": false
},
{
"type": "<",
"named": false
},
{
"type": "<=",
"named": false
},
{
"type": "==",
"named": false
},
{
"type": ">",
"named": false
},
{
"type": ">=",
"named": false
},
{
"type": "||",
"named": false
}
]
},
@ -217,6 +277,10 @@
{
"type": "regex_literal",
"named": true
},
{
"type": "unary_expression",
"named": true
}
]
}
@ -294,6 +358,10 @@
{
"type": "regex_literal",
"named": true
},
{
"type": "unary_expression",
"named": true
}
]
}
@ -361,6 +429,10 @@
{
"type": "regex_literal",
"named": true
},
{
"type": "unary_expression",
"named": true
}
]
}
@ -396,11 +468,67 @@
"required": true,
"types": [
{
"type": "float",
"type": "array",
"named": true
},
{
"type": "integer",
"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
}
]
@ -474,6 +602,10 @@
{
"type": "regex_literal",
"named": true
},
{
"type": "unary_expression",
"named": true
}
]
},
@ -540,6 +672,10 @@
{
"type": "regex_literal",
"named": true
},
{
"type": "unary_expression",
"named": true
}
]
}
@ -622,6 +758,10 @@
{
"type": "regex_literal",
"named": true
},
{
"type": "unary_expression",
"named": true
}
]
}
@ -709,6 +849,10 @@
{
"type": "regex_literal",
"named": true
},
{
"type": "unary_expression",
"named": true
}
]
}
@ -807,6 +951,10 @@
{
"type": "regex_literal",
"named": true
},
{
"type": "unary_expression",
"named": true
}
]
}
@ -916,6 +1064,10 @@
{
"type": "regex_literal",
"named": true
},
{
"type": "unary_expression",
"named": true
}
]
},
@ -986,6 +1138,10 @@
{
"type": "regex_literal",
"named": true
},
{
"type": "unary_expression",
"named": true
}
]
}
@ -1058,6 +1214,10 @@
{
"type": "regex_literal",
"named": true
},
{
"type": "unary_expression",
"named": true
}
]
}
@ -1155,6 +1315,10 @@
{
"type": "regex_literal",
"named": true
},
{
"type": "unary_expression",
"named": true
}
]
},
@ -1237,6 +1401,10 @@
{
"type": "regex_literal",
"named": true
},
{
"type": "unary_expression",
"named": true
}
]
}
@ -1318,6 +1486,10 @@
{
"type": "regex_literal",
"named": true
},
{
"type": "unary_expression",
"named": true
}
]
}
@ -1394,14 +1566,120 @@
{
"type": "regex_literal",
"named": true
},
{
"type": "unary_expression",
"named": true
}
]
}
},
{
"type": "unary_expression",
"named": true,
"fields": {
"operand": {
"multiple": false,
"required": true,
"types": [
{
"type": "array",
"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
}
]
},
"operator": {
"multiple": false,
"required": true,
"types": [
{
"type": "!",
"named": false
},
{
"type": "-",
"named": false
}
]
}
}
},
{
"type": "!",
"named": false
},
{
"type": "!=",
"named": false
},
{
"type": "&",
"named": false
},
{
"type": "&&",
"named": false
},
{
"type": "(",
"named": false
@ -1410,14 +1688,34 @@
"type": ")",
"named": false
},
{
"type": "*",
"named": false
},
{
"type": "+",
"named": false
},
{
"type": "++",
"named": false
},
{
"type": ",",
"named": false
},
{
"type": "-",
"named": false
},
{
"type": ".",
"named": false
},
{
"type": "/",
"named": false
},
{
"type": "//",
"named": false
@ -1442,6 +1740,10 @@
"type": "=",
"named": false
},
{
"type": "==",
"named": false
},
{
"type": "=>",
"named": false
@ -1522,6 +1824,10 @@
"type": "{",
"named": false
},
{
"type": "||",
"named": false
},
{
"type": "}",
"named": false

File diff suppressed because it is too large Load Diff

7
examples/arithmetic.dcdl Normal file
View File

@ -0,0 +1,7 @@
{
workers = 2 + 2;
memory_gib = 1.5 * 4;
port = 9000 + 443;
timeout_seconds = 30 / 2;
negative_offset = -3;
}

View File

@ -0,0 +1,6 @@
{
base_roles = ["read", "write"];
extra_roles = ["admin"];
roles = ["read", "write"] ++ ["admin"];
ports = [8000 + 80] ++ [9000 + 443];
}

11
examples/logical.dcdl Normal file
View File

@ -0,0 +1,11 @@
let
env = "prod";
replicas = 3;
in
{
is_prod = env == "prod";
scaled = replicas > 1;
enabled = env == "prod" && replicas > 1;
disabled = !(env == "prod" && replicas > 1);
safe = env != "dev" || replicas >= 1;
}

View File

@ -0,0 +1,5 @@
import { defineConfig } from 'astro/config';
export default defineConfig({
output: 'static',
});

6480
site/decodal-site/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,17 @@
{
"name": "decodal-site",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"dev": "astro dev --host 0.0.0.0",
"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"
},
"dependencies": {
"@astrojs/check": "^0.9.4",
"astro": "^4.16.18",
"marked": "^12.0.2"
}
}

1
site/decodal-site/src/env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference path="../.astro/types.d.ts" />

View File

@ -0,0 +1,46 @@
---
import { nav } from '../lib/docs.js';
import '../style.css';
const { title = 'Decodal', active = '', playground = false } = Astro.props;
function renderNav(items, prefix = []) {
return `<ul class="nav-tree">${items
.map((item, index) => {
const number = [...prefix, index + 1];
const href = `/docs/${item.slug}/`;
const activeClass = active === item.slug ? ' class="active"' : '';
const children = item.children ? renderNav(item.children, number) : '';
return `<li><a${activeClass} href="${href}"><span class="nav-number">${number.join('.')}.</span>${item.title}</a>${children}</li>`;
})
.join('')}</ul>`;
}
---
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{title}</title>
</head>
<body>
<header class="topbar">
<a class="brand" href="/docs/introduction/">Decodal</a>
<nav class="topnav">
<a href="/docs/introduction/">Docs</a>
<a href="/playground/">Playground</a>
</nav>
</header>
<div class={playground ? 'layout playground-layout' : 'layout'}>
{!playground && (
<aside class="sidebar">
<a class="sidebar-title" href="/docs/">Manual</a>
<nav set:html={renderNav(nav)} />
</aside>
)}
<main class={playground ? 'playground' : ''}>
<slot />
</main>
</div>
</body>
</html>

View File

@ -0,0 +1,121 @@
import { marked } from 'marked';
import { escapeAttribute, highlightCode } from './highlight.js';
const modules = import.meta.glob('../../../../doc/manual/souce/**/*.md', {
query: '?raw',
import: 'default',
eager: true,
});
export const docs = Object.fromEntries(
Object.entries(modules).map(([path, content]) => {
const slug = path
.replace(/^\.\.\/\.\.\/\.\.\/\.\.\/doc\/manual\/souce\//, '')
.replace(/\.md$/, '')
.replace(/\/index$/, '');
return [slug || 'index', content];
}),
);
export const nav = [
{ title: 'Introduction', slug: 'introduction' },
{
title: 'Language Specification',
slug: 'language',
children: [
{ title: 'Syntax', slug: 'language/syntax' },
{
title: 'Value',
slug: 'language/value',
children: [
{ title: 'String', slug: 'language/value/string' },
{ title: 'Int', slug: 'language/value/int' },
{ title: 'Float', slug: 'language/value/float' },
{ title: 'Bool', slug: 'language/value/bool' },
],
},
{
title: 'Expression',
slug: 'language/expression',
children: [
{ title: 'Literal', slug: 'language/expression/literal' },
{ title: 'Identifier', slug: 'language/expression/identifier' },
{ title: 'Path Reference', slug: 'language/expression/path-reference' },
{ title: 'Object', slug: 'language/expression/object' },
{ title: 'Array', slug: 'language/expression/array' },
{ title: 'Function', slug: 'language/expression/function' },
{ title: 'Function Call', slug: 'language/expression/function-call' },
{ title: 'Let', slug: 'language/expression/let' },
{ title: 'Match', slug: 'language/expression/match' },
{ title: 'Import', slug: 'language/expression/import' },
{ title: 'Composition', slug: 'language/expression/composition' },
{ title: 'Default', slug: 'language/expression/default' },
{ title: 'Arithmetic', slug: 'language/expression/arithmetic' },
{ title: 'Logical and Comparison', slug: 'language/expression/logical-and-comparison' },
{ title: 'String Interpolation', slug: 'language/expression/string-interpolation' },
],
},
{ title: 'Constraints and Defaults', slug: 'language/constraints-and-defaults' },
{ title: 'Operators', slug: 'language/operators' },
{ title: 'Functions', slug: 'language/functions' },
{ title: 'Modules and Imports', slug: 'language/modules-and-imports' },
{ title: 'Evaluation Semantics', slug: 'language/evaluation' },
{ title: 'Materialization and Errors', slug: 'language/materialization-and-errors' },
{ title: 'Naming', slug: 'language/naming' },
{ title: 'Examples', slug: 'language/examples' },
],
},
{
title: 'Implementation Design',
slug: 'design',
children: [
{ title: 'Execution Pipeline', slug: 'design/execution-pipeline' },
{ title: 'Runtime Model', slug: 'design/runtime-model' },
{ title: 'Thunk and Lazy Evaluation', slug: 'design/thunk-and-lazy-evaluation' },
{ title: 'Composition and Materialization', slug: 'design/composition-and-materialization' },
{ title: 'Diagnostics and Fallback', slug: 'design/diagnostics-and-fallback' },
{ title: 'Embedding API', slug: 'design/embedding-api' },
{ title: 'Features', slug: 'design/features' },
],
},
{ title: 'Development', slug: 'development' },
{ title: 'Open Issues', slug: 'open-issues' },
];
const renderer = new marked.Renderer();
renderer.code = (code, language = '') => {
const normalizedLanguage = language.split(/\s+/)[0] ?? '';
const className = normalizedLanguage ? ` class="language-${escapeAttribute(normalizedLanguage)}"` : '';
return `<pre class="code-block"><code${className}>${highlightCode(code, normalizedLanguage)}</code></pre>`;
};
renderer.codespan = (code) => `<code>${code}</code>`;
marked.setOptions({ gfm: true, renderer });
export function allDocSlugs() {
return Object.keys(docs).filter((slug) => slug !== 'index');
}
export function renderMarkdown(slug) {
const source = docs[slug] ?? docs.index;
const html = marked.parse(source ?? '# Not found\n');
return html.replace(/href="([^"#][^"]*)\.md(#[^"]*)?"/g, (_all, href, hash = '') => {
const target = normalizeDocLink(slug, href);
return `href="/docs/${target}/${hash}"`;
});
}
function normalizeDocLink(currentSlug, href) {
const base = currentSlug.includes('/') ? currentSlug.split('/').slice(0, -1) : [];
const parts = [...base, ...href.split('/')];
const out = [];
for (const part of parts) {
if (!part || part === '.') continue;
if (part === '..') out.pop();
else out.push(part);
}
if (out[out.length - 1] === 'index') out.pop();
return out.join('/') || 'index';
}

View File

@ -0,0 +1,203 @@
const DECODAL_KEYWORDS = new Set(['let', 'in', 'fn', 'match', 'import', 'default']);
const DECODAL_TYPES = new Set(['String', 'Int', 'Float', 'Bool']);
const DECODAL_LITERALS = new Set(['true', 'false']);
const HTML_ESCAPE = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#39;',
};
export function escapeHtml(value) {
return String(value).replace(/[&<>"']/g, (char) => HTML_ESCAPE[char]);
}
export function escapeAttribute(value) {
return escapeHtml(value).replace(/`/g, '&#96;');
}
export function highlightCode(code, language = '') {
const normalized = language.toLowerCase();
if (normalized === 'dcdl' || normalized === 'decodal') return highlightDecodal(code);
if (normalized === 'sh' || normalized === 'bash' || normalized === 'shell') return highlightShell(code);
return escapeHtml(code);
}
export function highlightDecodal(source) {
let html = '';
let index = 0;
let canEndExpression = false;
while (index < source.length) {
const char = source[index];
const next = source[index + 1];
if (char === '#') {
const end = readUntilLineEnd(source, index);
html += token('comment', source.slice(index, end));
index = end;
continue;
}
if (char === '"') {
const end = readString(source, index);
html += token('string', source.slice(index, end));
index = end;
canEndExpression = true;
continue;
}
if (char === '/' && next && next !== '/' && !canEndExpression) {
const end = readRegex(source, index);
html += token('regex', source.slice(index, end));
index = end;
canEndExpression = true;
continue;
}
if (isNumberStart(source, index)) {
const end = readNumber(source, index);
html += token('number', source.slice(index, end));
index = end;
canEndExpression = true;
continue;
}
if (isIdentifierStart(char)) {
const end = readIdentifier(source, index);
const ident = source.slice(index, end);
if (DECODAL_KEYWORDS.has(ident)) html += token('keyword', ident);
else if (DECODAL_TYPES.has(ident)) html += token('type', ident);
else if (DECODAL_LITERALS.has(ident)) html += token('literal', ident);
else html += escapeHtml(ident);
canEndExpression = true;
index = end;
continue;
}
if (isOperatorStart(char)) {
const end = readOperator(source, index);
html += token('operator', source.slice(index, end));
canEndExpression = /[})\]]/.test(char);
index = end;
continue;
}
html += escapeHtml(char);
index += 1;
}
return html;
}
function highlightShell(source) {
return source
.split(/(\n)/)
.map((line) => {
if (line === '\n') return line;
const commentIndex = line.indexOf('#');
const code = commentIndex >= 0 ? line.slice(0, commentIndex) : line;
const comment = commentIndex >= 0 ? line.slice(commentIndex) : '';
return `${highlightShellCode(code)}${comment ? token('comment', comment) : ''}`;
})
.join('');
}
function highlightShellCode(source) {
const escaped = escapeHtml(source);
return escaped
.replace(/(&quot;[^&]*(?:&(?!quot;)[^&]*)*&quot;|'[^']*')/g, '<span class="tok-string">$1</span>')
.replace(/(^|\s)(--?[A-Za-z0-9][A-Za-z0-9-]*)/g, '$1<span class="tok-operator">$2</span>');
}
function token(kind, value) {
return `<span class="tok-${kind}">${escapeHtml(value)}</span>`;
}
function readUntilLineEnd(source, start) {
const end = source.indexOf('\n', start);
return end < 0 ? source.length : end;
}
function readString(source, start) {
let index = start + 1;
while (index < source.length) {
if (source[index] === '\\') {
index += 2;
continue;
}
if (source[index] === '"') return index + 1;
index += 1;
}
return source.length;
}
function readRegex(source, start) {
let index = start + 1;
let inClass = false;
while (index < source.length) {
const char = source[index];
if (char === '\\') {
index += 2;
continue;
}
if (char === '[') inClass = true;
else if (char === ']') inClass = false;
else if (char === '/' && !inClass) {
index += 1;
while (/[A-Za-z]/.test(source[index] ?? '')) index += 1;
return index;
}
if (char === '\n') return index;
index += 1;
}
return source.length;
}
function isNumberStart(source, index) {
const char = source[index];
const next = source[index + 1];
const prev = source[index - 1];
if (/[0-9]/.test(char)) return true;
return char === '-' && /[0-9]/.test(next ?? '') && !isIdentifierPart(prev ?? '');
}
function readNumber(source, start) {
let index = start;
if (source[index] === '-') index += 1;
while (/[0-9]/.test(source[index] ?? '')) index += 1;
if (source[index] === '.' && /[0-9]/.test(source[index + 1] ?? '')) {
index += 1;
while (/[0-9]/.test(source[index] ?? '')) index += 1;
}
return index;
}
function isIdentifierStart(char) {
return /[A-Za-z_]/.test(char ?? '');
}
function isIdentifierPart(char) {
return /[A-Za-z0-9_-]/.test(char ?? '');
}
function readIdentifier(source, start) {
let index = start + 1;
while (isIdentifierPart(source[index])) index += 1;
return index;
}
function isOperatorStart(char) {
return '&=<>!|:;,.{}()[]+-*/'.includes(char ?? '');
}
function readOperator(source, start) {
let index = start + 1;
if ('&|/+'.includes(source[start]) && source[index] === source[start]) return index + 1;
if ((source[start] === '<' || source[start] === '>' || source[start] === '=' || source[start] === '!') && source[index] === '=') {
index += 1;
}
return index;
}

View File

@ -0,0 +1,17 @@
---
import ManualLayout from '../../layouts/ManualLayout.astro';
import { allDocSlugs, renderMarkdown } from '../../lib/docs.js';
export function getStaticPaths() {
return allDocSlugs().map((slug) => ({
params: { slug },
props: { slug },
}));
}
const { slug } = Astro.props;
const html = renderMarkdown(slug);
---
<ManualLayout title={`Decodal - ${slug}`} active={slug}>
<article class="markdown" set:html={html} />
</ManualLayout>

View File

@ -0,0 +1,3 @@
---
return Astro.redirect('/docs/introduction/');
---

View File

@ -0,0 +1,3 @@
---
return Astro.redirect('/docs/introduction/');
---

View File

@ -0,0 +1,44 @@
---
import ManualLayout from '../layouts/ManualLayout.astro';
---
<ManualLayout title="Decodal Playground" playground>
<section class="playground-page">
<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">
<p id="status" class="status">Loading WASM...</p>
<button id="run" disabled>Run</button>
</div>
</div>
<div class="playground-shell">
<aside class="file-panel">
<div class="panel-header">
<span>Files</span>
<button id="new-file" type="button">New</button>
</div>
<div id="file-tree" class="file-tree"></div>
<button id="delete-file" class="danger-button" type="button">Delete file</button>
</aside>
<label 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>
<section class="pane output-pane">
<span>Output</span>
<pre id="output"></pre>
</section>
</div>
</section>
<script>
import '../scripts/playground.js';
</script>
</ManualLayout>

View File

@ -0,0 +1,193 @@
import init, { evaluateProject } from '../wasm/decodal_wasm.js';
import { highlightDecodal } from '../lib/highlight.js';
const STORAGE_KEY = 'decodal-playground-project-v1';
const starterFiles = {
'main.dcdl': `let
schema = import "./schemas/service.dcdl";
in
schema.Service & {
name = "api";
port = 9000 + 443;
tags = ["web"] ++ ["prod"];
feature.enable = 9000 + 443 > 9000 && true;
}
`,
'schemas/service.dcdl': `Service = {
name = String;
port = Int & > 443 default 8443;
feature.enable = Bool default true;
};
`,
};
const source = document.getElementById('source');
const sourceHighlight = document.getElementById('source-highlight');
const output = document.getElementById('output');
const run = document.getElementById('run');
const status = document.getElementById('status');
const fileTree = document.getElementById('file-tree');
const activeFile = document.getElementById('active-file');
const newFile = document.getElementById('new-file');
const deleteFile = document.getElementById('delete-file');
const project = loadProject();
setActiveFile(project.activePath);
renderFileTree();
updateHighlight();
function loadProject() {
try {
const stored = JSON.parse(localStorage.getItem(STORAGE_KEY) ?? 'null');
if (stored && stored.files && typeof stored.activePath === 'string') {
const files = normalizeFiles(stored.files);
const activePath = files[stored.activePath] === undefined ? Object.keys(files)[0] : stored.activePath;
if (activePath) return { files, activePath };
}
} catch (_error) {
// Fall back to the starter project.
}
return { files: { ...starterFiles }, activePath: 'main.dcdl' };
}
function normalizeFiles(files) {
return Object.fromEntries(
Object.entries(files)
.filter(([_path, value]) => typeof value === 'string')
.map(([path, value]) => [normalizePath(path), value])
.filter(([path]) => path),
);
}
function saveProject() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(project));
}
function normalizePath(path) {
const parts = [];
for (const part of String(path).replaceAll('\\', '/').split('/')) {
if (!part || part === '.') continue;
if (part === '..') parts.pop();
else parts.push(part);
}
return parts.join('/');
}
function setActiveFile(path) {
const normalized = normalizePath(path);
if (project.files[normalized] === undefined) return;
project.activePath = normalized;
source.value = 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;
saveProject();
const result = JSON.parse(evaluateProject(project.activePath, JSON.stringify(project.files)));
output.textContent = result.ok ? result.output : result.error;
output.classList.toggle('error', !result.ok);
}
function renderFileTree() {
const tree = buildTree(Object.keys(project.files).sort());
fileTree.replaceChildren(renderTreeList(tree.children));
}
function buildTree(paths) {
const root = { name: '', children: new Map(), path: '' };
for (const path of paths) {
const parts = path.split('/');
let node = root;
let currentPath = '';
parts.forEach((part, index) => {
currentPath = currentPath ? `${currentPath}/${part}` : part;
if (!node.children.has(part)) {
node.children.set(part, { name: part, children: new Map(), path: currentPath, file: index + 1 === parts.length });
}
node = node.children.get(part);
});
}
return root;
}
function renderTreeList(children) {
const list = document.createElement('ul');
for (const child of [...children.values()].sort(compareNodes)) {
const item = document.createElement('li');
if (child.file) {
const button = document.createElement('button');
button.type = 'button';
button.className = child.path === project.activePath ? 'file active' : 'file';
button.textContent = child.name;
button.title = child.path;
button.addEventListener('click', () => setActiveFile(child.path));
item.append(button);
} else {
const label = document.createElement('span');
label.className = 'folder';
label.textContent = `${child.name}/`;
item.append(label, renderTreeList(child.children));
}
list.append(item);
}
return list;
}
function compareNodes(a, b) {
if (a.file !== b.file) return a.file ? 1 : -1;
return a.name.localeCompare(b.name);
}
try {
await init();
run.disabled = false;
status.textContent = '';
execute();
} catch (error) {
status.textContent = `Failed to load WASM: ${error?.message ?? error}`;
}
run.addEventListener('click', execute);
newFile.addEventListener('click', () => {
const path = normalizePath(prompt('New virtual file path', 'schemas/types.dcdl') ?? '');
if (!path) return;
if (project.files[path] !== undefined) {
setActiveFile(path);
return;
}
project.files[path] = path.endsWith('.dcdl') ? '' : '// Decodal source\n';
setActiveFile(path);
});
deleteFile.addEventListener('click', () => {
if (Object.keys(project.files).length <= 1) return;
if (!confirm(`Delete ${project.activePath}?`)) return;
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

@ -0,0 +1,467 @@
:root {
color-scheme: light;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--bg: #f7f7f8;
--surface: #ffffff;
--text: #1f2328;
--heading: #111827;
--muted: #4b5563;
--subtle: #64748b;
--border: #e5e7eb;
--link: #2563eb;
--topbar-bg: #111827;
--topbar-text: #ffffff;
--topbar-link: #dbeafe;
--active-bg: #dbeafe;
--active-text: #1d4ed8;
--nav-number: #94a3b8;
--shadow: 0 8px 24px rgb(15 23 42 / 0.05);
--inline-code-bg: #eef2ff;
--inline-code-text: #3730a3;
--editor-bg: #0f172a;
--editor-text: #e5e7eb;
--danger: #b91c1c;
--error: #fecaca;
--selection: rgb(59 130 246 / 0.35);
}
@media (prefers-color-scheme: dark) {
:root {
color-scheme: dark;
--bg: #0b1120;
--surface: #111827;
--text: #d1d5db;
--heading: #f9fafb;
--muted: #9ca3af;
--subtle: #94a3b8;
--border: #334155;
--link: #60a5fa;
--topbar-bg: #020617;
--topbar-text: #f8fafc;
--topbar-link: #bfdbfe;
--active-bg: #1e3a8a;
--active-text: #bfdbfe;
--nav-number: #64748b;
--shadow: 0 8px 24px rgb(0 0 0 / 0.25);
--inline-code-bg: #1e293b;
--inline-code-text: #bfdbfe;
--danger: #fca5a5;
}
}
body {
background: var(--bg);
color: var(--text);
margin: 0;
}
a {
color: var(--link);
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
.topbar {
align-items: center;
background: var(--topbar-bg);
color: var(--topbar-text);
display: flex;
height: 56px;
justify-content: space-between;
padding: 0 24px;
}
.brand {
color: var(--topbar-text);
font-size: 20px;
font-weight: 700;
}
.topnav {
display: flex;
gap: 18px;
}
.topnav a {
color: var(--topbar-link);
}
.layout {
display: grid;
grid-template-columns: 300px minmax(0, 1fr);
min-height: calc(100vh - 56px);
}
.playground-layout {
display: block;
}
.sidebar {
background: var(--surface);
border-right: 1px solid var(--border);
overflow: auto;
padding: 22px 18px;
}
.sidebar-title {
color: var(--heading);
display: block;
font-weight: 700;
margin-bottom: 12px;
}
.nav-tree {
list-style: none;
margin: 0;
padding-left: 0;
}
.nav-tree .nav-tree {
margin: 4px 0 6px 12px;
}
.nav-tree a {
border-radius: 6px;
color: var(--text);
display: flex;
font-size: 14px;
gap: 6px;
line-height: 1.35;
padding: 4px 8px;
}
.nav-number {
color: var(--nav-number);
flex: 0 0 auto;
font-variant-numeric: tabular-nums;
}
.nav-tree a.active {
background: var(--active-bg);
color: var(--active-text);
font-weight: 600;
}
.nav-tree a.active .nav-number {
color: var(--link);
}
main {
min-width: 0;
padding: 36px;
}
main.playground {
padding: 10px;
}
.markdown {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
box-shadow: var(--shadow);
margin: 0 auto;
max-width: 920px;
padding: 24px 36px 42px;
}
.markdown h1,
.markdown h2,
.markdown h3 {
color: var(--heading);
}
.markdown pre,
.output-pane pre {
background: var(--editor-bg);
border-radius: 10px;
color: var(--editor-text);
overflow: auto;
padding: 14px;
}
.markdown code {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
}
.markdown :not(pre) > code {
background: var(--inline-code-bg);
border-radius: 4px;
color: var(--inline-code-text);
padding: 1px 4px;
}
.markdown table {
border-collapse: collapse;
display: block;
margin: 20px 0;
overflow-x: auto;
width: 100%;
}
.markdown th,
.markdown td {
border: 1px solid var(--border);
padding: 8px 10px;
text-align: left;
vertical-align: top;
}
.markdown th {
background: var(--inline-code-bg);
color: var(--heading);
font-weight: 700;
}
.markdown tr:nth-child(even) td {
background: color-mix(in srgb, var(--surface) 92%, var(--border));
}
.tok-keyword {
color: #93c5fd;
font-weight: 700;
}
.tok-type {
color: #67e8f9;
}
.tok-literal,
.tok-number {
color: #fbbf24;
}
.tok-string,
.tok-regex {
color: #86efac;
}
.tok-comment {
color: var(--nav-number);
font-style: italic;
}
.tok-operator {
color: #f9a8d4;
}
.playground-page {
height: calc(100vh - 76px);
}
.playground-header {
align-items: center;
display: flex;
gap: 16px;
justify-content: space-between;
margin-bottom: 8px;
}
.playground-header h1 {
font-size: 20px;
margin: 0 0 2px;
}
.playground-header p {
color: var(--muted);
font-size: 13px;
margin: 0;
}
.playground-header code {
background: var(--inline-code-bg);
border-radius: 4px;
color: var(--inline-code-text);
padding: 1px 4px;
}
.playground-actions {
align-items: center;
display: flex;
gap: 12px;
}
button {
background: var(--link);
border: 0;
border-radius: 8px;
color: var(--topbar-text);
cursor: pointer;
font-weight: 700;
padding: 8px 14px;
}
button:disabled {
cursor: not-allowed;
opacity: 0.5;
}
.playground-shell {
display: grid;
gap: 10px;
grid-template-columns: 220px minmax(360px, 1.25fr) minmax(320px, 1fr);
height: calc(100% - 52px);
min-height: 0;
}
.file-panel,
.pane {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 10px;
display: flex;
flex-direction: column;
min-height: 0;
overflow: hidden;
}
.panel-header,
.pane > span {
align-items: center;
border-bottom: 1px solid var(--border);
color: var(--text);
display: flex;
font-size: 13px;
font-weight: 700;
justify-content: space-between;
min-height: 38px;
padding: 0 10px;
text-transform: uppercase;
}
.panel-header button {
font-size: 12px;
padding: 5px 8px;
text-transform: none;
}
.file-tree {
flex: 1;
min-height: 0;
overflow: auto;
padding: 8px;
}
.file-tree ul {
list-style: none;
margin: 0;
padding-left: 12px;
}
.file-tree > ul {
padding-left: 0;
}
.file-tree li {
margin: 2px 0;
}
.file-tree .folder {
color: var(--subtle);
display: block;
font-size: 13px;
font-weight: 700;
padding: 4px 6px;
}
.file-tree .file {
background: transparent;
border-radius: 6px;
color: var(--text);
display: block;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
font-size: 13px;
font-weight: 500;
padding: 5px 7px;
text-align: left;
width: 100%;
}
.file-tree .file:hover,
.file-tree .file.active {
background: var(--active-bg);
color: var(--active-text);
text-decoration: none;
}
.danger-button {
background: transparent;
border-top: 1px solid var(--border);
border-radius: 0;
color: var(--danger);
padding: 9px 10px;
text-align: left;
}
.editor-wrap {
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;
}
.editor-wrap pre {
color: var(--editor-text);
pointer-events: none;
}
.editor-wrap textarea {
background: transparent;
caret-color: var(--editor-text);
color: transparent;
outline: none;
resize: none;
width: 100%;
}
.editor-wrap textarea::selection {
background: var(--selection);
}
.output-pane pre {
border-radius: 0;
flex: 1;
margin: 0;
}
.output-pane pre.error {
color: var(--error);
}
.status {
color: var(--muted);
font-size: 13px;
margin: 0;
white-space: nowrap;
}
@media (max-width: 900px) {
.layout {
grid-template-columns: 1fr;
}
.sidebar {
border-bottom: 1px solid var(--border);
border-right: 0;
max-height: 260px;
}
.playground-shell {
grid-template-columns: 1fr;
}
}

1
site/decodal-site/src/wasm/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
# wasm-pack output is committed for the playground build.

View File

@ -0,0 +1,40 @@
/* tslint:disable */
/* eslint-disable */
export function evaluate(source: string): string;
export function evaluateProject(entry: string, files_json: string): string;
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
export interface InitOutput {
readonly memory: WebAssembly.Memory;
readonly evaluate: (a: number, b: number, c: number) => void;
readonly evaluateProject: (a: number, b: number, c: number, d: number, e: number) => void;
readonly __wbindgen_add_to_stack_pointer: (a: number) => number;
readonly __wbindgen_export: (a: number, b: number) => number;
readonly __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
readonly __wbindgen_export3: (a: number, b: number, c: number) => 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>;

View File

@ -0,0 +1,238 @@
/* @ts-self-types="./decodal_wasm.d.ts" */
/**
* @param {string} source
* @returns {string}
*/
export function evaluate(source) {
let deferred2_0;
let deferred2_1;
try {
const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
const ptr0 = passStringToWasm0(source, wasm.__wbindgen_export, wasm.__wbindgen_export2);
const len0 = WASM_VECTOR_LEN;
wasm.evaluate(retptr, ptr0, len0);
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
deferred2_0 = r0;
deferred2_1 = r1;
return getStringFromWasm0(r0, r1);
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
wasm.__wbindgen_export3(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 retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
const ptr0 = passStringToWasm0(entry, wasm.__wbindgen_export, wasm.__wbindgen_export2);
const len0 = WASM_VECTOR_LEN;
const ptr1 = passStringToWasm0(files_json, wasm.__wbindgen_export, wasm.__wbindgen_export2);
const len1 = WASM_VECTOR_LEN;
wasm.evaluateProject(retptr, ptr0, len0, ptr1, len1);
var r0 = getDataViewMemory0().getInt32(retptr + 4 * 0, true);
var r1 = getDataViewMemory0().getInt32(retptr + 4 * 1, true);
deferred3_0 = r0;
deferred3_1 = r1;
return getStringFromWasm0(r0, r1);
} finally {
wasm.__wbindgen_add_to_stack_pointer(16);
wasm.__wbindgen_export3(deferred3_0, deferred3_1, 1);
}
}
function __wbg_get_imports() {
const import0 = {
__proto__: null,
};
return {
__proto__: null,
"./decodal_wasm_bg.js": import0,
};
}
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 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;
}
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;
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.

View File

@ -0,0 +1,9 @@
/* tslint:disable */
/* eslint-disable */
export const memory: WebAssembly.Memory;
export const evaluate: (a: number, b: number, c: number) => void;
export const evaluateProject: (a: number, b: number, c: number, d: number, e: number) => void;
export const __wbindgen_add_to_stack_pointer: (a: number) => number;
export const __wbindgen_export: (a: number, b: number) => number;
export const __wbindgen_export2: (a: number, b: number, c: number, d: number) => number;
export const __wbindgen_export3: (a: number, b: number, c: number) => void;

View File

@ -0,0 +1,15 @@
{
"name": "decodal-wasm",
"type": "module",
"version": "0.1.0",
"files": [
"decodal_wasm_bg.wasm",
"decodal_wasm.js",
"decodal_wasm.d.ts"
],
"main": "decodal_wasm.js",
"types": "decodal_wasm.d.ts",
"sideEffects": [
"./snippets/*"
]
}