Compare commits

...

10 Commits

76 changed files with 12181 additions and 28 deletions

54
Cargo.lock generated
View File

@ -2,6 +2,60 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 version = 4
[[package]]
name = "aho-corasick"
version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "decodal" name = "decodal"
version = "0.1.0" version = "0.1.0"
dependencies = [
"decodal-core",
]
[[package]]
name = "decodal-core"
version = "0.1.0"
dependencies = [
"regex",
]
[[package]]
name = "memchr"
version = "2.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
[[package]]
name = "regex"
version = "1.12.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba"
dependencies = [
"aho-corasick",
"memchr",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "regex-automata"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
dependencies = [
"aho-corasick",
"memchr",
"regex-syntax",
]
[[package]]
name = "regex-syntax"
version = "0.8.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"

View File

@ -1,6 +1,13 @@
[package] [workspace]
name = "decodal" members = [
version = "0.1.0" "crates/decodal-core",
edition = "2024" "crates/decodal-cli",
]
resolver = "3"
[dependencies] [profile.release]
opt-level = "z"
lto = true
codegen-units = 1
strip = true
panic = "abort"

View File

@ -0,0 +1,11 @@
[package]
name = "decodal"
version = "0.1.0"
edition = "2024"
[features]
default = []
regex = ["decodal-core/regex"]
[dependencies]
decodal-core = { path = "../decodal-core" }

View File

@ -0,0 +1,215 @@
use std::{
env, fs,
path::{Path, PathBuf},
process::ExitCode,
};
use decodal_core::{Data, Diagnostic, DiagnosticKind, Engine, LoadedSource, SourceLoader, Span};
fn main() -> ExitCode {
match run() {
Ok(()) => ExitCode::SUCCESS,
Err(error) => {
print_diagnostic(&error);
ExitCode::FAILURE
}
}
}
fn run() -> Result<(), Diagnostic> {
let mut args = env::args().skip(1);
let first = args.next().unwrap_or_else(|| String::from("--help"));
match first.as_str() {
"--help" | "-h" => {
print_help();
Ok(())
}
"--version" | "-V" => {
println!("Decodal {}", decodal_core::version());
Ok(())
}
"check" => {
let path = args.next().unwrap_or_else(|| String::from("-"));
let data = materialize_path(&path)?;
drop(data);
println!("ok");
Ok(())
}
"eval" | "materialize" => {
let path = args.next().unwrap_or_else(|| String::from("-"));
let data = materialize_path(&path)?;
print_data(&data, 0);
println!();
Ok(())
}
path => {
let data = materialize_path(path)?;
print_data(&data, 0);
println!();
Ok(())
}
}
}
fn materialize_path(path: &str) -> Result<Data, Diagnostic> {
let root = read_root_source(path)?;
let mut engine = Engine::new(FsLoader);
let module = engine.add_root_source(root.key, root.name, &root.source)?;
let value = engine.eval_module(module)?;
engine.materialize(&value)
}
fn read_root_source(path: &str) -> Result<LoadedSource, Diagnostic> {
if path == "-" {
use std::io::Read;
let mut source = String::new();
std::io::stdin()
.read_to_string(&mut source)
.map_err(|error| {
Diagnostic::new(
DiagnosticKind::Import,
Span::default(),
format!("failed to read stdin: {error}"),
)
})?;
Ok(LoadedSource {
key: String::from("<stdin>"),
name: String::from("<stdin>"),
source,
})
} else {
load_path(Path::new(path))
}
}
#[derive(Debug, Clone, Copy)]
struct FsLoader;
impl SourceLoader for FsLoader {
fn load(
&mut self,
current_key: Option<&str>,
specifier: &str,
) -> Result<LoadedSource, Diagnostic> {
let path = Path::new(specifier);
let path = if path.is_absolute() {
PathBuf::from(path)
} else if let Some(current_key) = current_key.filter(|key| *key != "<stdin>") {
Path::new(current_key)
.parent()
.unwrap_or_else(|| Path::new("."))
.join(path)
} else {
PathBuf::from(path)
};
load_path(&path)
}
}
fn load_path(path: &Path) -> Result<LoadedSource, Diagnostic> {
let canonical = path.canonicalize().map_err(|error| {
Diagnostic::new(
DiagnosticKind::Import,
Span::default(),
format!("failed to resolve `{}`: {error}", path.display()),
)
})?;
let source = fs::read_to_string(&canonical).map_err(|error| {
Diagnostic::new(
DiagnosticKind::Import,
Span::default(),
format!("failed to read `{}`: {error}", canonical.display()),
)
})?;
let key = canonical.to_string_lossy().into_owned();
Ok(LoadedSource {
name: key.clone(),
key,
source,
})
}
fn print_help() {
println!("Decodal - Deferred Constraint Data Language");
println!();
println!("Usage:");
println!(" decodal <file.dcdl> Materialize a DCDL file");
println!(" decodal eval <file.dcdl> Materialize a DCDL file");
println!(" decodal check <file.dcdl> Evaluate and materialize without printing data");
println!(" decodal - Read DCDL source from stdin");
}
fn print_diagnostic(error: &Diagnostic) {
eprintln!(
"error[{kind:?}] {source}:{start}..{end}: {message}",
kind = error.kind,
source = error.span.source.0,
start = error.span.start,
end = error.span.end,
message = error.message,
);
}
fn print_data(data: &Data, indent: usize) {
match data {
Data::String(value) => print_string(value),
Data::Int(value) => print!("{value}"),
Data::Float(value) => print!("{value}"),
Data::Bool(value) => print!("{value}"),
Data::Array(items) => {
print!("[");
if !items.is_empty() {
println!();
for (index, item) in items.iter().enumerate() {
print_indent(indent + 2);
print_data(item, indent + 2);
if index + 1 != items.len() {
print!(",");
}
println!();
}
print_indent(indent);
}
print!("]");
}
Data::Object(fields) => {
print!("{{");
if !fields.is_empty() {
println!();
for (index, field) in fields.iter().enumerate() {
print_indent(indent + 2);
print_string(&field.name);
print!(": ");
print_data(&field.value, indent + 2);
if index + 1 != fields.len() {
print!(",");
}
println!();
}
print_indent(indent);
}
print!("}}");
}
}
}
fn print_indent(indent: usize) {
for _ in 0..indent {
print!(" ");
}
}
fn print_string(value: &str) {
print!("\"");
for ch in value.chars() {
match ch {
'"' => print!("\\\""),
'\\' => print!("\\\\"),
'\n' => print!("\\n"),
'\r' => print!("\\r"),
'\t' => print!("\\t"),
ch => print!("{ch}"),
}
}
print!("\"");
}

View File

@ -0,0 +1,12 @@
[package]
name = "decodal-core"
version = "0.1.0"
edition = "2024"
[features]
default = ["std"]
std = []
regex = ["std", "dep:regex"]
[dependencies]
regex = { version = "1.10", default-features = false, features = ["std", "unicode-perl"], optional = true }

View File

@ -0,0 +1,38 @@
use decodal_core::{Data, EmptyLoader, Engine, HostValue};
fn main() -> decodal_core::Result<()> {
let mut engine = Engine::new(EmptyLoader);
engine.bind_global(
"Service",
HostValue::object([
("name", HostValue::string_type()),
("port", HostValue::int_type().gt(443).default_int(8443)?),
("enabled", HostValue::bool_type().default_bool(true)?),
]),
)?;
let module = engine.add_root_source(
"embedded-main",
"embedded-main",
r#"
Service & {
name = "api";
port = 9443;
}
"#,
)?;
let value = engine.eval_module(module)?;
let data = engine.materialize(&value)?;
if let Data::Object(fields) = data {
assert_eq!(fields[0].value, Data::String(String::from("api")));
assert_eq!(fields[1].value, Data::Int(9443));
assert_eq!(fields[2].value, Data::Bool(true));
} else {
panic!("expected object");
}
Ok(())
}

View File

@ -0,0 +1,133 @@
use alloc::{string::String, vec::Vec};
use crate::span::Span;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ExprId(pub u32);
#[derive(Debug, Default, Clone)]
pub struct Ast {
exprs: Vec<SpannedExpr>,
}
impl Ast {
pub fn new() -> Self {
Self { exprs: Vec::new() }
}
pub fn push(&mut self, expr: Expr, span: Span) -> ExprId {
let id = ExprId(self.exprs.len() as u32);
self.exprs.push(SpannedExpr { expr, span });
id
}
pub fn get(&self, id: ExprId) -> &SpannedExpr {
&self.exprs[id.0 as usize]
}
pub fn span(&self, id: ExprId) -> Span {
self.get(id).span
}
pub fn len(&self) -> usize {
self.exprs.len()
}
pub fn is_empty(&self) -> bool {
self.exprs.is_empty()
}
}
#[derive(Debug, Clone)]
pub struct SpannedExpr {
pub expr: Expr,
pub span: Span,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Expr {
Literal(Literal),
Ident(String),
Object(Vec<Field>),
Array(Vec<ExprId>),
Let {
bindings: Vec<Field>,
body: ExprId,
},
Import(String),
Path {
base: ExprId,
field: String,
},
Call {
callee: ExprId,
args: Vec<ExprId>,
},
Function {
params: Vec<Param>,
body: ExprId,
},
Match {
scrutinee: ExprId,
arms: Vec<MatchArm>,
},
Binary {
op: BinaryOp,
lhs: ExprId,
rhs: ExprId,
},
Default {
base: ExprId,
fallback: ExprId,
},
CompareConstraint {
op: CompareOp,
value: ExprId,
},
RegexConstraint(String),
Wildcard,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Field {
pub path: Vec<String>,
pub value: ExprId,
pub span: Span,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Param {
pub name: String,
pub constraint: Option<ExprId>,
pub span: Span,
}
#[derive(Debug, Clone, PartialEq)]
pub struct MatchArm {
pub pattern: ExprId,
pub body: ExprId,
pub span: Span,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Literal {
String(String),
Int(i64),
Float(f64),
Bool(bool),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BinaryOp {
And,
Patch,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompareOp {
Gt,
Gte,
Lt,
Lte,
Eq,
}

View File

@ -0,0 +1,284 @@
use alloc::{string::String, vec::Vec};
use crate::{
Diagnostic, DiagnosticKind, Span,
ast::CompareOp,
runtime::{Constraint, LiteralValue, PrimitiveType},
};
pub fn normalize_constraints(
constraints: Vec<Constraint>,
span: Span,
) -> crate::Result<Vec<Constraint>> {
let mut primitive = None;
let mut lower: Option<Bound> = None;
let mut upper: Option<Bound> = None;
let mut rest = Vec::new();
for constraint in constraints {
match constraint {
Constraint::Type(next) => match primitive {
Some(current) if current != next => {
return Err(Diagnostic::new(
DiagnosticKind::Conflict,
span,
"primitive type constraints conflict",
));
}
Some(_) => {}
None => primitive = Some(next),
},
Constraint::Compare(op, value) => {
let number = Number::from_literal(&value).ok_or_else(|| {
Diagnostic::new(
DiagnosticKind::TypeMismatch,
span,
"comparison constraints require numeric literals",
)
})?;
match op {
CompareOp::Gt => merge_lower(&mut lower, Bound::new(number, false)),
CompareOp::Gte => merge_lower(&mut lower, Bound::new(number, true)),
CompareOp::Lt => merge_upper(&mut upper, Bound::new(number, false)),
CompareOp::Lte => merge_upper(&mut upper, Bound::new(number, true)),
CompareOp::Eq => {
merge_lower(&mut lower, Bound::new(number, true));
merge_upper(&mut upper, Bound::new(number, true));
}
}
}
Constraint::Regex(pattern) => rest.push(Constraint::Regex(pattern)),
Constraint::BuiltinPredicate(name) => rest.push(Constraint::BuiltinPredicate(name)),
}
}
if matches!(primitive, Some(PrimitiveType::String | PrimitiveType::Bool))
&& (lower.is_some() || upper.is_some())
{
return Err(Diagnostic::new(
DiagnosticKind::Conflict,
span,
"numeric comparison constraints conflict with non-numeric primitive type",
));
}
if primitive == Some(PrimitiveType::Int)
&& lower
.iter()
.chain(upper.iter())
.any(|bound| !matches!(bound.number, Number::Int(_)))
{
return Err(Diagnostic::new(
DiagnosticKind::Conflict,
span,
"Int comparison constraints must use integer literals",
));
}
ensure_bounds_non_empty(primitive, lower, upper, span)?;
let mut normalized = Vec::new();
if let Some(primitive) = primitive {
normalized.push(Constraint::Type(primitive));
}
if let Some(lower) = lower {
normalized.push(Constraint::Compare(
if lower.inclusive {
CompareOp::Gte
} else {
CompareOp::Gt
},
lower.number.into_literal(),
));
}
if let Some(upper) = upper {
normalized.push(Constraint::Compare(
if upper.inclusive {
CompareOp::Lte
} else {
CompareOp::Lt
},
upper.number.into_literal(),
));
}
normalized.extend(rest);
Ok(normalized)
}
#[derive(Debug, Clone, Copy, PartialEq)]
struct Bound {
number: Number,
inclusive: bool,
}
impl Bound {
fn new(number: Number, inclusive: bool) -> Self {
Self { number, inclusive }
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
enum Number {
Int(i64),
Float(f64),
}
impl Number {
fn from_literal(value: &LiteralValue) -> Option<Self> {
match value {
LiteralValue::Int(value) => Some(Self::Int(*value)),
LiteralValue::Float(value) => Some(Self::Float(*value)),
LiteralValue::String(_) | LiteralValue::Bool(_) => None,
}
}
fn into_literal(self) -> LiteralValue {
match self {
Self::Int(value) => LiteralValue::Int(value),
Self::Float(value) => LiteralValue::Float(value),
}
}
fn as_f64(self) -> f64 {
match self {
Self::Int(value) => value as f64,
Self::Float(value) => value,
}
}
}
fn merge_lower(current: &mut Option<Bound>, next: Bound) {
match current {
None => *current = Some(next),
Some(current_bound) if is_stricter_lower(next, *current_bound) => *current_bound = next,
Some(_) => {}
}
}
fn merge_upper(current: &mut Option<Bound>, next: Bound) {
match current {
None => *current = Some(next),
Some(current_bound) if is_stricter_upper(next, *current_bound) => *current_bound = next,
Some(_) => {}
}
}
fn is_stricter_lower(next: Bound, current: Bound) -> bool {
let next_value = next.number.as_f64();
let current_value = current.number.as_f64();
next_value > current_value
|| (next_value == current_value && !next.inclusive && current.inclusive)
}
fn is_stricter_upper(next: Bound, current: Bound) -> bool {
let next_value = next.number.as_f64();
let current_value = current.number.as_f64();
next_value < current_value
|| (next_value == current_value && !next.inclusive && current.inclusive)
}
fn ensure_bounds_non_empty(
primitive: Option<PrimitiveType>,
lower: Option<Bound>,
upper: Option<Bound>,
span: Span,
) -> crate::Result<()> {
if primitive == Some(PrimitiveType::Int) {
let min = lower.map(int_lower_bound).unwrap_or(i128::from(i64::MIN));
let max = upper.map(int_upper_bound).unwrap_or(i128::from(i64::MAX));
if min > max {
return Err(empty_numeric_bounds(span));
}
return Ok(());
}
if let (Some(lower), Some(upper)) = (lower, upper) {
let lower_value = lower.number.as_f64();
let upper_value = upper.number.as_f64();
if lower_value > upper_value {
return Err(empty_numeric_bounds(span));
}
if lower_value == upper_value && !(lower.inclusive && upper.inclusive) {
return Err(empty_numeric_bounds(span));
}
}
Ok(())
}
fn int_lower_bound(bound: Bound) -> i128 {
let Number::Int(value) = bound.number else {
unreachable!()
};
if bound.inclusive {
i128::from(value)
} else {
i128::from(value) + 1
}
}
fn int_upper_bound(bound: Bound) -> i128 {
let Number::Int(value) = bound.number else {
unreachable!()
};
if bound.inclusive {
i128::from(value)
} else {
i128::from(value) - 1
}
}
fn empty_numeric_bounds(span: Span) -> Diagnostic {
Diagnostic::new(
DiagnosticKind::Conflict,
span,
String::from("numeric comparison constraints have an empty intersection"),
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::runtime::LiteralValue;
#[test]
fn detects_primitive_conflict() {
assert!(
normalize_constraints(
alloc::vec![
Constraint::Type(PrimitiveType::Int),
Constraint::Type(PrimitiveType::String),
],
Span::default(),
)
.is_err()
);
}
#[test]
fn detects_empty_int_range() {
assert!(
normalize_constraints(
alloc::vec![
Constraint::Type(PrimitiveType::Int),
Constraint::Compare(CompareOp::Gt, LiteralValue::Int(10)),
Constraint::Compare(CompareOp::Lt, LiteralValue::Int(11)),
],
Span::default(),
)
.is_err()
);
}
#[test]
fn keeps_regex_constraints_without_intersection_check() {
let constraints = normalize_constraints(
alloc::vec![
Constraint::Regex(String::from("^a$")),
Constraint::Regex(String::from("^b$")),
],
Span::default(),
)
.unwrap();
assert_eq!(constraints.len(), 2);
}
}

View File

@ -0,0 +1,41 @@
use alloc::string::String;
use crate::span::Span;
pub type Result<T> = core::result::Result<T, Diagnostic>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Diagnostic {
pub kind: DiagnosticKind,
pub span: Span,
pub message: String,
}
impl Diagnostic {
pub fn new(kind: DiagnosticKind, span: Span, message: impl Into<String>) -> Self {
Self {
kind,
span,
message: message.into(),
}
}
pub fn syntax(span: Span, message: impl Into<String>) -> Self {
Self::new(DiagnosticKind::Syntax, span, message)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiagnosticKind {
Syntax,
UnresolvedIdentifier,
TypeMismatch,
ConstraintViolation,
Conflict,
DefaultConflict,
Cycle,
Import,
MatchFailure,
Materialize,
UnsupportedFeature,
}

View File

@ -0,0 +1,164 @@
use alloc::{boxed::Box, string::String, vec::Vec};
use crate::runtime::{Constraint, LiteralValue, PrimitiveType};
use crate::{CompareOp, Diagnostic, DiagnosticKind, Result, Span};
#[derive(Debug, Clone, PartialEq)]
pub enum HostValue {
String(String),
Int(i64),
Float(f64),
Bool(bool),
Array(Vec<HostValue>),
Object(Vec<HostField>),
Abstract {
constraints: Vec<Constraint>,
default: Option<Box<HostValue>>,
},
}
#[derive(Debug, Clone, PartialEq)]
pub struct HostField {
pub name: String,
pub value: HostValue,
}
impl HostValue {
pub fn string(value: impl Into<String>) -> Self {
Self::String(value.into())
}
pub fn int(value: i64) -> Self {
Self::Int(value)
}
pub fn float(value: f64) -> Self {
Self::Float(value)
}
pub fn bool(value: bool) -> Self {
Self::Bool(value)
}
pub fn array<I>(items: I) -> Self
where
I: IntoIterator<Item = HostValue>,
{
Self::Array(items.into_iter().collect())
}
pub fn object<I, N>(fields: I) -> Self
where
I: IntoIterator<Item = (N, HostValue)>,
N: Into<String>,
{
Self::Object(
fields
.into_iter()
.map(|(name, value)| HostField {
name: name.into(),
value,
})
.collect(),
)
}
pub fn string_type() -> Self {
Self::abstract_with_constraint(Constraint::Type(PrimitiveType::String))
}
pub fn int_type() -> Self {
Self::abstract_with_constraint(Constraint::Type(PrimitiveType::Int))
}
pub fn float_type() -> Self {
Self::abstract_with_constraint(Constraint::Type(PrimitiveType::Float))
}
pub fn bool_type() -> Self {
Self::abstract_with_constraint(Constraint::Type(PrimitiveType::Bool))
}
pub fn builtin_predicate(name: impl Into<String>) -> Self {
Self::abstract_with_constraint(Constraint::BuiltinPredicate(name.into()))
}
pub fn abstract_with_constraint(constraint: Constraint) -> Self {
Self::Abstract {
constraints: alloc::vec![constraint],
default: None,
}
}
pub fn with_constraint(mut self, constraint: Constraint) -> Self {
match &mut self {
Self::Abstract { constraints, .. } => constraints.push(constraint),
_ => {
self = Self::Abstract {
constraints: alloc::vec![constraint],
default: Some(Box::new(self)),
};
}
}
self
}
pub fn gt(self, value: i64) -> Self {
self.with_constraint(Constraint::Compare(CompareOp::Gt, LiteralValue::Int(value)))
}
pub fn gte(self, value: i64) -> Self {
self.with_constraint(Constraint::Compare(
CompareOp::Gte,
LiteralValue::Int(value),
))
}
pub fn lt(self, value: i64) -> Self {
self.with_constraint(Constraint::Compare(CompareOp::Lt, LiteralValue::Int(value)))
}
pub fn lte(self, value: i64) -> Self {
self.with_constraint(Constraint::Compare(
CompareOp::Lte,
LiteralValue::Int(value),
))
}
pub fn default(self, value: HostValue) -> Result<Self> {
match self {
Self::Abstract {
constraints,
default: None,
} => Ok(Self::Abstract {
constraints,
default: Some(Box::new(value)),
}),
Self::Abstract { .. } => Err(Diagnostic::new(
DiagnosticKind::DefaultConflict,
Span::default(),
"host value already has a default",
)),
concrete => Ok(Self::Abstract {
constraints: Vec::new(),
default: Some(Box::new(concrete)),
}),
}
}
pub fn default_string(self, value: impl Into<String>) -> Result<Self> {
self.default(Self::string(value))
}
pub fn default_int(self, value: i64) -> Result<Self> {
self.default(Self::int(value))
}
pub fn default_float(self, value: f64) -> Result<Self> {
self.default(Self::float(value))
}
pub fn default_bool(self, value: bool) -> Result<Self> {
self.default(Self::bool(value))
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,356 @@
use alloc::{string::String, vec::Vec};
use crate::{Diagnostic, SourceId, Span, diagnostic::Result};
#[derive(Debug, Clone, PartialEq)]
pub struct Token {
pub kind: TokenKind,
pub span: Span,
}
#[derive(Debug, Clone, PartialEq)]
pub enum TokenKind {
Ident(String),
Int(i64),
Float(f64),
String(String),
Regex(String),
True,
False,
Let,
In,
Match,
Import,
Default,
Underscore,
LBrace,
RBrace,
LBracket,
RBracket,
LParen,
RParen,
Semicolon,
Comma,
Dot,
Colon,
Equal,
Arrow,
Amp,
SlashSlash,
Gt,
Gte,
Lt,
Lte,
Eof,
}
pub struct Lexer<'a> {
source_id: SourceId,
source: &'a str,
bytes: &'a [u8],
pos: usize,
}
impl<'a> Lexer<'a> {
pub fn new(source: &'a str) -> Self {
Self::with_source_id(SourceId(0), source)
}
pub fn with_source_id(source_id: SourceId, source: &'a str) -> Self {
Self {
source_id,
source,
bytes: source.as_bytes(),
pos: 0,
}
}
pub fn tokenize(mut self) -> Result<Vec<Token>> {
let mut tokens = Vec::new();
loop {
let token = self.next_token()?;
let is_eof = token.kind == TokenKind::Eof;
tokens.push(token);
if is_eof {
return Ok(tokens);
}
}
}
fn next_token(&mut self) -> Result<Token> {
self.skip_ws_and_comments();
let start = self.pos;
let Some(ch) = self.peek() else {
return Ok(Token {
kind: TokenKind::Eof,
span: self.empty_span(self.pos),
});
};
let kind = match ch {
b'{' => {
self.pos += 1;
TokenKind::LBrace
}
b'}' => {
self.pos += 1;
TokenKind::RBrace
}
b'[' => {
self.pos += 1;
TokenKind::LBracket
}
b']' => {
self.pos += 1;
TokenKind::RBracket
}
b'(' => {
self.pos += 1;
TokenKind::LParen
}
b')' => {
self.pos += 1;
TokenKind::RParen
}
b';' => {
self.pos += 1;
TokenKind::Semicolon
}
b',' => {
self.pos += 1;
TokenKind::Comma
}
b'.' => {
self.pos += 1;
TokenKind::Dot
}
b':' => {
self.pos += 1;
TokenKind::Colon
}
b'_' => {
self.pos += 1;
TokenKind::Underscore
}
b'&' => {
self.pos += 1;
TokenKind::Amp
}
b'=' => {
self.pos += 1;
if self.consume(b'>') {
TokenKind::Arrow
} else {
TokenKind::Equal
}
}
b'>' => {
self.pos += 1;
if self.consume(b'=') {
TokenKind::Gte
} else {
TokenKind::Gt
}
}
b'<' => {
self.pos += 1;
if self.consume(b'=') {
TokenKind::Lte
} else {
TokenKind::Lt
}
}
b'/' => {
self.pos += 1;
if self.consume(b'/') {
TokenKind::SlashSlash
} else {
self.lex_regex(start)?
}
}
b'"' => self.lex_string()?,
b'0'..=b'9' => self.lex_number()?,
c if is_ident_start(c) => self.lex_ident_or_keyword(),
_ => {
return Err(Diagnostic::syntax(
self.span(start, start + 1),
"unexpected character",
));
}
};
Ok(Token {
kind,
span: self.span(start, self.pos),
})
}
fn skip_ws_and_comments(&mut self) {
loop {
while matches!(self.peek(), Some(b' ' | b'\t' | b'\r' | b'\n')) {
self.pos += 1;
}
if self.peek() == Some(b'#') {
while let Some(c) = self.peek() {
self.pos += 1;
if c == b'\n' {
break;
}
}
continue;
}
break;
}
}
fn lex_string(&mut self) -> Result<TokenKind> {
let start = self.pos;
self.pos += 1;
let mut value = String::new();
while let Some(c) = self.peek() {
self.pos += 1;
match c {
b'"' => return Ok(TokenKind::String(value)),
b'\\' => {
let Some(escaped) = self.peek() else {
return Err(Diagnostic::syntax(
self.span(start, self.pos),
"unterminated escape",
));
};
self.pos += 1;
let ch = match escaped {
b'"' => '"',
b'\\' => '\\',
b'n' => '\n',
b'r' => '\r',
b't' => '\t',
other => other as char,
};
value.push(ch);
}
other => value.push(other as char),
}
}
Err(Diagnostic::syntax(
self.span(start, self.pos),
"unterminated string",
))
}
fn lex_regex(&mut self, start: usize) -> Result<TokenKind> {
let mut pattern = String::new();
let mut escaped = false;
while let Some(c) = self.peek() {
self.pos += 1;
if escaped {
pattern.push(c as char);
escaped = false;
continue;
}
match c {
b'\\' => {
pattern.push('\\');
escaped = true;
}
b'/' => return Ok(TokenKind::Regex(pattern)),
other => pattern.push(other as char),
}
}
Err(Diagnostic::syntax(
self.span(start, self.pos),
"unterminated regex",
))
}
fn lex_number(&mut self) -> Result<TokenKind> {
let start = self.pos;
while matches!(self.peek(), Some(b'0'..=b'9')) {
self.pos += 1;
}
let mut is_float = false;
if self.peek() == Some(b'.') && matches!(self.peek_n(1), Some(b'0'..=b'9')) {
is_float = true;
self.pos += 1;
while matches!(self.peek(), Some(b'0'..=b'9')) {
self.pos += 1;
}
}
let text = &self.source[start..self.pos];
if is_float {
text.parse::<f64>().map(TokenKind::Float).map_err(|_| {
Diagnostic::syntax(self.span(start, self.pos), "invalid float literal")
})
} else {
text.parse::<i64>()
.map(TokenKind::Int)
.map_err(|_| Diagnostic::syntax(self.span(start, self.pos), "invalid int literal"))
}
}
fn lex_ident_or_keyword(&mut self) -> TokenKind {
let start = self.pos;
self.pos += 1;
while matches!(self.peek(), Some(c) if is_ident_continue(c)) {
self.pos += 1;
}
let text = &self.source[start..self.pos];
match text {
"true" => TokenKind::True,
"false" => TokenKind::False,
"let" => TokenKind::Let,
"in" => TokenKind::In,
"match" => TokenKind::Match,
"import" => TokenKind::Import,
"default" => TokenKind::Default,
_ => TokenKind::Ident(String::from(text)),
}
}
fn span(&self, start: usize, end: usize) -> Span {
Span::new(self.source_id, start, end)
}
fn empty_span(&self, offset: usize) -> Span {
Span::empty(self.source_id, offset)
}
fn peek(&self) -> Option<u8> {
self.bytes.get(self.pos).copied()
}
fn peek_n(&self, n: usize) -> Option<u8> {
self.bytes.get(self.pos + n).copied()
}
fn consume(&mut self, expected: u8) -> bool {
if self.peek() == Some(expected) {
self.pos += 1;
true
} else {
false
}
}
}
fn is_ident_start(c: u8) -> bool {
c.is_ascii_alphabetic()
}
fn is_ident_continue(c: u8) -> bool {
c.is_ascii_alphanumeric() || c == b'_'
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn tokenizes_basic_source() {
let tokens = Lexer::new("port = Int & >= 1;").tokenize().unwrap();
assert!(matches!(tokens[0].kind, TokenKind::Ident(_)));
assert_eq!(tokens[1].kind, TokenKind::Equal);
assert_eq!(tokens[3].kind, TokenKind::Amp);
assert_eq!(tokens[4].kind, TokenKind::Gte);
}
}

View File

@ -0,0 +1,29 @@
#![cfg_attr(not(feature = "std"), no_std)]
extern crate alloc;
pub mod ast;
pub mod constraints;
pub mod diagnostic;
pub mod embedding;
pub mod eval;
pub mod lexer;
pub mod module;
pub mod parser;
pub mod runtime;
pub mod span;
pub use ast::{Ast, BinaryOp, CompareOp, Expr, ExprId, Field, Literal, Param};
pub use constraints::normalize_constraints;
pub use diagnostic::{Diagnostic, DiagnosticKind, Result};
pub use embedding::{HostField, HostValue};
pub use eval::Engine;
pub use lexer::{Lexer, Token, TokenKind};
pub use module::{EmptyLoader, LoadedSource, Module, SourceLoader};
pub use parser::{ParseOutput, Parser, SourceForm, parse_source, parse_source_with_source_id};
pub use runtime::{Constraint, Data, ExprRef, LiteralValue, ModuleId, PrimitiveType, RuntimeValue};
pub use span::{SourceId, Span};
pub fn version() -> &'static str {
env!("CARGO_PKG_VERSION")
}

View File

@ -0,0 +1,42 @@
use alloc::string::String;
use crate::{
Ast, ExprId, SourceForm, SourceId,
runtime::{EnvId, ThunkId},
};
#[derive(Debug, Clone)]
pub struct Module {
pub key: String,
pub name: String,
pub source: SourceId,
pub ast: Ast,
pub root: ExprId,
pub source_form: SourceForm,
pub root_env: EnvId,
pub root_thunk: ThunkId,
}
#[derive(Debug, Clone)]
pub struct LoadedSource {
pub key: String,
pub name: String,
pub source: String,
}
pub trait SourceLoader {
fn load(&mut self, current_key: Option<&str>, specifier: &str) -> crate::Result<LoadedSource>;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct EmptyLoader;
impl SourceLoader for EmptyLoader {
fn load(&mut self, _current_key: Option<&str>, specifier: &str) -> crate::Result<LoadedSource> {
Err(crate::Diagnostic::new(
crate::DiagnosticKind::Import,
crate::Span::default(),
alloc::format!("no source loader is configured for import `{specifier}`"),
))
}
}

View File

@ -0,0 +1,525 @@
use alloc::{string::String, vec::Vec};
use crate::{
SourceId, Span,
ast::{Ast, BinaryOp, CompareOp, Expr, ExprId, Field, Literal, MatchArm, Param},
diagnostic::{Diagnostic, Result},
lexer::{Lexer, Token, TokenKind},
};
#[derive(Debug, Clone)]
pub struct ParseOutput {
pub ast: Ast,
pub root: ExprId,
pub source_form: SourceForm,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SourceForm {
Expr,
Fields,
}
pub fn parse_source(source: &str) -> Result<ParseOutput> {
parse_source_with_source_id(SourceId(0), source)
}
pub fn parse_source_with_source_id(source_id: SourceId, source: &str) -> Result<ParseOutput> {
let tokens = Lexer::with_source_id(source_id, source).tokenize()?;
Parser::new(tokens).parse()
}
pub struct Parser {
tokens: Vec<Token>,
pos: usize,
ast: Ast,
}
impl Parser {
pub fn new(tokens: Vec<Token>) -> Self {
Self {
tokens,
pos: 0,
ast: Ast::new(),
}
}
pub fn parse(mut self) -> Result<ParseOutput> {
let (root, source_form) = if self.starts_field() {
let fields = self.parse_fields_until_eof()?;
let span = fields
.first()
.map(|f| {
fields
.iter()
.fold(f.span, |acc, field| acc.join(field.span))
})
.unwrap_or_else(|| self.peek().span);
(
self.ast.push(Expr::Object(fields), span),
SourceForm::Fields,
)
} else {
let expr = self.parse_expr(0)?;
self.expect_eof()?;
(expr, SourceForm::Expr)
};
Ok(ParseOutput {
ast: self.ast,
root,
source_form,
})
}
fn parse_expr(&mut self, min_bp: u8) -> Result<ExprId> {
let mut lhs = self.parse_prefix()?;
loop {
if self.at_eof() || self.is_expr_stop() {
break;
}
// postfix: path reference
if self.consume_kind(&TokenKind::Dot).is_some() {
let (field, field_span) = self.expect_ident()?;
let span = self.ast.span(lhs).join(field_span);
lhs = self.ast.push(Expr::Path { base: lhs, field }, span);
continue;
}
// postfix: call
if self.consume_kind(&TokenKind::LParen).is_some() {
let mut args = Vec::new();
if self.consume_kind(&TokenKind::RParen).is_none() {
loop {
args.push(self.parse_expr(0)?);
if self.consume_kind(&TokenKind::Comma).is_some() {
continue;
}
self.expect_kind(&TokenKind::RParen, "expected ')' after call arguments")?;
break;
}
}
let span = self.ast.span(lhs).join(self.previous_span());
lhs = self.ast.push(Expr::Call { callee: lhs, args }, span);
continue;
}
let Some((kind, l_bp, r_bp)) = self.peek_infix() else {
break;
};
if l_bp < min_bp {
break;
}
let op_span = self.advance().span;
let rhs = self.parse_expr(r_bp)?;
let span = self.ast.span(lhs).join(self.ast.span(rhs)).join(op_span);
lhs = match kind {
InfixKind::And => self.ast.push(
Expr::Binary {
op: BinaryOp::And,
lhs,
rhs,
},
span,
),
InfixKind::Patch => self.ast.push(
Expr::Binary {
op: BinaryOp::Patch,
lhs,
rhs,
},
span,
),
InfixKind::Default => self.ast.push(
Expr::Default {
base: lhs,
fallback: rhs,
},
span,
),
};
}
Ok(lhs)
}
fn parse_prefix(&mut self) -> Result<ExprId> {
let token = self.advance().clone();
match token.kind {
TokenKind::String(value) => Ok(self
.ast
.push(Expr::Literal(Literal::String(value)), token.span)),
TokenKind::Int(value) => Ok(self
.ast
.push(Expr::Literal(Literal::Int(value)), token.span)),
TokenKind::Float(value) => Ok(self
.ast
.push(Expr::Literal(Literal::Float(value)), token.span)),
TokenKind::True => Ok(self
.ast
.push(Expr::Literal(Literal::Bool(true)), token.span)),
TokenKind::False => Ok(self
.ast
.push(Expr::Literal(Literal::Bool(false)), token.span)),
TokenKind::Ident(name) => Ok(self.ast.push(Expr::Ident(name), token.span)),
TokenKind::Regex(pattern) => {
Ok(self.ast.push(Expr::RegexConstraint(pattern), token.span))
}
TokenKind::Underscore => Ok(self.ast.push(Expr::Wildcard, token.span)),
TokenKind::LBrace => self.parse_object_after_lbrace(token.span),
TokenKind::LBracket => self.parse_array_after_lbracket(token.span),
TokenKind::LParen => self.parse_group_or_function(token.span),
TokenKind::Let => self.parse_let(token.span),
TokenKind::Match => self.parse_match(token.span),
TokenKind::Import => self.parse_import(token.span),
TokenKind::Gt | TokenKind::Gte | TokenKind::Lt | TokenKind::Lte => {
let op = match token.kind {
TokenKind::Gt => CompareOp::Gt,
TokenKind::Gte => CompareOp::Gte,
TokenKind::Lt => CompareOp::Lt,
TokenKind::Lte => CompareOp::Lte,
_ => unreachable!(),
};
let value = self.parse_expr(8)?;
let span = token.span.join(self.ast.span(value));
Ok(self.ast.push(Expr::CompareConstraint { op, value }, span))
}
_ => Err(Diagnostic::syntax(token.span, "expected expression")),
}
}
fn parse_object_after_lbrace(&mut self, start_span: Span) -> Result<ExprId> {
let mut fields = Vec::new();
if self.consume_kind(&TokenKind::RBrace).is_some() {
return Ok(self
.ast
.push(Expr::Object(fields), start_span.join(self.previous_span())));
}
loop {
fields.push(self.parse_field()?);
if self.consume_kind(&TokenKind::Semicolon).is_some() {
if self.consume_kind(&TokenKind::RBrace).is_some() {
break;
}
continue;
}
self.expect_kind(&TokenKind::RBrace, "expected ';' or '}' after object field")?;
break;
}
let span = start_span.join(self.previous_span());
Ok(self.ast.push(Expr::Object(fields), span))
}
fn parse_array_after_lbracket(&mut self, start_span: Span) -> Result<ExprId> {
let mut items = Vec::new();
if self.consume_kind(&TokenKind::RBracket).is_some() {
return Ok(self
.ast
.push(Expr::Array(items), start_span.join(self.previous_span())));
}
loop {
items.push(self.parse_expr(0)?);
if self.consume_kind(&TokenKind::Comma).is_some() {
if self.consume_kind(&TokenKind::RBracket).is_some() {
break;
}
continue;
}
self.expect_kind(&TokenKind::RBracket, "expected ',' or ']' after array item")?;
break;
}
let span = start_span.join(self.previous_span());
Ok(self.ast.push(Expr::Array(items), span))
}
fn parse_group_or_function(&mut self, start_span: Span) -> Result<ExprId> {
if self.looks_like_params() {
let params = self.parse_params_after_lparen()?;
self.expect_kind(&TokenKind::Arrow, "expected '=>' after function parameters")?;
let body = self.parse_expr(0)?;
let span = start_span.join(self.ast.span(body));
return Ok(self.ast.push(Expr::Function { params, body }, span));
}
let expr = self.parse_expr(0)?;
self.expect_kind(&TokenKind::RParen, "expected ')' after expression")?;
Ok(expr)
}
fn parse_params_after_lparen(&mut self) -> Result<Vec<Param>> {
let mut params = Vec::new();
if self.consume_kind(&TokenKind::RParen).is_some() {
return Ok(params);
}
loop {
let (name, name_span) = self.expect_ident()?;
let constraint = if self.consume_kind(&TokenKind::Colon).is_some() {
Some(self.parse_expr(0)?)
} else {
None
};
let span = constraint
.map(|id| name_span.join(self.ast.span(id)))
.unwrap_or(name_span);
params.push(Param {
name,
constraint,
span,
});
if self.consume_kind(&TokenKind::Comma).is_some() {
continue;
}
self.expect_kind(&TokenKind::RParen, "expected ',' or ')' after parameter")?;
break;
}
Ok(params)
}
fn parse_let(&mut self, start_span: Span) -> Result<ExprId> {
let mut bindings = Vec::new();
while !self.check_kind(&TokenKind::In) && !self.at_eof() {
bindings.push(self.parse_field()?);
self.expect_kind(&TokenKind::Semicolon, "expected ';' after let binding")?;
}
self.expect_kind(&TokenKind::In, "expected 'in' after let bindings")?;
let body = self.parse_expr(0)?;
let span = start_span.join(self.ast.span(body));
Ok(self.ast.push(Expr::Let { bindings, body }, span))
}
fn parse_match(&mut self, start_span: Span) -> Result<ExprId> {
let scrutinee = self.parse_expr(0)?;
self.expect_kind(&TokenKind::LBrace, "expected '{' after match scrutinee")?;
let mut arms = Vec::new();
if self.consume_kind(&TokenKind::RBrace).is_none() {
loop {
let pattern = self.parse_expr(0)?;
self.expect_kind(&TokenKind::Colon, "expected ':' after match pattern")?;
let body = self.parse_expr(0)?;
let span = self.ast.span(pattern).join(self.ast.span(body));
arms.push(MatchArm {
pattern,
body,
span,
});
if self.consume_kind(&TokenKind::Semicolon).is_some() {
if self.consume_kind(&TokenKind::RBrace).is_some() {
break;
}
continue;
}
self.expect_kind(&TokenKind::RBrace, "expected ';' or '}' after match arm")?;
break;
}
}
let span = start_span.join(self.previous_span());
Ok(self.ast.push(Expr::Match { scrutinee, arms }, span))
}
fn parse_import(&mut self, start_span: Span) -> Result<ExprId> {
let token = self.advance().clone();
let path = match token.kind {
TokenKind::String(path) | TokenKind::Ident(path) => path,
_ => return Err(Diagnostic::syntax(token.span, "expected import path")),
};
Ok(self
.ast
.push(Expr::Import(path), start_span.join(token.span)))
}
fn parse_fields_until_eof(&mut self) -> Result<Vec<Field>> {
let mut fields = Vec::new();
while !self.at_eof() {
fields.push(self.parse_field()?);
self.consume_kind(&TokenKind::Semicolon);
}
Ok(fields)
}
fn parse_field(&mut self) -> Result<Field> {
let (first, first_span) = self.expect_ident()?;
let mut path = Vec::new();
path.push(first);
let mut span = first_span;
while self.consume_kind(&TokenKind::Dot).is_some() {
let (name, name_span) = self.expect_ident()?;
span = span.join(name_span);
path.push(name);
}
self.expect_kind(&TokenKind::Equal, "expected '=' after field name")?;
let value = self.parse_expr(0)?;
span = span.join(self.ast.span(value));
Ok(Field { path, value, span })
}
fn starts_field(&self) -> bool {
matches!(self.peek_kind(), TokenKind::Ident(_))
&& matches!(self.peek_kind_n(1), TokenKind::Equal | TokenKind::Dot)
}
fn looks_like_params(&self) -> bool {
match self.peek_kind() {
TokenKind::RParen => matches!(self.peek_kind_n(1), TokenKind::Arrow),
TokenKind::Ident(_) => {
let mut i = self.pos;
loop {
if !matches!(self.kind_at(i), TokenKind::Ident(_)) {
return false;
}
i += 1;
if matches!(self.kind_at(i), TokenKind::Colon) {
// Skip a simple constraint expression approximately until comma/rparen.
i += 1;
let mut depth = 0usize;
while !matches!(self.kind_at(i), TokenKind::Eof) {
match self.kind_at(i) {
TokenKind::LParen | TokenKind::LBrace | TokenKind::LBracket => {
depth += 1
}
TokenKind::RParen if depth == 0 => break,
TokenKind::RParen | TokenKind::RBrace | TokenKind::RBracket => {
depth = depth.saturating_sub(1)
}
TokenKind::Comma if depth == 0 => break,
_ => {}
}
i += 1;
}
}
if matches!(self.kind_at(i), TokenKind::Comma) {
i += 1;
continue;
}
if matches!(self.kind_at(i), TokenKind::RParen) {
return matches!(self.kind_at(i + 1), TokenKind::Arrow);
}
return false;
}
}
_ => false,
}
}
fn peek_infix(&self) -> Option<(InfixKind, u8, u8)> {
match self.peek_kind() {
TokenKind::Default => Some((InfixKind::Default, 1, 2)),
TokenKind::SlashSlash => Some((InfixKind::Patch, 3, 4)),
TokenKind::Amp => Some((InfixKind::And, 5, 6)),
_ => None,
}
}
fn is_expr_stop(&self) -> bool {
matches!(
self.peek_kind(),
TokenKind::Semicolon
| TokenKind::Comma
| TokenKind::RParen
| TokenKind::RBracket
| TokenKind::RBrace
| TokenKind::Colon
| TokenKind::In
| TokenKind::Arrow
)
}
fn expect_ident(&mut self) -> Result<(String, Span)> {
let token = self.advance().clone();
match token.kind {
TokenKind::Ident(name) => Ok((name, token.span)),
_ => Err(Diagnostic::syntax(token.span, "expected identifier")),
}
}
fn expect_kind(&mut self, expected: &TokenKind, message: &'static str) -> Result<Span> {
if let Some(span) = self.consume_kind(expected) {
Ok(span)
} else {
Err(Diagnostic::syntax(self.peek().span, message))
}
}
fn expect_eof(&mut self) -> Result<()> {
if self.at_eof() {
Ok(())
} else {
Err(Diagnostic::syntax(self.peek().span, "expected end of file"))
}
}
fn consume_kind(&mut self, expected: &TokenKind) -> Option<Span> {
if core::mem::discriminant(self.peek_kind()) == core::mem::discriminant(expected) {
Some(self.advance().span)
} else {
None
}
}
fn check_kind(&self, expected: &TokenKind) -> bool {
core::mem::discriminant(self.peek_kind()) == core::mem::discriminant(expected)
}
fn at_eof(&self) -> bool {
matches!(self.peek_kind(), TokenKind::Eof)
}
fn advance(&mut self) -> &Token {
let index = self.pos;
if !self.at_eof() {
self.pos += 1;
}
&self.tokens[index]
}
fn previous_span(&self) -> Span {
self.tokens[self.pos.saturating_sub(1)].span
}
fn peek(&self) -> &Token {
&self.tokens[self.pos]
}
fn peek_kind(&self) -> &TokenKind {
&self.peek().kind
}
fn peek_kind_n(&self, n: usize) -> &TokenKind {
self.kind_at(self.pos + n)
}
fn kind_at(&self, index: usize) -> &TokenKind {
self.tokens
.get(index)
.map(|token| &token.kind)
.unwrap_or(&TokenKind::Eof)
}
}
#[derive(Debug, Clone, Copy)]
enum InfixKind {
And,
Patch,
Default,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::Expr;
#[test]
fn parses_top_level_fields_as_object() {
let parsed = parse_source("port = Int & >= 1 default 8080;").unwrap();
assert!(matches!(parsed.ast.get(parsed.root).expr, Expr::Object(_)));
}
#[test]
fn parses_object_dot_field() {
let parsed = parse_source("{ feature.enable = false; }").unwrap();
let Expr::Object(fields) = &parsed.ast.get(parsed.root).expr else {
panic!()
};
assert_eq!(fields[0].path, ["feature", "enable"]);
}
}

View File

@ -0,0 +1,146 @@
use alloc::{string::String, vec::Vec};
use crate::{ExprId, ast::CompareOp};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ModuleId(pub u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ExprRef {
pub module: ModuleId,
pub expr: ExprId,
}
#[derive(Debug, Clone, PartialEq)]
pub enum RuntimeValue {
Concrete(ConcreteValue),
Abstract(AbstractValue),
}
#[derive(Debug, Clone, PartialEq)]
pub enum ConcreteValue {
String(String),
Int(i64),
Float(f64),
Bool(bool),
Array(Vec<ThunkId>),
Object(ObjectValue),
Function(FunctionValue),
}
#[derive(Debug, Clone, PartialEq)]
pub struct ObjectValue {
pub fields: Vec<ObjectField>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ObjectField {
pub name: String,
pub value: ThunkId,
}
#[derive(Debug, Clone, PartialEq)]
pub struct FunctionValue {
pub params: Vec<FunctionParam>,
pub body: ExprRef,
pub env: EnvId,
}
#[derive(Debug, Clone, PartialEq)]
pub struct FunctionParam {
pub name: String,
pub constraint: Option<ExprRef>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct AbstractValue {
pub constraints: Vec<Constraint>,
pub default: Option<ThunkId>,
}
#[derive(Debug, Clone, PartialEq)]
pub enum Constraint {
Type(PrimitiveType),
Compare(CompareOp, LiteralValue),
Regex(String),
BuiltinPredicate(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PrimitiveType {
String,
Int,
Float,
Bool,
}
#[derive(Debug, Clone, PartialEq)]
pub enum LiteralValue {
String(String),
Int(i64),
Float(f64),
Bool(bool),
}
#[derive(Debug, Clone, PartialEq)]
pub enum Data {
String(String),
Int(i64),
Float(f64),
Bool(bool),
Array(Vec<Data>),
Object(Vec<DataField>),
}
#[derive(Debug, Clone, PartialEq)]
pub struct DataField {
pub name: String,
pub value: Data,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ThunkId(pub u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct EnvId(pub u32);
#[derive(Debug, Clone)]
pub struct Thunk {
pub kind: ThunkKind,
pub state: ThunkState,
}
#[derive(Debug, Clone)]
pub enum ThunkKind {
Expr {
expr: ExprRef,
env: EnvId,
},
Constrained {
constraint: ExprRef,
constraint_env: EnvId,
value: ExprRef,
value_env: EnvId,
},
Value(RuntimeValue),
}
#[derive(Debug, Clone)]
pub enum ThunkState {
Unevaluated,
Evaluating,
Evaluated(RuntimeValue),
Error,
}
#[derive(Debug, Clone)]
pub struct Env {
pub parent: Option<EnvId>,
pub bindings: Vec<Binding>,
}
#[derive(Debug, Clone)]
pub struct Binding {
pub name: String,
pub value: ThunkId,
}

View File

@ -0,0 +1,31 @@
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub struct SourceId(pub u32);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Span {
pub source: SourceId,
pub start: u32,
pub end: u32,
}
impl Span {
pub const fn new(source: SourceId, start: usize, end: usize) -> Self {
Self {
source,
start: start as u32,
end: end as u32,
}
}
pub const fn empty(source: SourceId, offset: usize) -> Self {
Self::new(source, offset, offset)
}
pub fn join(self, other: Self) -> Self {
Self {
source: self.source,
start: self.start.min(other.start),
end: self.end.max(other.end),
}
}
}

View File

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

View File

@ -0,0 +1,97 @@
# Embedding API
Decodal core can be embedded without giving the core crate access to a filesystem.
The host supplies imported sources through `SourceLoader` and may also provide global bindings through the host prelude API.
## Host prelude
`Engine` owns a prelude environment.
Bindings in this environment are visible from every module loaded by the engine.
```text
prelude env
module root env
let / function env
```
Module top-level bindings shadow prelude bindings.
Primitive type names such as `String`, `Int`, `Float`, and `Bool` are handled before environment lookup, so they are reserved and cannot be shadowed by host bindings.
## Global bindings
The host can bind values before adding or evaluating user sources.
```rust
use decodal_core::{EmptyLoader, Engine, HostValue};
let mut engine = Engine::new(EmptyLoader);
engine.bind_global(
"Service",
HostValue::object([
("name", HostValue::string_type()),
("port", HostValue::int_type().gt(443).default_int(8443)?),
("enabled", HostValue::bool_type().default_bool(true)?),
]),
)?;
```
A user source can then refer to `Service` without importing it.
```dcdl
Service & {
name = "api";
port = 9443;
}
```
## HostValue
`HostValue` is the public builder-facing value representation for embedding.
It keeps host code from constructing internal `ThunkId` or `ObjectValue` values directly.
```text
HostValue =
String
Int
Float
Bool
Array(Vec<HostValue>)
Object(Vec<HostField>)
Abstract { constraints, default }
```
When a host value is bound, the engine internalizes it into `RuntimeValue` and allocates value thunks for object fields, array items, and defaults.
## Abstract host objects
A host-provided schema object is represented as a concrete object structure whose fields may contain abstract values.
```rust
HostValue::object([
("name", HostValue::string_type()),
("port", HostValue::int_type().gt(443).default_int(8443)?),
])
```
Conceptually this becomes:
```text
Concrete(Object {
name -> Thunk(Abstract { constraints: [String], default: none })
port -> Thunk(Abstract { constraints: [Int, > 443], default: 8443 })
})
```
This matches the runtime model used for Decodal source-defined schema objects.
## SourceLoader and prelude together
`SourceLoader` and host prelude bindings are independent mechanisms.
- Use `SourceLoader` when user sources should explicitly import host-provided modules.
- Use prelude bindings when host-provided schemas or constants should be globally available.
Both mechanisms share the same runtime evaluator, thunk model, and materialization rules.

View File

@ -54,23 +54,30 @@ desugar は、意味論を単純にするための表層構文変換を行う。
## module registry ## module registry
module registry は、読み込んだ module を canonical path で管理する。 module registry は、読み込んだ module を loader が返す安定 key で管理する。
CLI では canonical path を key とする。
組み込み利用では、resource name や static source table の key を使える。
```text ```text
ModuleRegistry: ModuleRegistry:
CanonicalPath -> ModuleId ModuleKey -> ModuleId
``` ```
処理系は、まず root module を parse / desugar して registry に登録する。 処理系は、まず root module を parse / desugar して registry に登録する。
import 先 module は、この段階で全て読み込む必要はない。 import 先 module は、この段階で全て読み込む必要はない。
import expression が評価されたとき、module registry は path を解決し、未登録なら対象 module を parse / desugar して登録する。 import expression が評価されたとき、処理系は `SourceLoader` に現在の module key と import specifier を渡す。
loader は module key、表示名、source text を返す。
module registry は key が未登録なら対象 module を parse / desugar して登録する。
登録された module は module root thunk を持つ。 登録された module は module root thunk を持つ。
同じ module が複数回 import された場合は、同じ `ModuleId` を返す。 同じ module が複数回 import された場合は、同じ `ModuleId` を返す。
つまり import は module を即時評価しない。 つまり import は module を即時評価しない。
module を読み込み、module root を thunk として登録するだけにする。 module を読み込み、module root を thunk として登録するだけにする。
AST の `ExprId` は module-local である。
そのため runtime が保持する式参照は `ExprRef { module, expr }` として module-qualified にする。
## demand-driven evaluation ## demand-driven evaluation
評価器は、必要になった thunk だけを force する。 評価器は、必要になった thunk だけを force する。

View File

@ -0,0 +1,34 @@
# Features
Decodal keeps the embedded core small by making heavy functionality opt-in.
## Core defaults
`decodal-core` defaults to `std` only.
```toml
[features]
default = ["std"]
std = []
regex = ["std", "dep:regex"]
```
Building `decodal-core` with `--no-default-features` keeps the core in `no_std + alloc` mode and avoids optional dependencies.
## Regex
Regex constraints are implemented behind the `regex` feature.
When the feature is disabled, regex constraints parse and compose, but validating a concrete value against them returns an unsupported feature diagnostic.
```sh
cargo run -q -p decodal --features regex -- examples/regex/main.dcdl
```
Regex constraints are accumulated during `&` composition.
The implementation does not try to prove whether the intersection of two regex constraints is empty.
Concrete strings must match every regex constraint attached to the abstract value.
## CLI features
`decodal-cli` exposes a matching `regex` feature that enables `decodal-core/regex`.
The feature is not enabled by default so the default CLI binary remains small.

View File

@ -15,3 +15,5 @@ bytecode VM や JIT ではなく、AST を demand-driven に評価すること
3. [Thunk and Lazy Evaluation](./thunk-and-lazy-evaluation.md) 3. [Thunk and Lazy Evaluation](./thunk-and-lazy-evaluation.md)
4. [Composition and Materialization](./composition-and-materialization.md) 4. [Composition and Materialization](./composition-and-materialization.md)
5. [Diagnostics and Fallback](./diagnostics-and-fallback.md) 5. [Diagnostics and Fallback](./diagnostics-and-fallback.md)
6. [Embedding API](./embedding-api.md)
7. [Features](./features.md)

View File

@ -7,11 +7,16 @@ thunk は、まだ評価していない式をあとで評価できるように
```text ```text
Thunk { Thunk {
expr: ExprId expr: ExprRef
env: EnvId env: EnvId
state: ThunkState state: ThunkState
} }
ExprRef {
module: ModuleId
expr: ExprId
}
ThunkState = ThunkState =
Unevaluated Unevaluated
Evaluating Evaluating
@ -19,7 +24,8 @@ ThunkState =
Error(Diagnostic) Error(Diagnostic)
``` ```
`expr` は評価対象の AST node を指す。 `expr` は評価対象の AST node を module-qualified に指す。
`ExprId` は module-local な ID なので、runtime では `ModuleId` と組み合わせた `ExprRef` を保持する。
`env` は、その式を評価するときに使う lexical environment を指す。 `env` は、その式を評価するときに使う lexical environment を指す。
式だけではなく environment も保持するのは、遅延評価された式が定義時の名前解決文脈を必要とするためである。 式だけではなく environment も保持するのは、遅延評価された式が定義時の名前解決文脈を必要とするためである。

View File

@ -0,0 +1,91 @@
# Development
This document describes the development workflow for Decodal itself.
## Rust checks
Run the normal Rust checks from the repository root.
```sh
cargo fmt --check
cargo test
cargo check -p decodal-core --no-default-features
nix flake check
```
Regex support is optional and should be tested explicitly when touched.
```sh
cargo test -p decodal-core --features regex
cargo run -q -p decodal --features regex -- examples/regex/main.dcdl
```
## Tree-sitter grammar
The Tree-sitter grammar is kept in:
```text
editors/tree-sitter-decodal/
```
Important files:
```text
editors/tree-sitter-decodal/grammar.js
editors/tree-sitter-decodal/queries/highlights.scm
editors/tree-sitter-decodal/queries/locals.scm
editors/tree-sitter-decodal/corpus/basic.txt
```
The grammar is intended to be portable across editors such as Zed, Neovim, Helix, and Emacs.
Zed support should consume this grammar rather than relying on a TextMate grammar.
## Tree-sitter commands
From the grammar directory:
```sh
cd editors/tree-sitter-decodal
npm install
npx tree-sitter generate
npx tree-sitter test
```
To inspect a parse tree:
```sh
npx tree-sitter parse ../../examples/advanced/main.dcdl
```
The generated parser files under `editors/tree-sitter-decodal/src/` are committed so editor integrations can consume the grammar without regenerating it first.
`node_modules/` is ignored and must not be committed.
## Updating the grammar
When the Decodal syntax changes:
1. Update `grammar.js`.
2. Run `npx tree-sitter generate`.
3. Add or update corpus tests in `corpus/`.
4. Update highlight queries in `queries/highlights.scm` if token names changed.
5. Run `npx tree-sitter test`.
6. Run Rust checks from the repository root if the language parser or examples also changed.
## Development shell
The Nix development shell includes Rust tooling, Node.js, and Tree-sitter CLI tooling.
```sh
nix develop
```
The shell provides:
- `cargo`
- `rustc`
- `rustfmt`
- `clippy`
- `node`
- `npm`
- `tree-sitter`
- `nixfmt`

View File

@ -41,4 +41,7 @@ Decodal は Deferred Constraint Data Language、略称 DCDL のプロジェク
3. [Thunk and Lazy Evaluation](./design/thunk-and-lazy-evaluation.md) 3. [Thunk and Lazy Evaluation](./design/thunk-and-lazy-evaluation.md)
4. [Composition and Materialization](./design/composition-and-materialization.md) 4. [Composition and Materialization](./design/composition-and-materialization.md)
5. [Diagnostics and Fallback](./design/diagnostics-and-fallback.md) 5. [Diagnostics and Fallback](./design/diagnostics-and-fallback.md)
4. [Open Issues](./open-issues.md) 6. [Embedding API](./design/embedding-api.md)
7. [Features](./design/features.md)
4. [Development](./development.md)
5. [Open Issues](./open-issues.md)

View File

@ -31,9 +31,52 @@ A & B = A と B の両方を満たす値または制約
```dcdl ```dcdl
Int & String # エラー Int & String # エラー
>= 10 & <= 5 # エラーになりうる > 10 & < 5 # エラー
Int & > 10 & < 11 # エラー整数値の候補が存在しない
``` ```
## 制約の正規化
`&` によって abstract value 同士を合成した場合、処理系は軽量に判定できる制約を正規化する。
正規化対象:
- primitive type 制約。
- 数値比較制約。
primitive type 制約は、異なる型が同時に要求された場合 conflict になる。
```dcdl
Int & Float
Int & String
```
数値比較制約は上下限として正規化される。
```dcdl
Int & >= 1 & <= 65535 & > 443
```
これは概念的に以下へ正規化される。
```text
Type(Int)
> 443
<= 65535
```
上下限の交差が空であれば conflict になる。
`Int` 制約がある場合は、整数候補が存在するかも判定する。
```dcdl
> 10 & < 5 # conflict
Int & > 10 & < 11 # conflict
Int & >= 10 & <= 10 # OK
```
`Int` の比較制約は整数リテラルを使う。
`Float` の比較制約は整数リテラルまたは浮動小数リテラルを使える。
## 組み込み制約 ## 組み込み制約
最小の組み込み制約は以下である。 最小の組み込み制約は以下である。
@ -53,13 +96,28 @@ IPv4Address
## 正規表現制約 ## 正規表現制約
正規表現リテラルは文字列制約として使える候補である 正規表現リテラルは文字列制約として使える。
```dcdl ```dcdl
Host = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/; Host = /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/;
``` ```
ただし、組み込み向け実装では正規表現エンジンを optional feature にできる。 正規表現制約は積み重ね可能である。
複数の正規表現制約が同じ abstract value に付与された場合、具体文字列はすべての正規表現制約に一致しなければならない。
```dcdl
String & /^a/ & /z$/
```
処理系は、正規表現制約同士の交差が空であるかを合成時に判定する必要はない。
つまり、以下は合成時には conflict にならず、具体値検証時に失敗する。
```dcdl
String & /^a$/ & /^b$/
```
正規表現エンジンは optional feature にできる。
正規表現 feature が無効な処理系では、正規表現制約の検証は unsupported feature diagnostic になる。
軽量実装では代表的な制約を組み込み述語として提供してもよい。 軽量実装では代表的な制約を組み込み述語として提供してもよい。
```dcdl ```dcdl

View File

@ -5,12 +5,11 @@
## 構文 ## 構文
```dcdl ```dcdl
import ./config.n import "./config.dcdl"
import "./config.n"
``` ```
パス表記の詳細は未確定である。 import specifier は文字列リテラルとする。
パスリテラルと文字列リテラルの両方を許可するか、どちらかに統一するかは今後決める パスリテラル構文は採用しない
## モジュール ## モジュール
@ -18,6 +17,29 @@ import 先はモジュール単位で読み込まれる。
ただし、モジュール全体を即時評価する必要はない。 ただし、モジュール全体を即時評価する必要はない。
各フィールドは thunk として保持され、必要になったときだけ評価される。 各フィールドは thunk として保持され、必要になったときだけ評価される。
top-level に field 定義列を書いた module は、recursive module scope を作る。
つまり、top-level field は同じ module の他の top-level field から識別子として参照できる。
```dcdl
schema = {
hoge = String;
};
result = schema;
```
この場合、`result` の右辺の `schema` は同じ module の top-level field `schema` を参照する。
通常の object literal 内の field を暗黙に recursive scope にするかは別仕様とする。
## SourceLoader
`import` specifier の解決は処理系 core ではなく host 側の `SourceLoader` が行う。
CLI では、specifier を現在の module path からの相対 path として解決する。
組み込み利用では、resource table や static source map など、filesystem 以外の loader を使える。
module cache の key は loader が返す安定 key を使う。
CLI では canonical path を key とする。
## 循環 import ## 循環 import
モジュール間に循環参照があっても、必要なフィールドの依存関係が循環していなければ評価できる。 モジュール間に循環参照があっても、必要なフィールドの依存関係が循環していなければ評価できる。
@ -25,25 +47,25 @@ import 先はモジュール単位で読み込まれる。
例: 例:
```dcdl ```dcdl
# main.n # main.dcdl
{ {
schema = { schema = {
hoge = String; hoge = String;
}; };
result = (import ./func.n)(schema); result = (import "./func.dcdl")(schema);
} }
``` ```
```dcdl ```dcdl
# func.n # func.dcdl
(input: (import ./main.n).schema) => (input: (import "./main.dcdl").schema) =>
{ {
# ... # ...
} }
``` ```
`func.n` は `main.n` を import しているが、参照しているのは `main.schema` である。 `func.dcdl` は `main.dcdl` を import しているが、参照しているのは `main.schema` である。
`main.schema``main.result` に依存していなければ、この循環 import は成立する。 `main.schema``main.result` に依存していなければ、この循環 import は成立する。
## import の評価単位 ## import の評価単位

View File

@ -15,6 +15,25 @@ schema.dcdl
service.dcdl service.dcdl
``` ```
## Module source
ファイル全体は単一の式として書ける。
また、top-level に field 定義列を書いた場合は、暗黙の object として扱う。
```dcdl
host = String;
port = Int default 8080;
```
上の source は以下と同じ意味である。
```dcdl
{
host = String;
port = Int default 8080;
}
```
## コメント ## コメント
コメントは `#` から行末までとする。 コメントは `#` から行末までとする。
@ -38,7 +57,7 @@ host = "127.0.0.1"; # trailing comment
## 識別子 ## 識別子
識別子の厳密な字句規則は未確定である。 識別子は ASCII 英字で始まり、ASCII 英数字または `_` を続けられる。
慣習としては `lower_snake`、`lowerCamel`、`UpperCamel` を使える想定とする。 慣習としては `lower_snake`、`lowerCamel`、`UpperCamel` を使える想定とする。
```dcdl ```dcdl

View File

@ -9,7 +9,6 @@
- 演算子の優先順位。 - 演算子の優先順位。
- `rec` の扱い。 - `rec` の扱い。
- コメント構文を `#` のみにするか。 - コメント構文を `#` のみにするか。
- パス import と文字列 import の扱い分け。
## 型・制約 ## 型・制約

View File

@ -0,0 +1,39 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.{json,toml,yml,gyp}]
indent_style = space
indent_size = 2
[*.js]
indent_style = space
indent_size = 2
[*.rs]
indent_style = space
indent_size = 4
[*.{c,cc,h}]
indent_style = space
indent_size = 4
[*.{py,pyi}]
indent_style = space
indent_size = 4
[*.swift]
indent_style = space
indent_size = 4
[*.go]
indent_style = tab
indent_size = 8
[Makefile]
indent_style = tab
indent_size = 8

View File

@ -0,0 +1,11 @@
* text eol=lf
src/*.json linguist-generated
src/parser.c linguist-generated
src/tree_sitter/* linguist-generated
bindings/** linguist-generated
binding.gyp linguist-generated
setup.py linguist-generated
Makefile linguist-generated
Package.swift linguist-generated

38
editors/tree-sitter-decodal/.gitignore vendored Normal file
View File

@ -0,0 +1,38 @@
# Rust artifacts
Cargo.lock
target/
# Node artifacts
build/
prebuilds/
node_modules/
*.tgz
# Swift artifacts
.build/
# Go artifacts
go.sum
_obj/
# Python artifacts
.venv/
dist/
*.egg-info
*.whl
# C artifacts
*.a
*.so
*.so.*
*.dylib
*.dll
*.pc
# Example dirs
/examples/*/
# Grammar volatiles
*.wasm
*.obj
*.o

View File

@ -0,0 +1,23 @@
[package]
name = "tree-sitter-decodal"
description = "Decodal grammar for tree-sitter"
version = "0.0.1"
license = "MIT"
readme = "README.md"
keywords = ["incremental", "parsing", "tree-sitter", "decodal"]
categories = ["parsing", "text-editors"]
repository = "https://github.com/tree-sitter/tree-sitter-decodal"
edition = "2021"
autoexamples = false
build = "bindings/rust/build.rs"
include = ["bindings/rust/*", "grammar.js", "queries/*", "src/*"]
[lib]
path = "bindings/rust/lib.rs"
[dependencies]
tree-sitter = ">=0.22.6"
[build-dependencies]
cc = "1.0.87"

112
editors/tree-sitter-decodal/Makefile generated Normal file
View File

@ -0,0 +1,112 @@
VERSION := 0.0.1
LANGUAGE_NAME := tree-sitter-decodal
# repository
SRC_DIR := src
PARSER_REPO_URL := $(shell git -C $(SRC_DIR) remote get-url origin 2>/dev/null)
ifeq ($(PARSER_URL),)
PARSER_URL := $(subst .git,,$(PARSER_REPO_URL))
ifeq ($(shell echo $(PARSER_URL) | grep '^[a-z][-+.0-9a-z]*://'),)
PARSER_URL := $(subst :,/,$(PARSER_URL))
PARSER_URL := $(subst git@,https://,$(PARSER_URL))
endif
endif
TS ?= tree-sitter
# ABI versioning
SONAME_MAJOR := $(word 1,$(subst ., ,$(VERSION)))
SONAME_MINOR := $(word 2,$(subst ., ,$(VERSION)))
# install directory layout
PREFIX ?= /usr/local
INCLUDEDIR ?= $(PREFIX)/include
LIBDIR ?= $(PREFIX)/lib
PCLIBDIR ?= $(LIBDIR)/pkgconfig
# source/object files
PARSER := $(SRC_DIR)/parser.c
EXTRAS := $(filter-out $(PARSER),$(wildcard $(SRC_DIR)/*.c))
OBJS := $(patsubst %.c,%.o,$(PARSER) $(EXTRAS))
# flags
ARFLAGS ?= rcs
override CFLAGS += -I$(SRC_DIR) -std=c11 -fPIC
# OS-specific bits
ifeq ($(OS),Windows_NT)
$(error "Windows is not supported")
else ifeq ($(shell uname),Darwin)
SOEXT = dylib
SOEXTVER_MAJOR = $(SONAME_MAJOR).dylib
SOEXTVER = $(SONAME_MAJOR).$(SONAME_MINOR).dylib
LINKSHARED := $(LINKSHARED)-dynamiclib -Wl,
ifneq ($(ADDITIONAL_LIBS),)
LINKSHARED := $(LINKSHARED)$(ADDITIONAL_LIBS),
endif
LINKSHARED := $(LINKSHARED)-install_name,$(LIBDIR)/lib$(LANGUAGE_NAME).$(SONAME_MAJOR).dylib,-rpath,@executable_path/../Frameworks
else
SOEXT = so
SOEXTVER_MAJOR = so.$(SONAME_MAJOR)
SOEXTVER = so.$(SONAME_MAJOR).$(SONAME_MINOR)
LINKSHARED := $(LINKSHARED)-shared -Wl,
ifneq ($(ADDITIONAL_LIBS),)
LINKSHARED := $(LINKSHARED)$(ADDITIONAL_LIBS)
endif
LINKSHARED := $(LINKSHARED)-soname,lib$(LANGUAGE_NAME).so.$(SONAME_MAJOR)
endif
ifneq ($(filter $(shell uname),FreeBSD NetBSD DragonFly),)
PCLIBDIR := $(PREFIX)/libdata/pkgconfig
endif
all: lib$(LANGUAGE_NAME).a lib$(LANGUAGE_NAME).$(SOEXT) $(LANGUAGE_NAME).pc
lib$(LANGUAGE_NAME).a: $(OBJS)
$(AR) $(ARFLAGS) $@ $^
lib$(LANGUAGE_NAME).$(SOEXT): $(OBJS)
$(CC) $(LDFLAGS) $(LINKSHARED) $^ $(LDLIBS) -o $@
ifneq ($(STRIP),)
$(STRIP) $@
endif
$(LANGUAGE_NAME).pc: bindings/c/$(LANGUAGE_NAME).pc.in
sed -e 's|@URL@|$(PARSER_URL)|' \
-e 's|@VERSION@|$(VERSION)|' \
-e 's|@LIBDIR@|$(LIBDIR)|' \
-e 's|@INCLUDEDIR@|$(INCLUDEDIR)|' \
-e 's|@REQUIRES@|$(REQUIRES)|' \
-e 's|@ADDITIONAL_LIBS@|$(ADDITIONAL_LIBS)|' \
-e 's|=$(PREFIX)|=$${prefix}|' \
-e 's|@PREFIX@|$(PREFIX)|' $< > $@
$(PARSER): $(SRC_DIR)/grammar.json
$(TS) generate --no-bindings $^
install: all
install -d '$(DESTDIR)$(INCLUDEDIR)'/tree_sitter '$(DESTDIR)$(PCLIBDIR)' '$(DESTDIR)$(LIBDIR)'
install -m644 bindings/c/$(LANGUAGE_NAME).h '$(DESTDIR)$(INCLUDEDIR)'/tree_sitter/$(LANGUAGE_NAME).h
install -m644 $(LANGUAGE_NAME).pc '$(DESTDIR)$(PCLIBDIR)'/$(LANGUAGE_NAME).pc
install -m644 lib$(LANGUAGE_NAME).a '$(DESTDIR)$(LIBDIR)'/lib$(LANGUAGE_NAME).a
install -m755 lib$(LANGUAGE_NAME).$(SOEXT) '$(DESTDIR)$(LIBDIR)'/lib$(LANGUAGE_NAME).$(SOEXTVER)
ln -sf lib$(LANGUAGE_NAME).$(SOEXTVER) '$(DESTDIR)$(LIBDIR)'/lib$(LANGUAGE_NAME).$(SOEXTVER_MAJOR)
ln -sf lib$(LANGUAGE_NAME).$(SOEXTVER_MAJOR) '$(DESTDIR)$(LIBDIR)'/lib$(LANGUAGE_NAME).$(SOEXT)
uninstall:
$(RM) '$(DESTDIR)$(LIBDIR)'/lib$(LANGUAGE_NAME).a \
'$(DESTDIR)$(LIBDIR)'/lib$(LANGUAGE_NAME).$(SOEXTVER) \
'$(DESTDIR)$(LIBDIR)'/lib$(LANGUAGE_NAME).$(SOEXTVER_MAJOR) \
'$(DESTDIR)$(LIBDIR)'/lib$(LANGUAGE_NAME).$(SOEXT) \
'$(DESTDIR)$(INCLUDEDIR)'/tree_sitter/$(LANGUAGE_NAME).h \
'$(DESTDIR)$(PCLIBDIR)'/$(LANGUAGE_NAME).pc
clean:
$(RM) $(OBJS) $(LANGUAGE_NAME).pc lib$(LANGUAGE_NAME).a lib$(LANGUAGE_NAME).$(SOEXT)
test:
$(TS) test
.PHONY: all install uninstall clean test

View File

@ -0,0 +1,47 @@
// swift-tools-version:5.3
import PackageDescription
let package = Package(
name: "TreeSitterDecodal",
products: [
.library(name: "TreeSitterDecodal", targets: ["TreeSitterDecodal"]),
],
dependencies: [],
targets: [
.target(name: "TreeSitterDecodal",
path: ".",
exclude: [
"Cargo.toml",
"Makefile",
"binding.gyp",
"bindings/c",
"bindings/go",
"bindings/node",
"bindings/python",
"bindings/rust",
"prebuilds",
"grammar.js",
"package.json",
"package-lock.json",
"pyproject.toml",
"setup.py",
"test",
"examples",
".editorconfig",
".github",
".gitignore",
".gitattributes",
".gitmodules",
],
sources: [
"src/parser.c",
// NOTE: if your language has an external scanner, add it here.
],
resources: [
.copy("queries")
],
publicHeadersPath: "bindings/swift",
cSettings: [.headerSearchPath("src")])
],
cLanguageStandard: .c11
)

30
editors/tree-sitter-decodal/binding.gyp generated Normal file
View File

@ -0,0 +1,30 @@
{
"targets": [
{
"target_name": "tree_sitter_decodal_binding",
"dependencies": [
"<!(node -p \"require('node-addon-api').targets\"):node_addon_api_except",
],
"include_dirs": [
"src",
],
"sources": [
"bindings/node/binding.cc",
"src/parser.c",
# NOTE: if your language has an external scanner, add it here.
],
"conditions": [
["OS!='win'", {
"cflags_c": [
"-std=c11",
],
}, { # OS == "win"
"cflags_c": [
"/std:c11",
"/utf-8",
],
}],
],
}
]
}

View File

@ -0,0 +1,16 @@
#ifndef TREE_SITTER_DECODAL_H_
#define TREE_SITTER_DECODAL_H_
typedef struct TSLanguage TSLanguage;
#ifdef __cplusplus
extern "C" {
#endif
const TSLanguage *tree_sitter_decodal(void);
#ifdef __cplusplus
}
#endif
#endif // TREE_SITTER_DECODAL_H_

View File

@ -0,0 +1,11 @@
prefix=@PREFIX@
libdir=@LIBDIR@
includedir=@INCLUDEDIR@
Name: tree-sitter-decodal
Description: Decodal grammar for tree-sitter
URL: @URL@
Version: @VERSION@
Requires: @REQUIRES@
Libs: -L${libdir} @ADDITIONAL_LIBS@ -ltree-sitter-decodal
Cflags: -I${includedir}

View File

@ -0,0 +1,13 @@
package tree_sitter_decodal
// #cgo CFLAGS: -std=c11 -fPIC
// #include "../../src/parser.c"
// // NOTE: if your language has an external scanner, add it here.
import "C"
import "unsafe"
// Get the tree-sitter Language for this grammar.
func Language() unsafe.Pointer {
return unsafe.Pointer(C.tree_sitter_decodal())
}

View File

@ -0,0 +1,15 @@
package tree_sitter_decodal_test
import (
"testing"
tree_sitter "github.com/smacker/go-tree-sitter"
"github.com/tree-sitter/tree-sitter-decodal"
)
func TestCanLoadGrammar(t *testing.T) {
language := tree_sitter.NewLanguage(tree_sitter_decodal.Language())
if language == nil {
t.Errorf("Error loading Decodal grammar")
}
}

View File

@ -0,0 +1,5 @@
module github.com/tree-sitter/tree-sitter-decodal
go 1.22
require github.com/smacker/go-tree-sitter v0.0.0-20230720070738-0d0a9f78d8f8

View File

@ -0,0 +1,20 @@
#include <napi.h>
typedef struct TSLanguage TSLanguage;
extern "C" TSLanguage *tree_sitter_decodal();
// "tree-sitter", "language" hashed with BLAKE2
const napi_type_tag LANGUAGE_TYPE_TAG = {
0x8AF2E5212AD58ABF, 0xD5006CAD83ABBA16
};
Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports["name"] = Napi::String::New(env, "decodal");
auto language = Napi::External<TSLanguage>::New(env, tree_sitter_decodal());
language.TypeTag(&LANGUAGE_TYPE_TAG);
exports["language"] = language;
return exports;
}
NODE_API_MODULE(tree_sitter_decodal_binding, Init)

28
editors/tree-sitter-decodal/bindings/node/index.d.ts generated vendored Normal file
View File

@ -0,0 +1,28 @@
type BaseNode = {
type: string;
named: boolean;
};
type ChildNode = {
multiple: boolean;
required: boolean;
types: BaseNode[];
};
type NodeInfo =
| (BaseNode & {
subtypes: BaseNode[];
})
| (BaseNode & {
fields: { [name: string]: ChildNode };
children: ChildNode[];
});
type Language = {
name: string;
language: unknown;
nodeTypeInfo: NodeInfo[];
};
declare const language: Language;
export = language;

View File

@ -0,0 +1,7 @@
const root = require("path").join(__dirname, "..", "..");
module.exports = require("node-gyp-build")(root);
try {
module.exports.nodeTypeInfo = require("../../src/node-types.json");
} catch (_) {}

View File

@ -0,0 +1,5 @@
"Decodal grammar for tree-sitter"
from ._binding import language
__all__ = ["language"]

View File

@ -0,0 +1 @@
def language() -> int: ...

View File

@ -0,0 +1,27 @@
#include <Python.h>
typedef struct TSLanguage TSLanguage;
TSLanguage *tree_sitter_decodal(void);
static PyObject* _binding_language(PyObject *self, PyObject *args) {
return PyLong_FromVoidPtr(tree_sitter_decodal());
}
static PyMethodDef methods[] = {
{"language", _binding_language, METH_NOARGS,
"Get the tree-sitter language for this grammar."},
{NULL, NULL, 0, NULL}
};
static struct PyModuleDef module = {
.m_base = PyModuleDef_HEAD_INIT,
.m_name = "_binding",
.m_doc = NULL,
.m_size = -1,
.m_methods = methods
};
PyMODINIT_FUNC PyInit__binding(void) {
return PyModule_Create(&module);
}

View File

@ -0,0 +1,22 @@
fn main() {
let src_dir = std::path::Path::new("src");
let mut c_config = cc::Build::new();
c_config.std("c11").include(src_dir);
#[cfg(target_env = "msvc")]
c_config.flag("-utf-8");
let parser_path = src_dir.join("parser.c");
c_config.file(&parser_path);
println!("cargo:rerun-if-changed={}", parser_path.to_str().unwrap());
// NOTE: if your language uses an external scanner, uncomment this block:
/*
let scanner_path = src_dir.join("scanner.c");
c_config.file(&scanner_path);
println!("cargo:rerun-if-changed={}", scanner_path.to_str().unwrap());
*/
c_config.compile("tree-sitter-decodal");
}

View File

@ -0,0 +1,54 @@
//! This crate provides Decodal language support for the [tree-sitter][] parsing library.
//!
//! Typically, you will use the [language][language func] function to add this language to a
//! tree-sitter [Parser][], and then use the parser to parse some code:
//!
//! ```
//! let code = r#"
//! "#;
//! let mut parser = tree_sitter::Parser::new();
//! parser.set_language(&tree_sitter_decodal::language()).expect("Error loading Decodal grammar");
//! let tree = parser.parse(code, None).unwrap();
//! assert!(!tree.root_node().has_error());
//! ```
//!
//! [Language]: https://docs.rs/tree-sitter/*/tree_sitter/struct.Language.html
//! [language func]: fn.language.html
//! [Parser]: https://docs.rs/tree-sitter/*/tree_sitter/struct.Parser.html
//! [tree-sitter]: https://tree-sitter.github.io/
use tree_sitter::Language;
extern "C" {
fn tree_sitter_decodal() -> Language;
}
/// Get the tree-sitter [Language][] for this grammar.
///
/// [Language]: https://docs.rs/tree-sitter/*/tree_sitter/struct.Language.html
pub fn language() -> Language {
unsafe { tree_sitter_decodal() }
}
/// The content of the [`node-types.json`][] file for this grammar.
///
/// [`node-types.json`]: https://tree-sitter.github.io/tree-sitter/using-parsers#static-node-types
pub const NODE_TYPES: &str = include_str!("../../src/node-types.json");
// Uncomment these to include any queries that this grammar contains
// pub const HIGHLIGHTS_QUERY: &str = include_str!("../../queries/highlights.scm");
// pub const INJECTIONS_QUERY: &str = include_str!("../../queries/injections.scm");
// pub const LOCALS_QUERY: &str = include_str!("../../queries/locals.scm");
// pub const TAGS_QUERY: &str = include_str!("../../queries/tags.scm");
#[cfg(test)]
mod tests {
#[test]
fn test_can_load_grammar() {
let mut parser = tree_sitter::Parser::new();
parser
.set_language(&super::language())
.expect("Error loading Decodal grammar");
}
}

View File

@ -0,0 +1,16 @@
#ifndef TREE_SITTER_DECODAL_H_
#define TREE_SITTER_DECODAL_H_
typedef struct TSLanguage TSLanguage;
#ifdef __cplusplus
extern "C" {
#endif
const TSLanguage *tree_sitter_decodal(void);
#ifdef __cplusplus
}
#endif
#endif // TREE_SITTER_DECODAL_H_

View File

@ -0,0 +1,76 @@
==================
Top-level fields
==================
host = String;
port = Int & > 443 default 8080;
---
(source_file
(field_definition
path: (field_path (identifier))
value: (identifier))
(field_definition
path: (field_path (identifier))
value: (default_expression
base: (binary_expression
left: (identifier)
right: (comparison_constraint
value: (integer)))
fallback: (literal (integer)))))
==================
Import and function
==================
let
schema = import "./schema.dcdl";
mk = (cfg: schema.Service) => cfg;
in
mk({ name = "api"; })
---
(source_file
(let_expression
(field_definition
path: (field_path (identifier))
value: (import_expression
specifier: (string)))
(field_definition
path: (field_path (identifier))
value: (function_expression
(parameter
name: (identifier)
constraint: (path_expression
object: (identifier)
field: (identifier)))
body: (identifier)))
body: (call_expression
function: (identifier)
(object
(field_definition
path: (field_path (identifier))
value: (literal (string)))))))
==================
Match and patch
==================
base // {
summary = match env {
"prod": "production";
_: "other";
};
}
---
(source_file
(binary_expression
left: (identifier)
right: (object
(field_definition
path: (field_path (identifier))
value: (match_expression
scrutinee: (identifier)
(match_arm
pattern: (literal (string))
body: (literal (string)))
(match_arm
body: (literal (string))))))))

View File

@ -0,0 +1,186 @@
const PREC = {
DEFAULT: 1,
PATCH: 2,
AND: 3,
CALL: 7,
PATH: 8,
};
function commaSep(rule) {
return optional(seq(rule, repeat(seq(',', rule)), optional(',')));
}
function semiSep(rule) {
return optional(seq(rule, repeat(seq(';', rule)), optional(';')));
}
module.exports = grammar({
name: 'decodal',
extras: $ => [
/[\s\uFEFF\u2060\u200B]/,
$.comment,
],
word: $ => $.identifier,
conflicts: $ => [
[$._expression, $.parameter],
],
rules: {
source_file: $ => repeat($._statement),
_statement: $ => choice(
seq($.field_definition, optional(';')),
seq($._expression, optional(';')),
),
comment: _ => token(seq('#', /.*/)),
_expression: $ => choice(
$.literal,
$.identifier,
$.regex_literal,
$.comparison_constraint,
$.object,
$.array,
$.let_expression,
$.function_expression,
$.match_expression,
$.import_expression,
$.parenthesized_expression,
$.call_expression,
$.path_expression,
$.binary_expression,
$.default_expression,
),
literal: $ => choice(
$.string,
$.integer,
$.float,
$.boolean,
),
boolean: _ => choice('true', 'false'),
string: _ => token(seq(
'"',
repeat(choice(/[^"\\\n]/, /\\./)),
'"',
)),
integer: _ => token(/[0-9]+/),
float: _ => token(/[0-9]+\.[0-9]+/),
identifier: _ => /[A-Za-z][A-Za-z0-9_]*/,
regex_literal: _ => token(seq(
'/',
repeat1(choice(/[^\/\\\n]/, /\\./)),
'/',
)),
object: $ => seq(
'{',
semiSep($.field_definition),
'}',
),
field_definition: $ => seq(
field('path', $.field_path),
'=',
field('value', $._expression),
),
field_path: $ => prec(10, seq(
$.identifier,
repeat(seq('.', $.identifier)),
)),
array: $ => seq(
'[',
commaSep($._expression),
']',
),
let_expression: $ => seq(
'let',
repeat(seq($.field_definition, ';')),
'in',
field('body', $._expression),
),
function_expression: $ => prec.right(seq(
'(',
commaSep($.parameter),
')',
'=>',
field('body', $._expression),
)),
parameter: $ => seq(
field('name', $.identifier),
optional(seq(':', field('constraint', $._expression))),
),
match_expression: $ => seq(
'match',
field('scrutinee', $._expression),
'{',
semiSep($.match_arm),
'}',
),
match_arm: $ => seq(
field('pattern', choice('_', $._expression)),
':',
field('body', $._expression),
),
import_expression: $ => seq(
'import',
field('specifier', $.string),
),
parenthesized_expression: $ => seq('(', $._expression, ')'),
call_expression: $ => prec.left(PREC.CALL, seq(
field('function', $._expression),
'(',
commaSep($._expression),
')',
)),
path_expression: $ => prec.left(PREC.PATH, seq(
field('object', $._expression),
'.',
field('field', $.identifier),
)),
comparison_constraint: $ => prec(6, seq(
field('operator', choice('>', '>=', '<', '<=')),
field('value', choice($.integer, $.float)),
)),
binary_expression: $ => choice(
prec.left(PREC.AND, seq(
field('left', $._expression),
field('operator', '&'),
field('right', $._expression),
)),
prec.left(PREC.PATCH, seq(
field('left', $._expression),
field('operator', token(prec(2, '//'))),
field('right', $._expression),
)),
),
default_expression: $ => prec.right(PREC.DEFAULT, seq(
field('base', $._expression),
'default',
field('fallback', $._expression),
)),
},
});

View File

@ -0,0 +1,27 @@
{
"name": "tree-sitter-decodal",
"version": "0.1.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "tree-sitter-decodal",
"version": "0.1.0",
"license": "MIT",
"devDependencies": {
"tree-sitter-cli": "^0.22.6"
}
},
"node_modules/tree-sitter-cli": {
"version": "0.22.6",
"resolved": "https://registry.npmjs.org/tree-sitter-cli/-/tree-sitter-cli-0.22.6.tgz",
"integrity": "sha512-s7mYOJXi8sIFkt/nLJSqlYZP96VmKTc3BAwIX0rrrlRxWjWuCwixFqwzxWZBQz4R8Hx01iP7z3cT3ih58BUmZQ==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"tree-sitter": "cli.js"
}
}
}
}

View File

@ -0,0 +1,50 @@
{
"name": "tree-sitter-decodal",
"version": "0.1.0",
"description": "Tree-sitter grammar for Decodal / DCDL",
"main": "grammar.js",
"types": "bindings/node",
"keywords": [
"tree-sitter",
"decodal",
"dcdl"
],
"files": [
"grammar.js",
"binding.gyp",
"prebuilds/**",
"bindings/node/*",
"queries/*",
"src/**"
],
"license": "MIT",
"tree-sitter": [
{
"scope": "source.dcdl",
"file-types": [
"dcdl"
]
}
],
"devDependencies": {
"tree-sitter-cli": "^0.22.6",
"prebuildify": "^6.0.0"
},
"scripts": {
"generate": "tree-sitter generate",
"test": "tree-sitter test",
"install": "node-gyp-build",
"prebuildify": "prebuildify --napi --strip"
},
"dependencies": {
"node-gyp-build": "^4.8.0"
},
"peerDependencies": {
"tree-sitter": "^0.21.0"
},
"peerDependenciesMeta": {
"tree_sitter": {
"optional": true
}
}
}

View File

@ -0,0 +1,29 @@
[build-system]
requires = ["setuptools>=42", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "tree-sitter-decodal"
description = "Decodal grammar for tree-sitter"
version = "0.0.1"
keywords = ["incremental", "parsing", "tree-sitter", "decodal"]
classifiers = [
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Topic :: Software Development :: Compilers",
"Topic :: Text Processing :: Linguistic",
"Typing :: Typed"
]
requires-python = ">=3.8"
license.text = "MIT"
readme = "README.md"
[project.urls]
Homepage = "https://github.com/tree-sitter/tree-sitter-decodal"
[project.optional-dependencies]
core = ["tree-sitter~=0.21"]
[tool.cibuildwheel]
build = "cp38-*"
build-frontend = "build"

View File

@ -0,0 +1,58 @@
(comment) @comment
(string) @string
(regex_literal) @string.regexp
(integer) @number
(float) @number
(boolean) @boolean
[
"let"
"in"
"match"
"import"
"default"
] @keyword
[
"&"
"//"
"=>"
"="
">"
">="
"<"
"<="
] @operator
[
"{"
"}"
"["
"]"
"("
")"
] @punctuation.bracket
[
";"
","
"."
":"
] @punctuation.delimiter
((identifier) @type.builtin
(#match? @type.builtin "^(String|Int|Float|Bool)$"))
(field_definition
path: (field_path (identifier) @property))
(path_expression
field: (identifier) @property)
(parameter
name: (identifier) @variable.parameter)
(function_expression) @function
(call_expression
function: (identifier) @function.call)

View File

@ -0,0 +1,3 @@
(parameter name: (identifier) @local.definition)
(field_definition path: (field_path (identifier) @local.definition))
(identifier) @local.reference

60
editors/tree-sitter-decodal/setup.py generated Normal file
View File

@ -0,0 +1,60 @@
from os.path import isdir, join
from platform import system
from setuptools import Extension, find_packages, setup
from setuptools.command.build import build
from wheel.bdist_wheel import bdist_wheel
class Build(build):
def run(self):
if isdir("queries"):
dest = join(self.build_lib, "tree_sitter_decodal", "queries")
self.copy_tree("queries", dest)
super().run()
class BdistWheel(bdist_wheel):
def get_tag(self):
python, abi, platform = super().get_tag()
if python.startswith("cp"):
python, abi = "cp38", "abi3"
return python, abi, platform
setup(
packages=find_packages("bindings/python"),
package_dir={"": "bindings/python"},
package_data={
"tree_sitter_decodal": ["*.pyi", "py.typed"],
"tree_sitter_decodal.queries": ["*.scm"],
},
ext_package="tree_sitter_decodal",
ext_modules=[
Extension(
name="_binding",
sources=[
"bindings/python/tree_sitter_decodal/binding.c",
"src/parser.c",
# NOTE: if your language uses an external scanner, add it here.
],
extra_compile_args=[
"-std=c11",
] if system() != "Windows" else [
"/std:c11",
"/utf-8",
],
define_macros=[
("Py_LIMITED_API", "0x03080000"),
("PY_SSIZE_T_CLEAN", None)
],
include_dirs=["src"],
py_limited_api=True,
)
],
cmdclass={
"build": Build,
"bdist_wheel": BdistWheel
},
zip_safe=False
)

View File

@ -0,0 +1,991 @@
{
"name": "decodal",
"word": "identifier",
"rules": {
"source_file": {
"type": "REPEAT",
"content": {
"type": "SYMBOL",
"name": "_statement"
}
},
"_statement": {
"type": "CHOICE",
"members": [
{
"type": "SEQ",
"members": [
{
"type": "SYMBOL",
"name": "field_definition"
},
{
"type": "CHOICE",
"members": [
{
"type": "STRING",
"value": ";"
},
{
"type": "BLANK"
}
]
}
]
},
{
"type": "SEQ",
"members": [
{
"type": "SYMBOL",
"name": "_expression"
},
{
"type": "CHOICE",
"members": [
{
"type": "STRING",
"value": ";"
},
{
"type": "BLANK"
}
]
}
]
}
]
},
"comment": {
"type": "TOKEN",
"content": {
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "#"
},
{
"type": "PATTERN",
"value": ".*"
}
]
}
},
"_expression": {
"type": "CHOICE",
"members": [
{
"type": "SYMBOL",
"name": "literal"
},
{
"type": "SYMBOL",
"name": "identifier"
},
{
"type": "SYMBOL",
"name": "regex_literal"
},
{
"type": "SYMBOL",
"name": "comparison_constraint"
},
{
"type": "SYMBOL",
"name": "object"
},
{
"type": "SYMBOL",
"name": "array"
},
{
"type": "SYMBOL",
"name": "let_expression"
},
{
"type": "SYMBOL",
"name": "function_expression"
},
{
"type": "SYMBOL",
"name": "match_expression"
},
{
"type": "SYMBOL",
"name": "import_expression"
},
{
"type": "SYMBOL",
"name": "parenthesized_expression"
},
{
"type": "SYMBOL",
"name": "call_expression"
},
{
"type": "SYMBOL",
"name": "path_expression"
},
{
"type": "SYMBOL",
"name": "binary_expression"
},
{
"type": "SYMBOL",
"name": "default_expression"
}
]
},
"literal": {
"type": "CHOICE",
"members": [
{
"type": "SYMBOL",
"name": "string"
},
{
"type": "SYMBOL",
"name": "integer"
},
{
"type": "SYMBOL",
"name": "float"
},
{
"type": "SYMBOL",
"name": "boolean"
}
]
},
"boolean": {
"type": "CHOICE",
"members": [
{
"type": "STRING",
"value": "true"
},
{
"type": "STRING",
"value": "false"
}
]
},
"string": {
"type": "TOKEN",
"content": {
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "\""
},
{
"type": "REPEAT",
"content": {
"type": "CHOICE",
"members": [
{
"type": "PATTERN",
"value": "[^\"\\\\\\n]"
},
{
"type": "PATTERN",
"value": "\\\\."
}
]
}
},
{
"type": "STRING",
"value": "\""
}
]
}
},
"integer": {
"type": "TOKEN",
"content": {
"type": "PATTERN",
"value": "[0-9]+"
}
},
"float": {
"type": "TOKEN",
"content": {
"type": "PATTERN",
"value": "[0-9]+\\.[0-9]+"
}
},
"identifier": {
"type": "PATTERN",
"value": "[A-Za-z][A-Za-z0-9_]*"
},
"regex_literal": {
"type": "TOKEN",
"content": {
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "/"
},
{
"type": "REPEAT1",
"content": {
"type": "CHOICE",
"members": [
{
"type": "PATTERN",
"value": "[^\\/\\\\\\n]"
},
{
"type": "PATTERN",
"value": "\\\\."
}
]
}
},
{
"type": "STRING",
"value": "/"
}
]
}
},
"object": {
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "{"
},
{
"type": "CHOICE",
"members": [
{
"type": "SEQ",
"members": [
{
"type": "SYMBOL",
"name": "field_definition"
},
{
"type": "REPEAT",
"content": {
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": ";"
},
{
"type": "SYMBOL",
"name": "field_definition"
}
]
}
},
{
"type": "CHOICE",
"members": [
{
"type": "STRING",
"value": ";"
},
{
"type": "BLANK"
}
]
}
]
},
{
"type": "BLANK"
}
]
},
{
"type": "STRING",
"value": "}"
}
]
},
"field_definition": {
"type": "SEQ",
"members": [
{
"type": "FIELD",
"name": "path",
"content": {
"type": "SYMBOL",
"name": "field_path"
}
},
{
"type": "STRING",
"value": "="
},
{
"type": "FIELD",
"name": "value",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
}
]
},
"field_path": {
"type": "PREC",
"value": 10,
"content": {
"type": "SEQ",
"members": [
{
"type": "SYMBOL",
"name": "identifier"
},
{
"type": "REPEAT",
"content": {
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "."
},
{
"type": "SYMBOL",
"name": "identifier"
}
]
}
}
]
}
},
"array": {
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "["
},
{
"type": "CHOICE",
"members": [
{
"type": "SEQ",
"members": [
{
"type": "SYMBOL",
"name": "_expression"
},
{
"type": "REPEAT",
"content": {
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": ","
},
{
"type": "SYMBOL",
"name": "_expression"
}
]
}
},
{
"type": "CHOICE",
"members": [
{
"type": "STRING",
"value": ","
},
{
"type": "BLANK"
}
]
}
]
},
{
"type": "BLANK"
}
]
},
{
"type": "STRING",
"value": "]"
}
]
},
"let_expression": {
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "let"
},
{
"type": "REPEAT",
"content": {
"type": "SEQ",
"members": [
{
"type": "SYMBOL",
"name": "field_definition"
},
{
"type": "STRING",
"value": ";"
}
]
}
},
{
"type": "STRING",
"value": "in"
},
{
"type": "FIELD",
"name": "body",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
}
]
},
"function_expression": {
"type": "PREC_RIGHT",
"value": 0,
"content": {
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "("
},
{
"type": "CHOICE",
"members": [
{
"type": "SEQ",
"members": [
{
"type": "SYMBOL",
"name": "parameter"
},
{
"type": "REPEAT",
"content": {
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": ","
},
{
"type": "SYMBOL",
"name": "parameter"
}
]
}
},
{
"type": "CHOICE",
"members": [
{
"type": "STRING",
"value": ","
},
{
"type": "BLANK"
}
]
}
]
},
{
"type": "BLANK"
}
]
},
{
"type": "STRING",
"value": ")"
},
{
"type": "STRING",
"value": "=>"
},
{
"type": "FIELD",
"name": "body",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
}
]
}
},
"parameter": {
"type": "SEQ",
"members": [
{
"type": "FIELD",
"name": "name",
"content": {
"type": "SYMBOL",
"name": "identifier"
}
},
{
"type": "CHOICE",
"members": [
{
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": ":"
},
{
"type": "FIELD",
"name": "constraint",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
}
]
},
{
"type": "BLANK"
}
]
}
]
},
"match_expression": {
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "match"
},
{
"type": "FIELD",
"name": "scrutinee",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
},
{
"type": "STRING",
"value": "{"
},
{
"type": "CHOICE",
"members": [
{
"type": "SEQ",
"members": [
{
"type": "SYMBOL",
"name": "match_arm"
},
{
"type": "REPEAT",
"content": {
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": ";"
},
{
"type": "SYMBOL",
"name": "match_arm"
}
]
}
},
{
"type": "CHOICE",
"members": [
{
"type": "STRING",
"value": ";"
},
{
"type": "BLANK"
}
]
}
]
},
{
"type": "BLANK"
}
]
},
{
"type": "STRING",
"value": "}"
}
]
},
"match_arm": {
"type": "SEQ",
"members": [
{
"type": "FIELD",
"name": "pattern",
"content": {
"type": "CHOICE",
"members": [
{
"type": "STRING",
"value": "_"
},
{
"type": "SYMBOL",
"name": "_expression"
}
]
}
},
{
"type": "STRING",
"value": ":"
},
{
"type": "FIELD",
"name": "body",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
}
]
},
"import_expression": {
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "import"
},
{
"type": "FIELD",
"name": "specifier",
"content": {
"type": "SYMBOL",
"name": "string"
}
}
]
},
"parenthesized_expression": {
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "("
},
{
"type": "SYMBOL",
"name": "_expression"
},
{
"type": "STRING",
"value": ")"
}
]
},
"call_expression": {
"type": "PREC_LEFT",
"value": 7,
"content": {
"type": "SEQ",
"members": [
{
"type": "FIELD",
"name": "function",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
},
{
"type": "STRING",
"value": "("
},
{
"type": "CHOICE",
"members": [
{
"type": "SEQ",
"members": [
{
"type": "SYMBOL",
"name": "_expression"
},
{
"type": "REPEAT",
"content": {
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": ","
},
{
"type": "SYMBOL",
"name": "_expression"
}
]
}
},
{
"type": "CHOICE",
"members": [
{
"type": "STRING",
"value": ","
},
{
"type": "BLANK"
}
]
}
]
},
{
"type": "BLANK"
}
]
},
{
"type": "STRING",
"value": ")"
}
]
}
},
"path_expression": {
"type": "PREC_LEFT",
"value": 8,
"content": {
"type": "SEQ",
"members": [
{
"type": "FIELD",
"name": "object",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
},
{
"type": "STRING",
"value": "."
},
{
"type": "FIELD",
"name": "field",
"content": {
"type": "SYMBOL",
"name": "identifier"
}
}
]
}
},
"comparison_constraint": {
"type": "PREC",
"value": 6,
"content": {
"type": "SEQ",
"members": [
{
"type": "FIELD",
"name": "operator",
"content": {
"type": "CHOICE",
"members": [
{
"type": "STRING",
"value": ">"
},
{
"type": "STRING",
"value": ">="
},
{
"type": "STRING",
"value": "<"
},
{
"type": "STRING",
"value": "<="
}
]
}
},
{
"type": "FIELD",
"name": "value",
"content": {
"type": "CHOICE",
"members": [
{
"type": "SYMBOL",
"name": "integer"
},
{
"type": "SYMBOL",
"name": "float"
}
]
}
}
]
}
},
"binary_expression": {
"type": "CHOICE",
"members": [
{
"type": "PREC_LEFT",
"value": 3,
"content": {
"type": "SEQ",
"members": [
{
"type": "FIELD",
"name": "left",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
},
{
"type": "FIELD",
"name": "operator",
"content": {
"type": "STRING",
"value": "&"
}
},
{
"type": "FIELD",
"name": "right",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
}
]
}
},
{
"type": "PREC_LEFT",
"value": 2,
"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"
}
}
]
}
}
]
},
"default_expression": {
"type": "PREC_RIGHT",
"value": 1,
"content": {
"type": "SEQ",
"members": [
{
"type": "FIELD",
"name": "base",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
},
{
"type": "STRING",
"value": "default"
},
{
"type": "FIELD",
"name": "fallback",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
}
]
}
}
},
"extras": [
{
"type": "PATTERN",
"value": "[\\s\\uFEFF\\u2060\\u200B]"
},
{
"type": "SYMBOL",
"name": "comment"
}
],
"conflicts": [
[
"_expression",
"parameter"
]
],
"precedences": [],
"externals": [],
"inline": [],
"supertypes": []
}

File diff suppressed because it is too large Load Diff

4200
editors/tree-sitter-decodal/src/parser.c generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,54 @@
#ifndef TREE_SITTER_ALLOC_H_
#define TREE_SITTER_ALLOC_H_
#ifdef __cplusplus
extern "C" {
#endif
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
// Allow clients to override allocation functions
#ifdef TREE_SITTER_REUSE_ALLOCATOR
extern void *(*ts_current_malloc)(size_t);
extern void *(*ts_current_calloc)(size_t, size_t);
extern void *(*ts_current_realloc)(void *, size_t);
extern void (*ts_current_free)(void *);
#ifndef ts_malloc
#define ts_malloc ts_current_malloc
#endif
#ifndef ts_calloc
#define ts_calloc ts_current_calloc
#endif
#ifndef ts_realloc
#define ts_realloc ts_current_realloc
#endif
#ifndef ts_free
#define ts_free ts_current_free
#endif
#else
#ifndef ts_malloc
#define ts_malloc malloc
#endif
#ifndef ts_calloc
#define ts_calloc calloc
#endif
#ifndef ts_realloc
#define ts_realloc realloc
#endif
#ifndef ts_free
#define ts_free free
#endif
#endif
#ifdef __cplusplus
}
#endif
#endif // TREE_SITTER_ALLOC_H_

View File

@ -0,0 +1,290 @@
#ifndef TREE_SITTER_ARRAY_H_
#define TREE_SITTER_ARRAY_H_
#ifdef __cplusplus
extern "C" {
#endif
#include "./alloc.h"
#include <assert.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#ifdef _MSC_VER
#pragma warning(disable : 4101)
#elif defined(__GNUC__) || defined(__clang__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wunused-variable"
#endif
#define Array(T) \
struct { \
T *contents; \
uint32_t size; \
uint32_t capacity; \
}
/// Initialize an array.
#define array_init(self) \
((self)->size = 0, (self)->capacity = 0, (self)->contents = NULL)
/// Create an empty array.
#define array_new() \
{ NULL, 0, 0 }
/// Get a pointer to the element at a given `index` in the array.
#define array_get(self, _index) \
(assert((uint32_t)(_index) < (self)->size), &(self)->contents[_index])
/// Get a pointer to the first element in the array.
#define array_front(self) array_get(self, 0)
/// Get a pointer to the last element in the array.
#define array_back(self) array_get(self, (self)->size - 1)
/// Clear the array, setting its size to zero. Note that this does not free any
/// memory allocated for the array's contents.
#define array_clear(self) ((self)->size = 0)
/// Reserve `new_capacity` elements of space in the array. If `new_capacity` is
/// less than the array's current capacity, this function has no effect.
#define array_reserve(self, new_capacity) \
_array__reserve((Array *)(self), array_elem_size(self), new_capacity)
/// Free any memory allocated for this array. Note that this does not free any
/// memory allocated for the array's contents.
#define array_delete(self) _array__delete((Array *)(self))
/// Push a new `element` onto the end of the array.
#define array_push(self, element) \
(_array__grow((Array *)(self), 1, array_elem_size(self)), \
(self)->contents[(self)->size++] = (element))
/// Increase the array's size by `count` elements.
/// New elements are zero-initialized.
#define array_grow_by(self, count) \
do { \
if ((count) == 0) break; \
_array__grow((Array *)(self), count, array_elem_size(self)); \
memset((self)->contents + (self)->size, 0, (count) * array_elem_size(self)); \
(self)->size += (count); \
} while (0)
/// Append all elements from one array to the end of another.
#define array_push_all(self, other) \
array_extend((self), (other)->size, (other)->contents)
/// Append `count` elements to the end of the array, reading their values from the
/// `contents` pointer.
#define array_extend(self, count, contents) \
_array__splice( \
(Array *)(self), array_elem_size(self), (self)->size, \
0, count, contents \
)
/// Remove `old_count` elements from the array starting at the given `index`. At
/// the same index, insert `new_count` new elements, reading their values from the
/// `new_contents` pointer.
#define array_splice(self, _index, old_count, new_count, new_contents) \
_array__splice( \
(Array *)(self), array_elem_size(self), _index, \
old_count, new_count, new_contents \
)
/// Insert one `element` into the array at the given `index`.
#define array_insert(self, _index, element) \
_array__splice((Array *)(self), array_elem_size(self), _index, 0, 1, &(element))
/// Remove one element from the array at the given `index`.
#define array_erase(self, _index) \
_array__erase((Array *)(self), array_elem_size(self), _index)
/// Pop the last element off the array, returning the element by value.
#define array_pop(self) ((self)->contents[--(self)->size])
/// Assign the contents of one array to another, reallocating if necessary.
#define array_assign(self, other) \
_array__assign((Array *)(self), (const Array *)(other), array_elem_size(self))
/// Swap one array with another
#define array_swap(self, other) \
_array__swap((Array *)(self), (Array *)(other))
/// Get the size of the array contents
#define array_elem_size(self) (sizeof *(self)->contents)
/// Search a sorted array for a given `needle` value, using the given `compare`
/// callback to determine the order.
///
/// If an existing element is found to be equal to `needle`, then the `index`
/// out-parameter is set to the existing value's index, and the `exists`
/// out-parameter is set to true. Otherwise, `index` is set to an index where
/// `needle` should be inserted in order to preserve the sorting, and `exists`
/// is set to false.
#define array_search_sorted_with(self, compare, needle, _index, _exists) \
_array__search_sorted(self, 0, compare, , needle, _index, _exists)
/// Search a sorted array for a given `needle` value, using integer comparisons
/// of a given struct field (specified with a leading dot) to determine the order.
///
/// See also `array_search_sorted_with`.
#define array_search_sorted_by(self, field, needle, _index, _exists) \
_array__search_sorted(self, 0, _compare_int, field, needle, _index, _exists)
/// Insert a given `value` into a sorted array, using the given `compare`
/// callback to determine the order.
#define array_insert_sorted_with(self, compare, value) \
do { \
unsigned _index, _exists; \
array_search_sorted_with(self, compare, &(value), &_index, &_exists); \
if (!_exists) array_insert(self, _index, value); \
} while (0)
/// Insert a given `value` into a sorted array, using integer comparisons of
/// a given struct field (specified with a leading dot) to determine the order.
///
/// See also `array_search_sorted_by`.
#define array_insert_sorted_by(self, field, value) \
do { \
unsigned _index, _exists; \
array_search_sorted_by(self, field, (value) field, &_index, &_exists); \
if (!_exists) array_insert(self, _index, value); \
} while (0)
// Private
typedef Array(void) Array;
/// This is not what you're looking for, see `array_delete`.
static inline void _array__delete(Array *self) {
if (self->contents) {
ts_free(self->contents);
self->contents = NULL;
self->size = 0;
self->capacity = 0;
}
}
/// This is not what you're looking for, see `array_erase`.
static inline void _array__erase(Array *self, size_t element_size,
uint32_t index) {
assert(index < self->size);
char *contents = (char *)self->contents;
memmove(contents + index * element_size, contents + (index + 1) * element_size,
(self->size - index - 1) * element_size);
self->size--;
}
/// This is not what you're looking for, see `array_reserve`.
static inline void _array__reserve(Array *self, size_t element_size, uint32_t new_capacity) {
if (new_capacity > self->capacity) {
if (self->contents) {
self->contents = ts_realloc(self->contents, new_capacity * element_size);
} else {
self->contents = ts_malloc(new_capacity * element_size);
}
self->capacity = new_capacity;
}
}
/// This is not what you're looking for, see `array_assign`.
static inline void _array__assign(Array *self, const Array *other, size_t element_size) {
_array__reserve(self, element_size, other->size);
self->size = other->size;
memcpy(self->contents, other->contents, self->size * element_size);
}
/// This is not what you're looking for, see `array_swap`.
static inline void _array__swap(Array *self, Array *other) {
Array swap = *other;
*other = *self;
*self = swap;
}
/// This is not what you're looking for, see `array_push` or `array_grow_by`.
static inline void _array__grow(Array *self, uint32_t count, size_t element_size) {
uint32_t new_size = self->size + count;
if (new_size > self->capacity) {
uint32_t new_capacity = self->capacity * 2;
if (new_capacity < 8) new_capacity = 8;
if (new_capacity < new_size) new_capacity = new_size;
_array__reserve(self, element_size, new_capacity);
}
}
/// This is not what you're looking for, see `array_splice`.
static inline void _array__splice(Array *self, size_t element_size,
uint32_t index, uint32_t old_count,
uint32_t new_count, const void *elements) {
uint32_t new_size = self->size + new_count - old_count;
uint32_t old_end = index + old_count;
uint32_t new_end = index + new_count;
assert(old_end <= self->size);
_array__reserve(self, element_size, new_size);
char *contents = (char *)self->contents;
if (self->size > old_end) {
memmove(
contents + new_end * element_size,
contents + old_end * element_size,
(self->size - old_end) * element_size
);
}
if (new_count > 0) {
if (elements) {
memcpy(
(contents + index * element_size),
elements,
new_count * element_size
);
} else {
memset(
(contents + index * element_size),
0,
new_count * element_size
);
}
}
self->size += new_count - old_count;
}
/// A binary search routine, based on Rust's `std::slice::binary_search_by`.
/// This is not what you're looking for, see `array_search_sorted_with` or `array_search_sorted_by`.
#define _array__search_sorted(self, start, compare, suffix, needle, _index, _exists) \
do { \
*(_index) = start; \
*(_exists) = false; \
uint32_t size = (self)->size - *(_index); \
if (size == 0) break; \
int comparison; \
while (size > 1) { \
uint32_t half_size = size / 2; \
uint32_t mid_index = *(_index) + half_size; \
comparison = compare(&((self)->contents[mid_index] suffix), (needle)); \
if (comparison <= 0) *(_index) = mid_index; \
size -= half_size; \
} \
comparison = compare(&((self)->contents[*(_index)] suffix), (needle)); \
if (comparison == 0) *(_exists) = true; \
else if (comparison < 0) *(_index) += 1; \
} while (0)
/// Helper macro for the `_sorted_by` routines below. This takes the left (existing)
/// parameter by reference in order to work with the generic sorting function above.
#define _compare_int(a, b) ((int)*(a) - (int)(b))
#ifdef _MSC_VER
#pragma warning(default : 4101)
#elif defined(__GNUC__) || defined(__clang__)
#pragma GCC diagnostic pop
#endif
#ifdef __cplusplus
}
#endif
#endif // TREE_SITTER_ARRAY_H_

View File

@ -0,0 +1,265 @@
#ifndef TREE_SITTER_PARSER_H_
#define TREE_SITTER_PARSER_H_
#ifdef __cplusplus
extern "C" {
#endif
#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#define ts_builtin_sym_error ((TSSymbol)-1)
#define ts_builtin_sym_end 0
#define TREE_SITTER_SERIALIZATION_BUFFER_SIZE 1024
#ifndef TREE_SITTER_API_H_
typedef uint16_t TSStateId;
typedef uint16_t TSSymbol;
typedef uint16_t TSFieldId;
typedef struct TSLanguage TSLanguage;
#endif
typedef struct {
TSFieldId field_id;
uint8_t child_index;
bool inherited;
} TSFieldMapEntry;
typedef struct {
uint16_t index;
uint16_t length;
} TSFieldMapSlice;
typedef struct {
bool visible;
bool named;
bool supertype;
} TSSymbolMetadata;
typedef struct TSLexer TSLexer;
struct TSLexer {
int32_t lookahead;
TSSymbol result_symbol;
void (*advance)(TSLexer *, bool);
void (*mark_end)(TSLexer *);
uint32_t (*get_column)(TSLexer *);
bool (*is_at_included_range_start)(const TSLexer *);
bool (*eof)(const TSLexer *);
};
typedef enum {
TSParseActionTypeShift,
TSParseActionTypeReduce,
TSParseActionTypeAccept,
TSParseActionTypeRecover,
} TSParseActionType;
typedef union {
struct {
uint8_t type;
TSStateId state;
bool extra;
bool repetition;
} shift;
struct {
uint8_t type;
uint8_t child_count;
TSSymbol symbol;
int16_t dynamic_precedence;
uint16_t production_id;
} reduce;
uint8_t type;
} TSParseAction;
typedef struct {
uint16_t lex_state;
uint16_t external_lex_state;
} TSLexMode;
typedef union {
TSParseAction action;
struct {
uint8_t count;
bool reusable;
} entry;
} TSParseActionEntry;
typedef struct {
int32_t start;
int32_t end;
} TSCharacterRange;
struct TSLanguage {
uint32_t version;
uint32_t symbol_count;
uint32_t alias_count;
uint32_t token_count;
uint32_t external_token_count;
uint32_t state_count;
uint32_t large_state_count;
uint32_t production_id_count;
uint32_t field_count;
uint16_t max_alias_sequence_length;
const uint16_t *parse_table;
const uint16_t *small_parse_table;
const uint32_t *small_parse_table_map;
const TSParseActionEntry *parse_actions;
const char * const *symbol_names;
const char * const *field_names;
const TSFieldMapSlice *field_map_slices;
const TSFieldMapEntry *field_map_entries;
const TSSymbolMetadata *symbol_metadata;
const TSSymbol *public_symbol_map;
const uint16_t *alias_map;
const TSSymbol *alias_sequences;
const TSLexMode *lex_modes;
bool (*lex_fn)(TSLexer *, TSStateId);
bool (*keyword_lex_fn)(TSLexer *, TSStateId);
TSSymbol keyword_capture_token;
struct {
const bool *states;
const TSSymbol *symbol_map;
void *(*create)(void);
void (*destroy)(void *);
bool (*scan)(void *, TSLexer *, const bool *symbol_whitelist);
unsigned (*serialize)(void *, char *);
void (*deserialize)(void *, const char *, unsigned);
} external_scanner;
const TSStateId *primary_state_ids;
};
static inline bool set_contains(TSCharacterRange *ranges, uint32_t len, int32_t lookahead) {
uint32_t index = 0;
uint32_t size = len - index;
while (size > 1) {
uint32_t half_size = size / 2;
uint32_t mid_index = index + half_size;
TSCharacterRange *range = &ranges[mid_index];
if (lookahead >= range->start && lookahead <= range->end) {
return true;
} else if (lookahead > range->end) {
index = mid_index;
}
size -= half_size;
}
TSCharacterRange *range = &ranges[index];
return (lookahead >= range->start && lookahead <= range->end);
}
/*
* Lexer Macros
*/
#ifdef _MSC_VER
#define UNUSED __pragma(warning(suppress : 4101))
#else
#define UNUSED __attribute__((unused))
#endif
#define START_LEXER() \
bool result = false; \
bool skip = false; \
UNUSED \
bool eof = false; \
int32_t lookahead; \
goto start; \
next_state: \
lexer->advance(lexer, skip); \
start: \
skip = false; \
lookahead = lexer->lookahead;
#define ADVANCE(state_value) \
{ \
state = state_value; \
goto next_state; \
}
#define ADVANCE_MAP(...) \
{ \
static const uint16_t map[] = { __VA_ARGS__ }; \
for (uint32_t i = 0; i < sizeof(map) / sizeof(map[0]); i += 2) { \
if (map[i] == lookahead) { \
state = map[i + 1]; \
goto next_state; \
} \
} \
}
#define SKIP(state_value) \
{ \
skip = true; \
state = state_value; \
goto next_state; \
}
#define ACCEPT_TOKEN(symbol_value) \
result = true; \
lexer->result_symbol = symbol_value; \
lexer->mark_end(lexer);
#define END_STATE() return result;
/*
* Parse Table Macros
*/
#define SMALL_STATE(id) ((id) - LARGE_STATE_COUNT)
#define STATE(id) id
#define ACTIONS(id) id
#define SHIFT(state_value) \
{{ \
.shift = { \
.type = TSParseActionTypeShift, \
.state = (state_value) \
} \
}}
#define SHIFT_REPEAT(state_value) \
{{ \
.shift = { \
.type = TSParseActionTypeShift, \
.state = (state_value), \
.repetition = true \
} \
}}
#define SHIFT_EXTRA() \
{{ \
.shift = { \
.type = TSParseActionTypeShift, \
.extra = true \
} \
}}
#define REDUCE(symbol_name, children, precedence, prod_id) \
{{ \
.reduce = { \
.type = TSParseActionTypeReduce, \
.symbol = symbol_name, \
.child_count = children, \
.dynamic_precedence = precedence, \
.production_id = prod_id \
}, \
}}
#define RECOVER() \
{{ \
.type = TSParseActionTypeRecover \
}}
#define ACCEPT_INPUT() \
{{ \
.type = TSParseActionTypeAccept \
}}
#ifdef __cplusplus
}
#endif
#endif // TREE_SITTER_PARSER_H_

View File

@ -0,0 +1,22 @@
# Advanced Decodal example
This example exercises multiple current Decodal features together:
- multi-file `import`
- top-level recursive module scope
- schema/value composition with `&`
- deep patch with `//`
- `default` fallback during materialization
- constrained function parameters
- lazy function arguments
- `match` expressions
- nested dot-path fields
- arrays
Run it with:
```sh
cargo run -q -p decodal -- examples/advanced/main.dcdl
```
The entrypoint is `main.dcdl`.

View File

@ -0,0 +1,47 @@
let
schema = import "./schema.dcdl";
profiles = import "./profiles.dcdl";
mkService = (cfg: schema.Service) =>
cfg // {
summary = match cfg.env {
"prod": "production service";
"dev": "development service";
_: "custom service";
};
};
devService = mkService(
profiles.base & {
name = "api-dev";
port = 8443;
}
);
prodService = mkService(
profiles.prod & {
name = "api";
port = 9443;
}
);
disabledService = mkService(
profiles.disabled & {
name = "api-disabled";
port = 10443;
}
);
in
{
services = [
devService,
prodService,
disabledService,
];
selected = match "prod" {
"prod": prodService;
"dev": devService;
_: disabledService;
};
}

View File

@ -0,0 +1,18 @@
# Environment-specific patches.
base = {
env = "dev";
tags = ["decodal", "dev"];
};
prod = base // {
env = "prod";
host = "api.internal";
feature.limit = 100;
tags = ["decodal", "prod"];
};
disabled = base // {
feature.enable = false;
tags = ["decodal", "disabled"];
};

View File

@ -0,0 +1,17 @@
# Shared service schema.
# Top-level fields are recursive, so SecurePort can refer to Port.
Port = Int & >= 1 & <= 65535;
SecurePort = Port & > 443;
Service = {
name = String;
env = String default "dev";
host = String default "localhost";
port = SecurePort default 8443;
feature = {
enable = Bool default true;
limit = Int & >= 0 default 10;
};
};

13
examples/basic.dcdl Normal file
View File

@ -0,0 +1,13 @@
let
MyConfig = {
host = String;
port = Int & > 443 default 8080;
feature.enable = Bool default true;
};
mkConfig = (cfg: MyConfig) => cfg;
in
mkConfig({
host = "127.0.0.1";
port = 8000;
feature.enable = false;
})

View File

@ -0,0 +1,6 @@
let
schema = import "./schema.dcdl";
in
schema.MyConfig & {
host = "localhost";
}

View File

@ -0,0 +1,4 @@
MyConfig = {
host = String;
port = Int & > 443 default 8080;
};

9
examples/regex/README.md Normal file
View File

@ -0,0 +1,9 @@
# Regex example
Regex constraints are optional. Run this example with the `regex` feature enabled:
```sh
cargo run -q -p decodal --features regex -- examples/regex/main.dcdl
```
Without the feature, regex validation returns an unsupported feature diagnostic.

7
examples/regex/main.dcdl Normal file
View File

@ -0,0 +1,7 @@
let
Identifier = String & /^[A-Za-z_][A-Za-z0-9_]*$/;
ApiName = Identifier & /^api_/;
in
{
service = ApiName default "api_gateway";
}

View File

@ -35,6 +35,11 @@ rustPlatform.buildRustPackage {
strictDeps = true; strictDeps = true;
cargoBuildFlags = [
"-p"
"decodal"
];
doInstallCheck = true; doInstallCheck = true;
installCheckPhase = '' installCheckPhase = ''
runHook preInstallCheck runHook preInstallCheck

View File

@ -1,3 +0,0 @@
fn main() {
println!("Hello, world!");
}