Compare commits
12 Commits
58d2c9b423
...
01ad6dca52
| Author | SHA1 | Date | |
|---|---|---|---|
| 01ad6dca52 | |||
| 6da0ec4c77 | |||
| 683151f6bd | |||
| 9e728fb634 | |||
| 2fe54bda62 | |||
| 3f7dd7c692 | |||
| dc28cddbff | |||
| aa6e5c51c1 | |||
| 4ab47e5719 | |||
| 19c9de1601 | |||
| 0e873fbd51 | |||
| 4020b7c2d5 |
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -3,3 +3,9 @@
|
||||||
/.env
|
/.env
|
||||||
/.yoi
|
/.yoi
|
||||||
/result
|
/result
|
||||||
|
node_modules
|
||||||
|
/dist
|
||||||
|
site/decodal-site/dist
|
||||||
|
|
||||||
|
# Astro
|
||||||
|
site/decodal-site/.astro
|
||||||
|
|
|
||||||
167
Cargo.lock
generated
167
Cargo.lock
generated
|
|
@ -11,6 +11,18 @@ dependencies = [
|
||||||
"memchr",
|
"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]]
|
[[package]]
|
||||||
name = "decodal"
|
name = "decodal"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|
@ -25,12 +37,51 @@ dependencies = [
|
||||||
"regex",
|
"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]]
|
[[package]]
|
||||||
name = "memchr"
|
name = "memchr"
|
||||||
version = "2.8.2"
|
version = "2.8.2"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
|
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]]
|
[[package]]
|
||||||
name = "regex"
|
name = "regex"
|
||||||
version = "1.12.4"
|
version = "1.12.4"
|
||||||
|
|
@ -59,3 +110,119 @@ name = "regex-syntax"
|
||||||
version = "0.8.11"
|
version = "0.8.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
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"
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
members = [
|
members = [
|
||||||
"crates/decodal-core",
|
"crates/decodal-core",
|
||||||
"crates/decodal-cli",
|
"crates/decodal-cli",
|
||||||
|
"crates/decodal-wasm",
|
||||||
]
|
]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,10 @@ pub enum Expr {
|
||||||
scrutinee: ExprId,
|
scrutinee: ExprId,
|
||||||
arms: Vec<MatchArm>,
|
arms: Vec<MatchArm>,
|
||||||
},
|
},
|
||||||
|
Unary {
|
||||||
|
op: UnaryOp,
|
||||||
|
expr: ExprId,
|
||||||
|
},
|
||||||
Binary {
|
Binary {
|
||||||
op: BinaryOp,
|
op: BinaryOp,
|
||||||
lhs: ExprId,
|
lhs: ExprId,
|
||||||
|
|
@ -117,8 +121,27 @@ pub enum Literal {
|
||||||
Bool(bool),
|
Bool(bool),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum UnaryOp {
|
||||||
|
Neg,
|
||||||
|
Not,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum BinaryOp {
|
pub enum BinaryOp {
|
||||||
|
Add,
|
||||||
|
Sub,
|
||||||
|
Mul,
|
||||||
|
Div,
|
||||||
|
Concat,
|
||||||
|
Equal,
|
||||||
|
NotEqual,
|
||||||
|
Greater,
|
||||||
|
GreaterEqual,
|
||||||
|
Less,
|
||||||
|
LessEqual,
|
||||||
|
LogicalAnd,
|
||||||
|
LogicalOr,
|
||||||
And,
|
And,
|
||||||
Patch,
|
Patch,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use alloc::{format, string::String, vec, vec::Vec};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
ExprId, SourceForm, SourceId, Span,
|
ExprId, SourceForm, SourceId, Span,
|
||||||
ast::{Ast, BinaryOp, CompareOp, Expr, Field, Literal},
|
ast::{Ast, BinaryOp, CompareOp, Expr, Field, Literal, UnaryOp},
|
||||||
constraints::normalize_constraints,
|
constraints::normalize_constraints,
|
||||||
diagnostic::{Diagnostic, DiagnosticKind, Result},
|
diagnostic::{Diagnostic, DiagnosticKind, Result},
|
||||||
embedding::HostValue,
|
embedding::HostValue,
|
||||||
|
|
@ -337,24 +337,106 @@ impl<L: SourceLoader> Engine<L> {
|
||||||
"no match arm matched",
|
"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 } => {
|
Expr::Binary { op, lhs, rhs } => {
|
||||||
let lhs = self.eval_expr(
|
let lhs_value = self.eval_expr(
|
||||||
ExprRef {
|
ExprRef {
|
||||||
module: reference.module,
|
module: reference.module,
|
||||||
expr: lhs,
|
expr: lhs,
|
||||||
},
|
},
|
||||||
env,
|
env,
|
||||||
)?;
|
)?;
|
||||||
let rhs = self.eval_expr(
|
|
||||||
ExprRef {
|
|
||||||
module: reference.module,
|
|
||||||
expr: rhs,
|
|
||||||
},
|
|
||||||
env,
|
|
||||||
)?;
|
|
||||||
match op {
|
match op {
|
||||||
BinaryOp::And => self.compose_and(lhs, rhs, span),
|
BinaryOp::LogicalAnd => {
|
||||||
BinaryOp::Patch => self.patch(lhs, rhs),
|
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 } => {
|
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 {
|
fn compare_value(value: &RuntimeValue, op: CompareOp, expected: &LiteralValue) -> bool {
|
||||||
match (value, expected) {
|
match (value, expected) {
|
||||||
(RuntimeValue::Concrete(ConcreteValue::Int(actual)), LiteralValue::Int(expected)) => {
|
(RuntimeValue::Concrete(ConcreteValue::Int(actual)), LiteralValue::Int(expected)) => {
|
||||||
|
|
@ -1072,6 +1378,111 @@ mod tests {
|
||||||
assert!(matches!(data, Data::Object(_)));
|
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]
|
#[test]
|
||||||
fn composes_schema_and_value() {
|
fn composes_schema_and_value() {
|
||||||
let data = eval_data(
|
let data = eval_data(
|
||||||
|
|
|
||||||
|
|
@ -34,8 +34,18 @@ pub enum TokenKind {
|
||||||
Dot,
|
Dot,
|
||||||
Colon,
|
Colon,
|
||||||
Equal,
|
Equal,
|
||||||
|
EqualEqual,
|
||||||
|
Bang,
|
||||||
|
BangEqual,
|
||||||
Arrow,
|
Arrow,
|
||||||
Amp,
|
Amp,
|
||||||
|
AmpAmp,
|
||||||
|
PipePipe,
|
||||||
|
Plus,
|
||||||
|
PlusPlus,
|
||||||
|
Minus,
|
||||||
|
Star,
|
||||||
|
Slash,
|
||||||
SlashSlash,
|
SlashSlash,
|
||||||
Gt,
|
Gt,
|
||||||
Gte,
|
Gte,
|
||||||
|
|
@ -67,9 +77,13 @@ impl<'a> Lexer<'a> {
|
||||||
|
|
||||||
pub fn tokenize(mut self) -> Result<Vec<Token>> {
|
pub fn tokenize(mut self) -> Result<Vec<Token>> {
|
||||||
let mut tokens = Vec::new();
|
let mut tokens = Vec::new();
|
||||||
|
let mut previous = None;
|
||||||
loop {
|
loop {
|
||||||
let token = self.next_token()?;
|
let token = self.next_token(previous.as_ref())?;
|
||||||
let is_eof = token.kind == TokenKind::Eof;
|
let is_eof = token.kind == TokenKind::Eof;
|
||||||
|
if !is_eof {
|
||||||
|
previous = Some(token.kind.clone());
|
||||||
|
}
|
||||||
tokens.push(token);
|
tokens.push(token);
|
||||||
if is_eof {
|
if is_eof {
|
||||||
return Ok(tokens);
|
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();
|
self.skip_ws_and_comments();
|
||||||
let start = self.pos;
|
let start = self.pos;
|
||||||
let Some(ch) = self.peek() else {
|
let Some(ch) = self.peek() else {
|
||||||
|
|
@ -134,16 +148,57 @@ impl<'a> Lexer<'a> {
|
||||||
}
|
}
|
||||||
b'&' => {
|
b'&' => {
|
||||||
self.pos += 1;
|
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'=' => {
|
b'=' => {
|
||||||
self.pos += 1;
|
self.pos += 1;
|
||||||
if self.consume(b'>') {
|
if self.consume(b'>') {
|
||||||
TokenKind::Arrow
|
TokenKind::Arrow
|
||||||
|
} else if self.consume(b'=') {
|
||||||
|
TokenKind::EqualEqual
|
||||||
} else {
|
} else {
|
||||||
TokenKind::Equal
|
TokenKind::Equal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
b'!' => {
|
||||||
|
self.pos += 1;
|
||||||
|
if self.consume(b'=') {
|
||||||
|
TokenKind::BangEqual
|
||||||
|
} else {
|
||||||
|
TokenKind::Bang
|
||||||
|
}
|
||||||
|
}
|
||||||
b'>' => {
|
b'>' => {
|
||||||
self.pos += 1;
|
self.pos += 1;
|
||||||
if self.consume(b'=') {
|
if self.consume(b'=') {
|
||||||
|
|
@ -164,6 +219,8 @@ impl<'a> Lexer<'a> {
|
||||||
self.pos += 1;
|
self.pos += 1;
|
||||||
if self.consume(b'/') {
|
if self.consume(b'/') {
|
||||||
TokenKind::SlashSlash
|
TokenKind::SlashSlash
|
||||||
|
} else if previous.is_some_and(token_can_end_expr) {
|
||||||
|
TokenKind::Slash
|
||||||
} else {
|
} else {
|
||||||
self.lex_regex(start)?
|
self.lex_regex(start)?
|
||||||
}
|
}
|
||||||
|
|
@ -341,6 +398,23 @@ fn is_ident_continue(c: u8) -> bool {
|
||||||
c.is_ascii_alphanumeric() || c == b'_'
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ use alloc::{string::String, vec::Vec};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
SourceId, Span,
|
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},
|
diagnostic::{Diagnostic, Result},
|
||||||
lexer::{Lexer, Token, TokenKind},
|
lexer::{Lexer, Token, TokenKind},
|
||||||
};
|
};
|
||||||
|
|
@ -115,6 +115,110 @@ impl Parser {
|
||||||
let rhs = self.parse_expr(r_bp)?;
|
let rhs = self.parse_expr(r_bp)?;
|
||||||
let span = self.ast.span(lhs).join(self.ast.span(rhs)).join(op_span);
|
let span = self.ast.span(lhs).join(self.ast.span(rhs)).join(op_span);
|
||||||
lhs = match kind {
|
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(
|
InfixKind::And => self.ast.push(
|
||||||
Expr::Binary {
|
Expr::Binary {
|
||||||
op: BinaryOp::And,
|
op: BinaryOp::And,
|
||||||
|
|
@ -173,6 +277,28 @@ impl Parser {
|
||||||
TokenKind::Let => self.parse_let(token.span),
|
TokenKind::Let => self.parse_let(token.span),
|
||||||
TokenKind::Match => self.parse_match(token.span),
|
TokenKind::Match => self.parse_match(token.span),
|
||||||
TokenKind::Import => self.parse_import(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 => {
|
TokenKind::Gt | TokenKind::Gte | TokenKind::Lt | TokenKind::Lte => {
|
||||||
let op = match token.kind {
|
let op = match token.kind {
|
||||||
TokenKind::Gt => CompareOp::Gt,
|
TokenKind::Gt => CompareOp::Gt,
|
||||||
|
|
@ -181,7 +307,7 @@ impl Parser {
|
||||||
TokenKind::Lte => CompareOp::Lte,
|
TokenKind::Lte => CompareOp::Lte,
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
let value = self.parse_expr(8)?;
|
let value = self.parse_expr(12)?;
|
||||||
let span = token.span.join(self.ast.span(value));
|
let span = token.span.join(self.ast.span(value));
|
||||||
Ok(self.ast.push(Expr::CompareConstraint { op, value }, span))
|
Ok(self.ast.push(Expr::CompareConstraint { op, value }, span))
|
||||||
}
|
}
|
||||||
|
|
@ -406,6 +532,19 @@ impl Parser {
|
||||||
TokenKind::Default => Some((InfixKind::Default, 1, 2)),
|
TokenKind::Default => Some((InfixKind::Default, 1, 2)),
|
||||||
TokenKind::SlashSlash => Some((InfixKind::Patch, 3, 4)),
|
TokenKind::SlashSlash => Some((InfixKind::Patch, 3, 4)),
|
||||||
TokenKind::Amp => Some((InfixKind::And, 5, 6)),
|
TokenKind::Amp => Some((InfixKind::And, 5, 6)),
|
||||||
|
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,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -498,6 +637,19 @@ impl Parser {
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
#[derive(Debug, Clone, Copy)]
|
||||||
enum InfixKind {
|
enum InfixKind {
|
||||||
|
Add,
|
||||||
|
Sub,
|
||||||
|
Mul,
|
||||||
|
Div,
|
||||||
|
Concat,
|
||||||
|
Equal,
|
||||||
|
NotEqual,
|
||||||
|
Greater,
|
||||||
|
GreaterEqual,
|
||||||
|
Less,
|
||||||
|
LessEqual,
|
||||||
|
LogicalAnd,
|
||||||
|
LogicalOr,
|
||||||
And,
|
And,
|
||||||
Patch,
|
Patch,
|
||||||
Default,
|
Default,
|
||||||
|
|
|
||||||
15
crates/decodal-wasm/Cargo.toml
Normal file
15
crates/decodal-wasm/Cargo.toml
Normal 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
|
||||||
233
crates/decodal-wasm/src/lib.rs
Normal file
233
crates/decodal-wasm/src/lib.rs
Normal 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"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -4,11 +4,13 @@ pkgs.mkShell {
|
||||||
cargo
|
cargo
|
||||||
clippy
|
clippy
|
||||||
git
|
git
|
||||||
|
lld
|
||||||
nodejs
|
nodejs
|
||||||
nixfmt
|
nixfmt
|
||||||
rustc
|
rustc
|
||||||
rustfmt
|
rustfmt
|
||||||
tree-sitter
|
tree-sitter
|
||||||
|
wasm-pack
|
||||||
];
|
];
|
||||||
|
|
||||||
shellHook = ''
|
shellHook = ''
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,51 @@ cargo test -p decodal-core --features regex
|
||||||
cargo run -q -p decodal --features regex -- examples/regex/main.dcdl
|
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
|
## Tree-sitter grammar
|
||||||
|
|
||||||
The Tree-sitter grammar is kept in:
|
The Tree-sitter grammar is kept in:
|
||||||
|
|
@ -73,7 +118,7 @@ When the Decodal syntax changes:
|
||||||
|
|
||||||
## Development shell
|
## 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
|
```sh
|
||||||
nix develop
|
nix develop
|
||||||
|
|
@ -87,5 +132,7 @@ The shell provides:
|
||||||
- `clippy`
|
- `clippy`
|
||||||
- `node`
|
- `node`
|
||||||
- `npm`
|
- `npm`
|
||||||
|
- `wasm-pack`
|
||||||
|
- `lld`
|
||||||
- `tree-sitter`
|
- `tree-sitter`
|
||||||
- `nixfmt`
|
- `nixfmt`
|
||||||
|
|
|
||||||
43
doc/manual/souce/language/expression/arithmetic.md
Normal file
43
doc/manual/souce/language/expression/arithmetic.md
Normal 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;
|
||||||
|
```
|
||||||
|
|
@ -7,9 +7,21 @@ array expression は、順序付きの値の列を表す。
|
||||||
["a", "b", "c"]
|
["a", "b", "c"]
|
||||||
```
|
```
|
||||||
|
|
||||||
## 未確定事項
|
## Array concat
|
||||||
|
|
||||||
- 配列要素の制約表現。
|
`++` は concrete array 同士を連結する。
|
||||||
- 異種配列を許可するか。
|
|
||||||
- `//` による patch を右辺置換だけにするか。
|
```dcdl
|
||||||
- append / prepend / remove などの操作を提供するか。
|
base = ["read", "write"];
|
||||||
|
extra = ["admin"];
|
||||||
|
roles = base ++ extra;
|
||||||
|
```
|
||||||
|
|
||||||
|
`roles` は以下と同じ値になる。
|
||||||
|
|
||||||
|
```dcdl
|
||||||
|
["read", "write", "admin"]
|
||||||
|
```
|
||||||
|
|
||||||
|
`++` は配列要素を変換しない。
|
||||||
|
左辺の要素の後に右辺の要素が並ぶ。
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
```
|
||||||
|
|
@ -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) を参照する。
|
||||||
|
|
||||||
## `&`: 制約合成
|
## `&`: 制約合成
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -115,12 +115,15 @@ rec
|
||||||
主要な演算子は以下である。
|
主要な演算子は以下である。
|
||||||
|
|
||||||
```text
|
```text
|
||||||
& 制約合成
|
+ - * / 四則演算
|
||||||
// patch 合成
|
++ 配列結合
|
||||||
default fallback 指定
|
! && || 論理演算
|
||||||
=> 関数
|
== != < <= > >= 比較式
|
||||||
. フィールド参照 / ドットパス定義
|
& 制約合成
|
||||||
|
// patch 合成
|
||||||
|
default fallback 指定
|
||||||
|
=> 関数
|
||||||
|
. フィールド参照 / ドットパス定義
|
||||||
```
|
```
|
||||||
|
|
||||||
演算子の優先順位は未確定である。
|
演算子の優先順位は [合成演算子](./operators.md) で定義する。
|
||||||
詳細は [合成演算子](./operators.md) で定義する。
|
|
||||||
|
|
|
||||||
|
|
@ -74,3 +74,87 @@ base // {
|
||||||
body: (literal (string)))
|
body: (literal (string)))
|
||||||
(match_arm
|
(match_arm
|
||||||
body: (literal (string))))))))
|
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)))))))
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,16 @@
|
||||||
const PREC = {
|
const PREC = {
|
||||||
DEFAULT: 1,
|
DEFAULT: 1,
|
||||||
PATCH: 2,
|
PATCH: 2,
|
||||||
AND: 3,
|
COMPOSE: 3,
|
||||||
CALL: 7,
|
OR: 4,
|
||||||
PATH: 8,
|
LOGICAL_AND: 5,
|
||||||
|
COMPARE: 6,
|
||||||
|
CONCAT: 7,
|
||||||
|
ADD: 8,
|
||||||
|
MUL: 9,
|
||||||
|
UNARY: 10,
|
||||||
|
CALL: 11,
|
||||||
|
PATH: 12,
|
||||||
};
|
};
|
||||||
|
|
||||||
function commaSep(rule) {
|
function commaSep(rule) {
|
||||||
|
|
@ -52,6 +59,7 @@ module.exports = grammar({
|
||||||
$.parenthesized_expression,
|
$.parenthesized_expression,
|
||||||
$.call_expression,
|
$.call_expression,
|
||||||
$.path_expression,
|
$.path_expression,
|
||||||
|
$.unary_expression,
|
||||||
$.binary_expression,
|
$.binary_expression,
|
||||||
$.default_expression,
|
$.default_expression,
|
||||||
),
|
),
|
||||||
|
|
@ -159,13 +167,48 @@ module.exports = grammar({
|
||||||
field('field', $.identifier),
|
field('field', $.identifier),
|
||||||
)),
|
)),
|
||||||
|
|
||||||
comparison_constraint: $ => prec(6, seq(
|
comparison_constraint: $ => prec.right(PREC.UNARY + 1, seq(
|
||||||
field('operator', choice('>', '>=', '<', '<=')),
|
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(
|
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('left', $._expression),
|
||||||
field('operator', '&'),
|
field('operator', '&'),
|
||||||
field('right', $._expression),
|
field('right', $._expression),
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,16 @@
|
||||||
[
|
[
|
||||||
"&"
|
"&"
|
||||||
"//"
|
"//"
|
||||||
|
"+"
|
||||||
|
"-"
|
||||||
|
"*"
|
||||||
|
"/"
|
||||||
|
"++"
|
||||||
|
"&&"
|
||||||
|
"||"
|
||||||
|
"!"
|
||||||
|
"=="
|
||||||
|
"!="
|
||||||
"=>"
|
"=>"
|
||||||
"="
|
"="
|
||||||
">"
|
">"
|
||||||
|
|
|
||||||
307
editors/tree-sitter-decodal/src/grammar.json
generated
307
editors/tree-sitter-decodal/src/grammar.json
generated
|
|
@ -127,6 +127,10 @@
|
||||||
"type": "SYMBOL",
|
"type": "SYMBOL",
|
||||||
"name": "path_expression"
|
"name": "path_expression"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "SYMBOL",
|
||||||
|
"name": "unary_expression"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "SYMBOL",
|
"type": "SYMBOL",
|
||||||
"name": "binary_expression"
|
"name": "binary_expression"
|
||||||
|
|
@ -712,7 +716,7 @@
|
||||||
},
|
},
|
||||||
"call_expression": {
|
"call_expression": {
|
||||||
"type": "PREC_LEFT",
|
"type": "PREC_LEFT",
|
||||||
"value": 7,
|
"value": 11,
|
||||||
"content": {
|
"content": {
|
||||||
"type": "SEQ",
|
"type": "SEQ",
|
||||||
"members": [
|
"members": [
|
||||||
|
|
@ -782,7 +786,7 @@
|
||||||
},
|
},
|
||||||
"path_expression": {
|
"path_expression": {
|
||||||
"type": "PREC_LEFT",
|
"type": "PREC_LEFT",
|
||||||
"value": 8,
|
"value": 12,
|
||||||
"content": {
|
"content": {
|
||||||
"type": "SEQ",
|
"type": "SEQ",
|
||||||
"members": [
|
"members": [
|
||||||
|
|
@ -810,8 +814,8 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"comparison_constraint": {
|
"comparison_constraint": {
|
||||||
"type": "PREC",
|
"type": "PREC_RIGHT",
|
||||||
"value": 6,
|
"value": 11,
|
||||||
"content": {
|
"content": {
|
||||||
"type": "SEQ",
|
"type": "SEQ",
|
||||||
"members": [
|
"members": [
|
||||||
|
|
@ -843,19 +847,44 @@
|
||||||
{
|
{
|
||||||
"type": "FIELD",
|
"type": "FIELD",
|
||||||
"name": "value",
|
"name": "value",
|
||||||
|
"content": {
|
||||||
|
"type": "SYMBOL",
|
||||||
|
"name": "_expression"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"unary_expression": {
|
||||||
|
"type": "PREC",
|
||||||
|
"value": 10,
|
||||||
|
"content": {
|
||||||
|
"type": "SEQ",
|
||||||
|
"members": [
|
||||||
|
{
|
||||||
|
"type": "FIELD",
|
||||||
|
"name": "operator",
|
||||||
"content": {
|
"content": {
|
||||||
"type": "CHOICE",
|
"type": "CHOICE",
|
||||||
"members": [
|
"members": [
|
||||||
{
|
{
|
||||||
"type": "SYMBOL",
|
"type": "STRING",
|
||||||
"name": "integer"
|
"value": "-"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "SYMBOL",
|
"type": "STRING",
|
||||||
"name": "float"
|
"value": "!"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "FIELD",
|
||||||
|
"name": "operand",
|
||||||
|
"content": {
|
||||||
|
"type": "SYMBOL",
|
||||||
|
"name": "_expression"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -863,6 +892,268 @@
|
||||||
"binary_expression": {
|
"binary_expression": {
|
||||||
"type": "CHOICE",
|
"type": "CHOICE",
|
||||||
"members": [
|
"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",
|
"type": "PREC_LEFT",
|
||||||
"value": 3,
|
"value": 3,
|
||||||
|
|
|
||||||
310
editors/tree-sitter-decodal/src/node-types.json
generated
310
editors/tree-sitter-decodal/src/node-types.json
generated
|
|
@ -66,6 +66,10 @@
|
||||||
{
|
{
|
||||||
"type": "regex_literal",
|
"type": "regex_literal",
|
||||||
"named": true
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "unary_expression",
|
||||||
|
"named": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -137,6 +141,10 @@
|
||||||
{
|
{
|
||||||
"type": "regex_literal",
|
"type": "regex_literal",
|
||||||
"named": true
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "unary_expression",
|
||||||
|
"named": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -144,13 +152,65 @@
|
||||||
"multiple": false,
|
"multiple": false,
|
||||||
"required": true,
|
"required": true,
|
||||||
"types": [
|
"types": [
|
||||||
|
{
|
||||||
|
"type": "!=",
|
||||||
|
"named": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "&",
|
"type": "&",
|
||||||
"named": false
|
"named": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "&&",
|
||||||
|
"named": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "*",
|
||||||
|
"named": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "+",
|
||||||
|
"named": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "++",
|
||||||
|
"named": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "-",
|
||||||
|
"named": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "/",
|
||||||
|
"named": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "//",
|
"type": "//",
|
||||||
"named": false
|
"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",
|
"type": "regex_literal",
|
||||||
"named": true
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "unary_expression",
|
||||||
|
"named": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -294,6 +358,10 @@
|
||||||
{
|
{
|
||||||
"type": "regex_literal",
|
"type": "regex_literal",
|
||||||
"named": true
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "unary_expression",
|
||||||
|
"named": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -361,6 +429,10 @@
|
||||||
{
|
{
|
||||||
"type": "regex_literal",
|
"type": "regex_literal",
|
||||||
"named": true
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "unary_expression",
|
||||||
|
"named": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -396,11 +468,67 @@
|
||||||
"required": true,
|
"required": true,
|
||||||
"types": [
|
"types": [
|
||||||
{
|
{
|
||||||
"type": "float",
|
"type": "array",
|
||||||
"named": true
|
"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
|
"named": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
@ -474,6 +602,10 @@
|
||||||
{
|
{
|
||||||
"type": "regex_literal",
|
"type": "regex_literal",
|
||||||
"named": true
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "unary_expression",
|
||||||
|
"named": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -540,6 +672,10 @@
|
||||||
{
|
{
|
||||||
"type": "regex_literal",
|
"type": "regex_literal",
|
||||||
"named": true
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "unary_expression",
|
||||||
|
"named": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -622,6 +758,10 @@
|
||||||
{
|
{
|
||||||
"type": "regex_literal",
|
"type": "regex_literal",
|
||||||
"named": true
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "unary_expression",
|
||||||
|
"named": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -709,6 +849,10 @@
|
||||||
{
|
{
|
||||||
"type": "regex_literal",
|
"type": "regex_literal",
|
||||||
"named": true
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "unary_expression",
|
||||||
|
"named": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -807,6 +951,10 @@
|
||||||
{
|
{
|
||||||
"type": "regex_literal",
|
"type": "regex_literal",
|
||||||
"named": true
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "unary_expression",
|
||||||
|
"named": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -916,6 +1064,10 @@
|
||||||
{
|
{
|
||||||
"type": "regex_literal",
|
"type": "regex_literal",
|
||||||
"named": true
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "unary_expression",
|
||||||
|
"named": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -986,6 +1138,10 @@
|
||||||
{
|
{
|
||||||
"type": "regex_literal",
|
"type": "regex_literal",
|
||||||
"named": true
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "unary_expression",
|
||||||
|
"named": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -1058,6 +1214,10 @@
|
||||||
{
|
{
|
||||||
"type": "regex_literal",
|
"type": "regex_literal",
|
||||||
"named": true
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "unary_expression",
|
||||||
|
"named": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -1155,6 +1315,10 @@
|
||||||
{
|
{
|
||||||
"type": "regex_literal",
|
"type": "regex_literal",
|
||||||
"named": true
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "unary_expression",
|
||||||
|
"named": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -1237,6 +1401,10 @@
|
||||||
{
|
{
|
||||||
"type": "regex_literal",
|
"type": "regex_literal",
|
||||||
"named": true
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "unary_expression",
|
||||||
|
"named": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -1318,6 +1486,10 @@
|
||||||
{
|
{
|
||||||
"type": "regex_literal",
|
"type": "regex_literal",
|
||||||
"named": true
|
"named": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "unary_expression",
|
||||||
|
"named": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -1394,14 +1566,120 @@
|
||||||
{
|
{
|
||||||
"type": "regex_literal",
|
"type": "regex_literal",
|
||||||
"named": true
|
"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": "&",
|
"type": "&",
|
||||||
"named": false
|
"named": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "&&",
|
||||||
|
"named": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "(",
|
"type": "(",
|
||||||
"named": false
|
"named": false
|
||||||
|
|
@ -1410,14 +1688,34 @@
|
||||||
"type": ")",
|
"type": ")",
|
||||||
"named": false
|
"named": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "*",
|
||||||
|
"named": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "+",
|
||||||
|
"named": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "++",
|
||||||
|
"named": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": ",",
|
"type": ",",
|
||||||
"named": false
|
"named": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "-",
|
||||||
|
"named": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": ".",
|
"type": ".",
|
||||||
"named": false
|
"named": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "/",
|
||||||
|
"named": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "//",
|
"type": "//",
|
||||||
"named": false
|
"named": false
|
||||||
|
|
@ -1442,6 +1740,10 @@
|
||||||
"type": "=",
|
"type": "=",
|
||||||
"named": false
|
"named": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "==",
|
||||||
|
"named": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "=>",
|
"type": "=>",
|
||||||
"named": false
|
"named": false
|
||||||
|
|
@ -1522,6 +1824,10 @@
|
||||||
"type": "{",
|
"type": "{",
|
||||||
"named": false
|
"named": false
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"type": "||",
|
||||||
|
"named": false
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"type": "}",
|
"type": "}",
|
||||||
"named": false
|
"named": false
|
||||||
|
|
|
||||||
11282
editors/tree-sitter-decodal/src/parser.c
generated
11282
editors/tree-sitter-decodal/src/parser.c
generated
File diff suppressed because it is too large
Load Diff
7
examples/arithmetic.dcdl
Normal file
7
examples/arithmetic.dcdl
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
workers = 2 + 2;
|
||||||
|
memory_gib = 1.5 * 4;
|
||||||
|
port = 9000 + 443;
|
||||||
|
timeout_seconds = 30 / 2;
|
||||||
|
negative_offset = -3;
|
||||||
|
}
|
||||||
6
examples/array-concat.dcdl
Normal file
6
examples/array-concat.dcdl
Normal 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
11
examples/logical.dcdl
Normal 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;
|
||||||
|
}
|
||||||
5
site/decodal-site/astro.config.mjs
Normal file
5
site/decodal-site/astro.config.mjs
Normal 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
6480
site/decodal-site/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
17
site/decodal-site/package.json
Normal file
17
site/decodal-site/package.json
Normal 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
1
site/decodal-site/src/env.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
/// <reference path="../.astro/types.d.ts" />
|
||||||
46
site/decodal-site/src/layouts/ManualLayout.astro
Normal file
46
site/decodal-site/src/layouts/ManualLayout.astro
Normal 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>
|
||||||
121
site/decodal-site/src/lib/docs.js
Normal file
121
site/decodal-site/src/lib/docs.js
Normal 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';
|
||||||
|
}
|
||||||
203
site/decodal-site/src/lib/highlight.js
Normal file
203
site/decodal-site/src/lib/highlight.js
Normal 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 = {
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'"': '"',
|
||||||
|
"'": ''',
|
||||||
|
};
|
||||||
|
|
||||||
|
export function escapeHtml(value) {
|
||||||
|
return String(value).replace(/[&<>"']/g, (char) => HTML_ESCAPE[char]);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function escapeAttribute(value) {
|
||||||
|
return escapeHtml(value).replace(/`/g, '`');
|
||||||
|
}
|
||||||
|
|
||||||
|
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;)[^&]*)*"|'[^']*')/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;
|
||||||
|
}
|
||||||
17
site/decodal-site/src/pages/docs/[...slug].astro
Normal file
17
site/decodal-site/src/pages/docs/[...slug].astro
Normal 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>
|
||||||
3
site/decodal-site/src/pages/docs/index.astro
Normal file
3
site/decodal-site/src/pages/docs/index.astro
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
---
|
||||||
|
return Astro.redirect('/docs/introduction/');
|
||||||
|
---
|
||||||
3
site/decodal-site/src/pages/index.astro
Normal file
3
site/decodal-site/src/pages/index.astro
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
---
|
||||||
|
return Astro.redirect('/docs/introduction/');
|
||||||
|
---
|
||||||
44
site/decodal-site/src/pages/playground.astro
Normal file
44
site/decodal-site/src/pages/playground.astro
Normal 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>
|
||||||
193
site/decodal-site/src/scripts/playground.js
Normal file
193
site/decodal-site/src/scripts/playground.js
Normal 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();
|
||||||
|
});
|
||||||
467
site/decodal-site/src/style.css
Normal file
467
site/decodal-site/src/style.css
Normal 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
1
site/decodal-site/src/wasm/.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
# wasm-pack output is committed for the playground build.
|
||||||
40
site/decodal-site/src/wasm/decodal_wasm.d.ts
vendored
Normal file
40
site/decodal-site/src/wasm/decodal_wasm.d.ts
vendored
Normal 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>;
|
||||||
238
site/decodal-site/src/wasm/decodal_wasm.js
Normal file
238
site/decodal-site/src/wasm/decodal_wasm.js
Normal 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 };
|
||||||
BIN
site/decodal-site/src/wasm/decodal_wasm_bg.wasm
Normal file
BIN
site/decodal-site/src/wasm/decodal_wasm_bg.wasm
Normal file
Binary file not shown.
9
site/decodal-site/src/wasm/decodal_wasm_bg.wasm.d.ts
vendored
Normal file
9
site/decodal-site/src/wasm/decodal_wasm_bg.wasm.d.ts
vendored
Normal 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;
|
||||||
15
site/decodal-site/src/wasm/package.json
Normal file
15
site/decodal-site/src/wasm/package.json
Normal 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/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user