Add unknown ranges and open object values
This commit is contained in:
@@ -9,7 +9,8 @@ It is designed around a lightweight Rust library:
|
||||
- no filesystem access in the library core
|
||||
- concrete and abstract values with constraints and defaults
|
||||
- asymmetric range refinement with `narrower as wider`
|
||||
- homogeneous associative-array schemas with `{...valueSchema}`
|
||||
- homogeneous associative-array schemas with `{...valueSchema}` and object rest constraints
|
||||
- the safe top range `Unknown`, which remains abstract until refined, defaulted, or supplied concretely
|
||||
- deterministic expression evaluation
|
||||
- optional regex support behind a Cargo feature
|
||||
- browser playground support through WebAssembly
|
||||
@@ -26,6 +27,7 @@ decodal = "0.3.0"
|
||||
## Derive support
|
||||
|
||||
For embedded Rust applications, Decodal can generate a schema and typed decoder from a Rust struct with the `derive` feature.
|
||||
Map fields marked with `#[decodal(rest)]` receive additional object fields; `BTreeMap<String, Data>` corresponds to `...Unknown`.
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
@@ -47,7 +49,7 @@ struct Service {
|
||||
|
||||
The derive implements:
|
||||
|
||||
- `DecodalSchema`, which produces a host schema for `Engine::bind_global`
|
||||
- `DecodalSchema`, which produces a `Value` range for `Engine::bind_global`
|
||||
- `DecodalDecode`, which converts materialized `Data` into the Rust struct
|
||||
|
||||
## CLI
|
||||
|
||||
@@ -5,10 +5,16 @@ use std::{
|
||||
};
|
||||
|
||||
use decodal::{
|
||||
Data, Diagnostic, DiagnosticKind, Engine, ImportLoader, LoadedImport, LoadedSource, SourceId,
|
||||
Span, format_diagnostic_with,
|
||||
Data, Diagnostic, DiagnosticKind, Engine, ImportLoader, LoadedImport, SourceId, Span,
|
||||
format_diagnostic_with,
|
||||
};
|
||||
|
||||
struct SourceInput {
|
||||
key: String,
|
||||
name: String,
|
||||
source: String,
|
||||
}
|
||||
|
||||
fn main() -> ExitCode {
|
||||
match run() {
|
||||
Ok(()) => ExitCode::SUCCESS,
|
||||
@@ -69,7 +75,7 @@ fn materialize_path(path: &str) -> Result<Data, String> {
|
||||
.map_err(|error| engine.format_diagnostic(&error))
|
||||
}
|
||||
|
||||
fn read_root_source(path: &str) -> Result<LoadedSource, Diagnostic> {
|
||||
fn read_root_source(path: &str) -> Result<SourceInput, Diagnostic> {
|
||||
if path == "-" {
|
||||
use std::io::Read;
|
||||
let mut source = String::new();
|
||||
@@ -82,7 +88,7 @@ fn read_root_source(path: &str) -> Result<LoadedSource, Diagnostic> {
|
||||
format!("failed to read stdin: {error}"),
|
||||
)
|
||||
})?;
|
||||
Ok(LoadedSource {
|
||||
Ok(SourceInput {
|
||||
key: String::from("<stdin>"),
|
||||
name: String::from("<stdin>"),
|
||||
source,
|
||||
@@ -112,11 +118,11 @@ impl ImportLoader for FsLoader {
|
||||
} else {
|
||||
PathBuf::from(path)
|
||||
};
|
||||
load_path(&path).map(LoadedImport::Source)
|
||||
load_path(&path).map(|source| LoadedImport::source(source.key, source.name, source.source))
|
||||
}
|
||||
}
|
||||
|
||||
fn load_path(path: &Path) -> Result<LoadedSource, Diagnostic> {
|
||||
fn load_path(path: &Path) -> Result<SourceInput, Diagnostic> {
|
||||
let canonical = path.canonicalize().map_err(|error| {
|
||||
Diagnostic::new(
|
||||
DiagnosticKind::Import,
|
||||
@@ -132,7 +138,7 @@ fn load_path(path: &Path) -> Result<LoadedSource, Diagnostic> {
|
||||
)
|
||||
})?;
|
||||
let key = canonical.to_string_lossy().into_owned();
|
||||
Ok(LoadedSource {
|
||||
Ok(SourceInput {
|
||||
name: key.clone(),
|
||||
key,
|
||||
source,
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
use decodal::{Data, EmptyLoader, Engine, HostValue};
|
||||
use decodal::{Data, EmptyLoader, Engine, Value};
|
||||
|
||||
fn main() -> decodal::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)?),
|
||||
Value::object([
|
||||
("name", Value::string_type()),
|
||||
("port", Value::int_type().gt(443).default_int(8443)?),
|
||||
("enabled", Value::bool_type().default_bool(true)?),
|
||||
]),
|
||||
)?;
|
||||
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
use decodal::{
|
||||
Data, Diagnostic, DiagnosticKind, Engine, HostValue, ImportLoader, LoadedImport, Span,
|
||||
};
|
||||
use decodal::{Data, Diagnostic, DiagnosticKind, Engine, ImportLoader, LoadedImport, Span, Value};
|
||||
|
||||
const POST: &str = r#"---
|
||||
title: Hello
|
||||
@@ -33,22 +31,22 @@ impl ImportLoader for ContentLoader {
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_markdown(source: &str) -> decodal::Result<HostValue> {
|
||||
fn parse_markdown(source: &str) -> decodal::Result<Value> {
|
||||
let source = source.strip_prefix("---\n").ok_or_else(frontmatter_error)?;
|
||||
let (frontmatter, body) = source.split_once("\n---\n").ok_or_else(frontmatter_error)?;
|
||||
let mut fields = Vec::new();
|
||||
for line in frontmatter.lines() {
|
||||
let (name, value) = line.split_once(':').ok_or_else(frontmatter_error)?;
|
||||
let value = match value.trim() {
|
||||
"true" => HostValue::bool(true),
|
||||
"false" => HostValue::bool(false),
|
||||
value => HostValue::string(value),
|
||||
"true" => Value::bool(true),
|
||||
"false" => Value::bool(false),
|
||||
value => Value::string(value),
|
||||
};
|
||||
fields.push((name.trim(), value));
|
||||
}
|
||||
Ok(HostValue::object([
|
||||
("frontmatter", HostValue::object(fields)),
|
||||
("body", HostValue::string(body)),
|
||||
Ok(Value::object([
|
||||
("frontmatter", Value::object(fields)),
|
||||
("body", Value::string(body)),
|
||||
]))
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,10 @@ pub struct SpannedExpr {
|
||||
pub enum Expr {
|
||||
Literal(Literal),
|
||||
Ident(String),
|
||||
Object(Vec<Field>),
|
||||
Object {
|
||||
fields: Vec<Field>,
|
||||
rest: Option<ObjectRest>,
|
||||
},
|
||||
Array(Vec<ExprId>),
|
||||
ArrayConstraint {
|
||||
item: ExprId,
|
||||
@@ -116,6 +119,12 @@ pub struct Field {
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ObjectRest {
|
||||
pub value: ExprId,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Param {
|
||||
pub name: String,
|
||||
|
||||
@@ -19,6 +19,10 @@ pub fn normalize_constraints(
|
||||
|
||||
for entry in constraints {
|
||||
match entry.constraint {
|
||||
Constraint::Unknown => rest.push(ConstraintEntry {
|
||||
constraint: Constraint::Unknown,
|
||||
span: entry.span,
|
||||
}),
|
||||
Constraint::Type(next) => match primitive {
|
||||
Some((current, current_span)) if current != next => {
|
||||
return Err(Diagnostic::new(
|
||||
|
||||
+483
-81
@@ -5,14 +5,15 @@ use crate::{
|
||||
ast::{Ast, BinaryOp, CompareOp, Expr, Field, Literal, UnaryOp},
|
||||
constraints::normalize_constraints,
|
||||
diagnostic::{Diagnostic, DiagnosticKind, Result},
|
||||
embedding::HostValue,
|
||||
module::{EmptyLoader, ImportLoader, LoadedImport, LoadedSource, LoadedValue, Module},
|
||||
module::{EmptyLoader, ImportLoader, LoadedImport, Module},
|
||||
parse_source_with_source_id,
|
||||
runtime::{
|
||||
AbstractValue, Binding, ConcreteValue, Constraint, ConstraintEntry, Data, DataField, Env,
|
||||
EnvId, ExprRef, FunctionParam, FunctionValue, LiteralValue, ModuleId, ObjectField,
|
||||
ObjectValue, PrimitiveType, RuntimeValue, Thunk, ThunkId, ThunkKind, ThunkState,
|
||||
ObjectRest as RuntimeObjectRest, ObjectValue, PrimitiveType, RuntimeValue, Thunk, ThunkId,
|
||||
ThunkKind, ThunkState,
|
||||
},
|
||||
value::Value,
|
||||
};
|
||||
|
||||
pub struct Engine<L = EmptyLoader> {
|
||||
@@ -94,8 +95,8 @@ impl<L: ImportLoader> Engine<L> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bind_global(&mut self, name: impl Into<String>, value: HostValue) -> Result<ThunkId> {
|
||||
let value = self.internalize_host_value(value)?;
|
||||
pub fn bind_global(&mut self, name: impl Into<String>, value: Value) -> Result<ThunkId> {
|
||||
let value = self.internalize_value(value)?;
|
||||
let thunk = self.add_value_thunk(value);
|
||||
self.bind(self.prelude_env, name.into(), thunk);
|
||||
Ok(thunk)
|
||||
@@ -280,9 +281,9 @@ impl<L: ImportLoader> Engine<L> {
|
||||
let module = ModuleId(self.modules.len() as u32);
|
||||
let root_env = self.new_env(Some(self.prelude_env));
|
||||
let root_thunk = if source_form == SourceForm::Fields {
|
||||
if let Expr::Object(fields) = ast.get(root).expr.clone() {
|
||||
if let Expr::Object { fields, rest } = ast.get(root).expr.clone() {
|
||||
let object = self
|
||||
.build_object(module, &fields, root_env)
|
||||
.build_object(module, &fields, rest.as_ref(), root_env)
|
||||
.expect("module object construction should not fail for parsed fields");
|
||||
for field in &object.fields {
|
||||
self.bind(root_env, field.name.clone(), field.value);
|
||||
@@ -318,14 +319,14 @@ impl<L: ImportLoader> Engine<L> {
|
||||
let current_key = self.modules[current.0 as usize].key.clone();
|
||||
let loaded = self.loader.load(Some(¤t_key), specifier)?;
|
||||
match loaded {
|
||||
LoadedImport::Source(LoadedSource { key, name, source }) => {
|
||||
LoadedImport::Source { key, name, source } => {
|
||||
if let Some(value) = self.find_imported_value(&key) {
|
||||
return self.force(value);
|
||||
}
|
||||
let module = self.add_source(key, name, &source)?;
|
||||
self.eval_module(module)
|
||||
}
|
||||
LoadedImport::Value(LoadedValue { key, value }) => {
|
||||
LoadedImport::Value { key, value } => {
|
||||
if let Some(module) = self.find_module(&key) {
|
||||
return self.eval_module(module);
|
||||
}
|
||||
@@ -333,7 +334,7 @@ impl<L: ImportLoader> Engine<L> {
|
||||
return self.force(value);
|
||||
}
|
||||
let origin = ImportedValueOrigin::root(key.clone());
|
||||
let value = self.internalize_host_value_with_origin(value, Some(&origin))?;
|
||||
let value = self.internalize_value_with_origin(value, Some(&origin))?;
|
||||
let root = self.add_value_thunk_with_import_origin(value, origin);
|
||||
self.imported_values.push(ImportedValue { key, root });
|
||||
self.force(root)
|
||||
@@ -354,8 +355,8 @@ impl<L: ImportLoader> Engine<L> {
|
||||
match expr {
|
||||
Expr::Literal(literal) => Ok(RuntimeValue::Concrete(literal_to_concrete(literal))),
|
||||
Expr::Ident(name) => self.eval_ident(&name, env, span),
|
||||
Expr::Object(fields) => self
|
||||
.build_object(reference.module, &fields, env)
|
||||
Expr::Object { fields, rest } => self
|
||||
.build_object(reference.module, &fields, rest.as_ref(), env)
|
||||
.map(|object| RuntimeValue::Concrete(ConcreteValue::Object(object))),
|
||||
Expr::Array(items) => {
|
||||
let thunks = items
|
||||
@@ -699,6 +700,15 @@ impl<L: ImportLoader> Engine<L> {
|
||||
}
|
||||
|
||||
fn eval_ident(&mut self, name: &str, env: EnvId, span: Span) -> Result<RuntimeValue> {
|
||||
if name == "Unknown" {
|
||||
return Ok(RuntimeValue::Abstract(AbstractValue {
|
||||
constraints: vec![ConstraintEntry {
|
||||
constraint: Constraint::Unknown,
|
||||
span,
|
||||
}],
|
||||
default: None,
|
||||
}));
|
||||
}
|
||||
if let Some(primitive) = primitive_type(name) {
|
||||
return Ok(RuntimeValue::Abstract(AbstractValue {
|
||||
constraints: vec![ConstraintEntry {
|
||||
@@ -738,9 +748,23 @@ impl<L: ImportLoader> Engine<L> {
|
||||
&mut self,
|
||||
module: ModuleId,
|
||||
fields: &[Field],
|
||||
rest: Option<&crate::ast::ObjectRest>,
|
||||
env: EnvId,
|
||||
) -> Result<ObjectValue> {
|
||||
let mut object = ObjectValue { fields: Vec::new() };
|
||||
let mut object = ObjectValue {
|
||||
fields: Vec::new(),
|
||||
rest: rest.map(|rest| RuntimeObjectRest {
|
||||
value: self.add_expr_thunk_with_span(
|
||||
ExprRef {
|
||||
module,
|
||||
expr: rest.value,
|
||||
},
|
||||
env,
|
||||
rest.span,
|
||||
),
|
||||
span: rest.span,
|
||||
}),
|
||||
};
|
||||
for field in fields {
|
||||
self.insert_field(
|
||||
&mut object,
|
||||
@@ -805,7 +829,10 @@ impl<L: ImportLoader> Engine<L> {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut nested = ObjectValue { fields: Vec::new() };
|
||||
let mut nested = ObjectValue {
|
||||
fields: Vec::new(),
|
||||
rest: None,
|
||||
};
|
||||
self.insert_field(&mut nested, module, &path[1..], expr, env, span)?;
|
||||
object.fields.push(ObjectField {
|
||||
name: name.clone(),
|
||||
@@ -1032,6 +1059,7 @@ impl<L: ImportLoader> Engine<L> {
|
||||
) -> Result<()> {
|
||||
for wider_entry in &wider.constraints {
|
||||
let implied = match &wider_entry.constraint {
|
||||
Constraint::Unknown => true,
|
||||
Constraint::Type(wider_type) => narrower.constraints.iter().any(|entry| {
|
||||
matches!(entry.constraint, Constraint::Type(narrower_type) if narrower_type == *wider_type)
|
||||
}),
|
||||
@@ -1145,11 +1173,20 @@ impl<L: ImportLoader> Engine<L> {
|
||||
));
|
||||
};
|
||||
|
||||
for narrower_field in &narrower.fields {
|
||||
if wider
|
||||
.fields
|
||||
let ObjectValue {
|
||||
fields: narrower_fields,
|
||||
rest: narrower_rest,
|
||||
} = narrower;
|
||||
let ObjectValue {
|
||||
fields: wider_fields,
|
||||
rest: wider_rest,
|
||||
} = wider;
|
||||
|
||||
for narrower_field in &narrower_fields {
|
||||
if wider_fields
|
||||
.iter()
|
||||
.all(|wider_field| wider_field.name != narrower_field.name)
|
||||
&& wider_rest.is_none()
|
||||
{
|
||||
let origin = self.thunk_import_origin(narrower_field.value).cloned();
|
||||
let diagnostic_span = if origin.is_some() {
|
||||
@@ -1172,10 +1209,33 @@ impl<L: ImportLoader> Engine<L> {
|
||||
}
|
||||
}
|
||||
|
||||
let mut refined = ObjectValue { fields: Vec::new() };
|
||||
for wider_field in wider.fields {
|
||||
let Some(narrower_field) = narrower
|
||||
.fields
|
||||
if let Some(narrower_rest) = &narrower_rest
|
||||
&& wider_rest.is_none()
|
||||
{
|
||||
let diagnostic = Diagnostic::new(
|
||||
DiagnosticKind::ConstraintViolation,
|
||||
source_span_or(narrower_rest.span, narrower_diagnostic_span),
|
||||
"left side of `as` permits additional fields but the right side is closed",
|
||||
)
|
||||
.with_label(operation_span, "`as` applied here");
|
||||
let diagnostic = with_source_label(
|
||||
diagnostic,
|
||||
narrower_rest.span,
|
||||
"narrower object rest range declared here",
|
||||
);
|
||||
return Err(with_source_label(
|
||||
diagnostic,
|
||||
wider_span,
|
||||
"closed wider object declared here",
|
||||
));
|
||||
}
|
||||
|
||||
let mut refined = ObjectValue {
|
||||
fields: Vec::new(),
|
||||
rest: None,
|
||||
};
|
||||
for wider_field in wider_fields.iter().cloned() {
|
||||
let Some(narrower_field) = narrower_fields
|
||||
.iter()
|
||||
.find(|narrower_field| narrower_field.name == wider_field.name)
|
||||
else {
|
||||
@@ -1227,6 +1287,82 @@ impl<L: ImportLoader> Engine<L> {
|
||||
span: narrower_field.span,
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(wider_rest) = &wider_rest {
|
||||
let wider_rest_value = self.force(wider_rest.value)?;
|
||||
for narrower_field in narrower_fields.iter().filter(|narrower_field| {
|
||||
wider_fields
|
||||
.iter()
|
||||
.all(|wider_field| wider_field.name != narrower_field.name)
|
||||
}) {
|
||||
let narrower_origin = self.thunk_import_origin(narrower_field.value).cloned();
|
||||
let narrower_value = self.force(narrower_field.value)?;
|
||||
let field_name = narrower_field.name.clone();
|
||||
let narrowed = self
|
||||
.apply_as(
|
||||
narrower_value.clone(),
|
||||
wider_rest_value.clone(),
|
||||
operation_span,
|
||||
narrower_field.span,
|
||||
wider_rest.span,
|
||||
)
|
||||
.map_err(|diagnostic| {
|
||||
let diagnostic = with_source_label(
|
||||
diagnostic,
|
||||
wider_rest.span,
|
||||
"wider object rest range declared here",
|
||||
);
|
||||
if narrower_origin.is_some() {
|
||||
annotate_imported_value(
|
||||
diagnostic,
|
||||
narrower_origin.as_ref(),
|
||||
Some(&narrower_value),
|
||||
)
|
||||
} else {
|
||||
with_source_label(
|
||||
diagnostic,
|
||||
narrower_field.span,
|
||||
format!("additional field `{field_name}` checked here"),
|
||||
)
|
||||
}
|
||||
})?;
|
||||
refined.fields.push(ObjectField {
|
||||
name: field_name,
|
||||
value: self.add_value_thunk_with_span_and_import_origin(
|
||||
narrowed,
|
||||
narrower_field.span,
|
||||
narrower_origin,
|
||||
),
|
||||
span: narrower_field.span,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
refined.rest = match (narrower_rest, wider_rest) {
|
||||
(Some(narrower_rest), Some(wider_rest)) => {
|
||||
let narrower_origin = self.thunk_import_origin(narrower_rest.value).cloned();
|
||||
let narrower_value = self.force(narrower_rest.value)?;
|
||||
let wider_value = self.force(wider_rest.value)?;
|
||||
let narrowed = self.apply_as(
|
||||
narrower_value,
|
||||
wider_value,
|
||||
operation_span,
|
||||
narrower_rest.span,
|
||||
wider_rest.span,
|
||||
)?;
|
||||
Some(RuntimeObjectRest {
|
||||
value: self.add_value_thunk_with_span_and_import_origin(
|
||||
narrowed,
|
||||
narrower_rest.span,
|
||||
narrower_origin,
|
||||
),
|
||||
span: narrower_rest.span,
|
||||
})
|
||||
}
|
||||
(None, Some(wider_rest)) => Some(wider_rest),
|
||||
(None, None) => None,
|
||||
(Some(_), None) => unreachable!("open narrower object was rejected above"),
|
||||
};
|
||||
Ok(RuntimeValue::Concrete(ConcreteValue::Object(refined)))
|
||||
}
|
||||
|
||||
@@ -1321,7 +1457,46 @@ impl<L: ImportLoader> Engine<L> {
|
||||
rhs: ObjectValue,
|
||||
span: Span,
|
||||
) -> Result<RuntimeValue> {
|
||||
for rhs_field in rhs.fields {
|
||||
let lhs_rest = lhs.rest.take();
|
||||
let rhs_rest = rhs.rest;
|
||||
let rhs_field_names = rhs
|
||||
.fields
|
||||
.iter()
|
||||
.map(|field| field.name.clone())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if let Some(rhs_rest) = &rhs_rest {
|
||||
let rhs_rest_value = self.force(rhs_rest.value)?;
|
||||
for lhs_field in lhs
|
||||
.fields
|
||||
.iter_mut()
|
||||
.filter(|field| !rhs_field_names.contains(&field.name))
|
||||
{
|
||||
let lhs_origin = self.thunk_import_origin(lhs_field.value).cloned();
|
||||
let lhs_value = self.force(lhs_field.value)?;
|
||||
let value = self
|
||||
.compose_and(lhs_value.clone(), rhs_rest_value.clone(), span)
|
||||
.map_err(|diagnostic| {
|
||||
let diagnostic = with_source_label(
|
||||
diagnostic,
|
||||
rhs_rest.span,
|
||||
"right object rest constraint declared here",
|
||||
);
|
||||
with_source_label(
|
||||
diagnostic,
|
||||
lhs_field.span,
|
||||
format!("left field `{}` constrained here", lhs_field.name),
|
||||
)
|
||||
})?;
|
||||
lhs_field.value = self.add_value_thunk_with_span_and_import_origin(
|
||||
value,
|
||||
lhs_field.span,
|
||||
lhs_origin,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for mut rhs_field in rhs.fields {
|
||||
if let Some(index) = lhs
|
||||
.fields
|
||||
.iter()
|
||||
@@ -1381,9 +1556,65 @@ impl<L: ImportLoader> Engine<L> {
|
||||
lhs.fields[index].span = rhs_field.span;
|
||||
}
|
||||
} else {
|
||||
if let Some(lhs_rest) = &lhs_rest {
|
||||
let rhs_origin = self.thunk_import_origin(rhs_field.value).cloned();
|
||||
let rhs_value = self.force(rhs_field.value)?;
|
||||
let lhs_rest_value = self.force(lhs_rest.value)?;
|
||||
let value = self.compose_and(rhs_value, lhs_rest_value, span).map_err(
|
||||
|diagnostic| {
|
||||
let diagnostic = with_source_label(
|
||||
diagnostic,
|
||||
lhs_rest.span,
|
||||
"left object rest constraint declared here",
|
||||
);
|
||||
with_source_label(
|
||||
diagnostic,
|
||||
rhs_field.span,
|
||||
format!("right field `{}` constrained here", rhs_field.name),
|
||||
)
|
||||
},
|
||||
)?;
|
||||
rhs_field.value = self.add_value_thunk_with_span_and_import_origin(
|
||||
value,
|
||||
rhs_field.span,
|
||||
rhs_origin,
|
||||
);
|
||||
}
|
||||
lhs.fields.push(rhs_field);
|
||||
}
|
||||
}
|
||||
|
||||
lhs.rest = match (lhs_rest, rhs_rest) {
|
||||
(Some(lhs_rest), Some(rhs_rest)) => {
|
||||
let lhs_origin = self.thunk_import_origin(lhs_rest.value).cloned();
|
||||
let lhs_value = self.force(lhs_rest.value)?;
|
||||
let rhs_value = self.force(rhs_rest.value)?;
|
||||
let value = self
|
||||
.compose_and(lhs_value, rhs_value, span)
|
||||
.map_err(|diagnostic| {
|
||||
let diagnostic = with_source_label(
|
||||
diagnostic,
|
||||
lhs_rest.span,
|
||||
"left object rest constraint declared here",
|
||||
);
|
||||
with_source_label(
|
||||
diagnostic,
|
||||
rhs_rest.span,
|
||||
"right object rest constraint declared here",
|
||||
)
|
||||
})?;
|
||||
Some(RuntimeObjectRest {
|
||||
value: self.add_value_thunk_with_span_and_import_origin(
|
||||
value,
|
||||
rhs_rest.span,
|
||||
lhs_origin,
|
||||
),
|
||||
span: rhs_rest.span,
|
||||
})
|
||||
}
|
||||
(Some(rest), None) | (None, Some(rest)) => Some(rest),
|
||||
(None, None) => None,
|
||||
};
|
||||
Ok(RuntimeValue::Concrete(ConcreteValue::Object(lhs)))
|
||||
}
|
||||
|
||||
@@ -1398,6 +1629,7 @@ impl<L: ImportLoader> Engine<L> {
|
||||
}
|
||||
|
||||
fn patch_objects(&mut self, mut lhs: ObjectValue, rhs: ObjectValue) -> Result<RuntimeValue> {
|
||||
let rhs_rest = rhs.rest;
|
||||
for rhs_field in rhs.fields {
|
||||
if let Some(index) = lhs
|
||||
.fields
|
||||
@@ -1418,6 +1650,9 @@ impl<L: ImportLoader> Engine<L> {
|
||||
lhs.fields.push(rhs_field);
|
||||
}
|
||||
}
|
||||
if rhs_rest.is_some() {
|
||||
lhs.rest = rhs_rest;
|
||||
}
|
||||
Ok(RuntimeValue::Concrete(ConcreteValue::Object(lhs)))
|
||||
}
|
||||
|
||||
@@ -1459,6 +1694,7 @@ impl<L: ImportLoader> Engine<L> {
|
||||
constraint.span
|
||||
};
|
||||
match constraint_value {
|
||||
Constraint::Unknown => Ok(value),
|
||||
Constraint::Type(primitive) => {
|
||||
if value_matches_primitive(&value, *primitive) {
|
||||
Ok(value)
|
||||
@@ -1547,8 +1783,12 @@ impl<L: ImportLoader> Engine<L> {
|
||||
));
|
||||
};
|
||||
let value_schema = self.force(*value_constraint)?;
|
||||
let mut constrained_fields = Vec::with_capacity(object.fields.len());
|
||||
for field in object.fields {
|
||||
let ObjectValue {
|
||||
fields,
|
||||
rest: object_rest,
|
||||
} = object;
|
||||
let mut constrained_fields = Vec::with_capacity(fields.len());
|
||||
for field in fields {
|
||||
let field_span = self.thunk_span(field.value);
|
||||
let field_origin = self.thunk_import_origin(field.value).cloned();
|
||||
let field_value = self.force(field.value)?;
|
||||
@@ -1592,8 +1832,30 @@ impl<L: ImportLoader> Engine<L> {
|
||||
span: field.span,
|
||||
});
|
||||
}
|
||||
let constrained_rest = if let Some(object_rest) = object_rest {
|
||||
let rest_origin = self.thunk_import_origin(object_rest.value).cloned();
|
||||
let rest_value = self.force(object_rest.value)?;
|
||||
let constrained = self.apply_as(
|
||||
rest_value,
|
||||
value_schema,
|
||||
span,
|
||||
object_rest.span,
|
||||
constraint_span,
|
||||
)?;
|
||||
Some(RuntimeObjectRest {
|
||||
value: self.add_value_thunk_with_span_and_import_origin(
|
||||
constrained,
|
||||
object_rest.span,
|
||||
rest_origin,
|
||||
),
|
||||
span: object_rest.span,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok(RuntimeValue::Concrete(ConcreteValue::Object(ObjectValue {
|
||||
fields: constrained_fields,
|
||||
rest: constrained_rest,
|
||||
})))
|
||||
}
|
||||
Constraint::Compare(op, expected) => compare_value(&value, *op, expected)
|
||||
@@ -1671,40 +1933,39 @@ impl<L: ImportLoader> Engine<L> {
|
||||
}
|
||||
}
|
||||
|
||||
fn internalize_host_value(&mut self, value: HostValue) -> Result<RuntimeValue> {
|
||||
self.internalize_host_value_with_origin(value, None)
|
||||
fn internalize_value(&mut self, value: Value) -> Result<RuntimeValue> {
|
||||
self.internalize_value_with_origin(value, None)
|
||||
}
|
||||
|
||||
fn internalize_host_value_with_origin(
|
||||
fn internalize_value_with_origin(
|
||||
&mut self,
|
||||
value: HostValue,
|
||||
value: Value,
|
||||
origin: Option<&ImportedValueOrigin>,
|
||||
) -> Result<RuntimeValue> {
|
||||
match value {
|
||||
HostValue::String(value) => Ok(RuntimeValue::Concrete(ConcreteValue::String(value))),
|
||||
HostValue::Int(value) => Ok(RuntimeValue::Concrete(ConcreteValue::Int(value))),
|
||||
HostValue::Float(value) => Ok(RuntimeValue::Concrete(ConcreteValue::Float(value))),
|
||||
HostValue::Bool(value) => Ok(RuntimeValue::Concrete(ConcreteValue::Bool(value))),
|
||||
HostValue::Array(items) => {
|
||||
Value::String(value) => Ok(RuntimeValue::Concrete(ConcreteValue::String(value))),
|
||||
Value::Int(value) => Ok(RuntimeValue::Concrete(ConcreteValue::Int(value))),
|
||||
Value::Float(value) => Ok(RuntimeValue::Concrete(ConcreteValue::Float(value))),
|
||||
Value::Bool(value) => Ok(RuntimeValue::Concrete(ConcreteValue::Bool(value))),
|
||||
Value::Array(items) => {
|
||||
let mut thunks = Vec::new();
|
||||
for (index, item) in items.into_iter().enumerate() {
|
||||
let item_origin = origin.map(|origin| origin.index(index));
|
||||
let value =
|
||||
self.internalize_host_value_with_origin(item, item_origin.as_ref())?;
|
||||
let value = self.internalize_value_with_origin(item, item_origin.as_ref())?;
|
||||
thunks
|
||||
.push(self.add_value_thunk_with_optional_import_origin(value, item_origin));
|
||||
}
|
||||
Ok(RuntimeValue::Concrete(ConcreteValue::Array(thunks)))
|
||||
}
|
||||
HostValue::ArrayConstraint {
|
||||
Value::ArrayRange {
|
||||
item,
|
||||
mut constraints,
|
||||
default,
|
||||
} => {
|
||||
let item = self.internalize_host_value_with_origin(*item, origin)?;
|
||||
let item = self.internalize_value_with_origin(*item, origin)?;
|
||||
let item = self.add_value_thunk_with_optional_import_origin(item, origin.cloned());
|
||||
let default = if let Some(default) = default {
|
||||
let value = self.internalize_host_value_with_origin(*default, origin)?;
|
||||
let value = self.internalize_value_with_origin(*default, origin)?;
|
||||
Some(self.add_value_thunk_with_optional_import_origin(value, origin.cloned()))
|
||||
} else {
|
||||
None
|
||||
@@ -1723,16 +1984,16 @@ impl<L: ImportLoader> Engine<L> {
|
||||
default,
|
||||
}))
|
||||
}
|
||||
HostValue::MapConstraint {
|
||||
Value::MapRange {
|
||||
value,
|
||||
mut constraints,
|
||||
default,
|
||||
} => {
|
||||
let value = self.internalize_host_value_with_origin(*value, origin)?;
|
||||
let value = self.internalize_value_with_origin(*value, origin)?;
|
||||
let value =
|
||||
self.add_value_thunk_with_optional_import_origin(value, origin.cloned());
|
||||
let default = if let Some(default) = default {
|
||||
let value = self.internalize_host_value_with_origin(*default, origin)?;
|
||||
let value = self.internalize_value_with_origin(*default, origin)?;
|
||||
Some(self.add_value_thunk_with_optional_import_origin(value, origin.cloned()))
|
||||
} else {
|
||||
None
|
||||
@@ -1751,8 +2012,11 @@ impl<L: ImportLoader> Engine<L> {
|
||||
default,
|
||||
}))
|
||||
}
|
||||
HostValue::Object(fields) => {
|
||||
let mut object = ObjectValue { fields: Vec::new() };
|
||||
Value::Object { fields, rest } => {
|
||||
let mut object = ObjectValue {
|
||||
fields: Vec::new(),
|
||||
rest: None,
|
||||
};
|
||||
for field in fields {
|
||||
let field_origin = origin.map(|origin| origin.field(field.name.clone()));
|
||||
if object
|
||||
@@ -1763,7 +2027,7 @@ impl<L: ImportLoader> Engine<L> {
|
||||
let diagnostic = Diagnostic::new(
|
||||
DiagnosticKind::Conflict,
|
||||
Span::default(),
|
||||
format!("duplicate host object field `{}`", field.name),
|
||||
format!("duplicate object field `{}`", field.name),
|
||||
);
|
||||
return Err(annotate_imported_value(
|
||||
diagnostic,
|
||||
@@ -1771,8 +2035,8 @@ impl<L: ImportLoader> Engine<L> {
|
||||
None,
|
||||
));
|
||||
}
|
||||
let value = self
|
||||
.internalize_host_value_with_origin(field.value, field_origin.as_ref())?;
|
||||
let value =
|
||||
self.internalize_value_with_origin(field.value, field_origin.as_ref())?;
|
||||
let value =
|
||||
self.add_value_thunk_with_optional_import_origin(value, field_origin);
|
||||
object.fields.push(ObjectField {
|
||||
@@ -1781,14 +2045,22 @@ impl<L: ImportLoader> Engine<L> {
|
||||
span: Span::default(),
|
||||
});
|
||||
}
|
||||
if let Some(rest) = rest {
|
||||
let rest = self.internalize_value_with_origin(*rest, origin)?;
|
||||
object.rest = Some(RuntimeObjectRest {
|
||||
value: self
|
||||
.add_value_thunk_with_optional_import_origin(rest, origin.cloned()),
|
||||
span: Span::default(),
|
||||
});
|
||||
}
|
||||
Ok(RuntimeValue::Concrete(ConcreteValue::Object(object)))
|
||||
}
|
||||
HostValue::Abstract {
|
||||
Value::Range {
|
||||
constraints,
|
||||
default,
|
||||
} => {
|
||||
let default = if let Some(default) = default {
|
||||
let value = self.internalize_host_value_with_origin(*default, origin)?;
|
||||
let value = self.internalize_value_with_origin(*default, origin)?;
|
||||
Some(self.add_value_thunk_with_optional_import_origin(value, origin.cloned()))
|
||||
} else {
|
||||
None
|
||||
@@ -2252,6 +2524,7 @@ fn upper_bound_implies(
|
||||
|
||||
fn constraint_description(constraint: &Constraint) -> String {
|
||||
match constraint {
|
||||
Constraint::Unknown => String::from("the unknown top range"),
|
||||
Constraint::Type(primitive) => format!("type {primitive:?}"),
|
||||
Constraint::ArrayItems(_) => String::from("an array element range"),
|
||||
Constraint::MapValues(_) => String::from("a map value range"),
|
||||
@@ -2590,7 +2863,7 @@ fn concrete_scalar_eq(lhs: &ConcreteValue, rhs: &ConcreteValue) -> bool {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{LoadedSource, parse_source};
|
||||
use crate::parse_source;
|
||||
|
||||
fn eval_data(source: &str) -> Data {
|
||||
let parsed = parse_source(source).unwrap();
|
||||
@@ -2887,6 +3160,29 @@ mod tests {
|
||||
assert!(value.default.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_is_the_unmaterializable_top_range() {
|
||||
assert_eq!(eval_data("42 as Unknown"), Data::Int(42));
|
||||
assert_eq!(eval_data("Unknown default {}"), Data::Object(Vec::new()));
|
||||
|
||||
let parsed = parse_source("Unknown").unwrap();
|
||||
let mut engine = Engine::from_parse(parsed.ast, parsed.root);
|
||||
let value = engine.eval_root().unwrap();
|
||||
let error = engine.materialize(&value).unwrap_err();
|
||||
assert_eq!(error.kind, DiagnosticKind::Materialize);
|
||||
assert!(error.message.contains("unresolved abstract value"));
|
||||
|
||||
let parsed = parse_source("Unknown as Int").unwrap();
|
||||
let mut engine = Engine::from_parse(parsed.ast, parsed.root);
|
||||
let error = engine.eval_root().unwrap_err();
|
||||
assert!(error.message.contains("not known to be narrower"));
|
||||
|
||||
let parsed = parse_source("{...Unknown}").unwrap();
|
||||
let mut engine = Engine::from_parse(parsed.ast, parsed.root);
|
||||
let value = engine.eval_root().unwrap();
|
||||
assert!(engine.materialize(&value).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn abstract_collection_ranges_can_be_proven_narrower() {
|
||||
let parsed = parse_source("[...(Int & > 10)] as [...Int]").unwrap();
|
||||
@@ -2980,6 +3276,100 @@ mod tests {
|
||||
assert_eq!(eval_data("{} as {...String}"), Data::Object(vec![]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_rest_constraint_accepts_and_preserves_additional_fields() {
|
||||
let data = eval_data(
|
||||
r#"
|
||||
{
|
||||
hoge = 1;
|
||||
fuga = "two";
|
||||
extra = true;
|
||||
} as {
|
||||
hoge = Int;
|
||||
fuga = String;
|
||||
...Unknown
|
||||
}
|
||||
"#,
|
||||
);
|
||||
assert_eq!(
|
||||
data,
|
||||
Data::Object(vec![
|
||||
DataField {
|
||||
name: String::from("hoge"),
|
||||
value: Data::Int(1),
|
||||
},
|
||||
DataField {
|
||||
name: String::from("fuga"),
|
||||
value: Data::String(String::from("two")),
|
||||
},
|
||||
DataField {
|
||||
name: String::from("extra"),
|
||||
value: Data::Bool(true),
|
||||
},
|
||||
])
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
eval_data("{ hoge = 1; ...Unknown }"),
|
||||
Data::Object(vec![DataField {
|
||||
name: String::from("hoge"),
|
||||
value: Data::Int(1),
|
||||
}])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_rest_constraint_rejects_invalid_additional_fields() {
|
||||
let parsed =
|
||||
parse_source(r#"{ hoge = 1; extra = "no"; } as { hoge = Int; ...Int }"#).unwrap();
|
||||
let mut engine = Engine::from_parse(parsed.ast, parsed.root);
|
||||
let error = engine.eval_root().unwrap_err();
|
||||
assert_eq!(error.kind, DiagnosticKind::ConstraintViolation);
|
||||
assert!(error.message.contains("expected Int"));
|
||||
assert!(
|
||||
error
|
||||
.labels
|
||||
.iter()
|
||||
.any(|label| label.message.contains("object rest range"))
|
||||
);
|
||||
|
||||
let parsed = parse_source("{ hoge = 1; ...Unknown } as { hoge = Int; }").unwrap();
|
||||
let mut engine = Engine::from_parse(parsed.ast, parsed.root);
|
||||
let error = engine.eval_root().unwrap_err();
|
||||
assert!(error.message.contains("permits additional fields"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_rest_constraints_compose_as_ranges() {
|
||||
assert_eq!(
|
||||
eval_data("{ fixed = 1; ...Unknown } & { additional = 2; ...Unknown }"),
|
||||
Data::Object(vec![
|
||||
DataField {
|
||||
name: String::from("fixed"),
|
||||
value: Data::Int(1),
|
||||
},
|
||||
DataField {
|
||||
name: String::from("additional"),
|
||||
value: Data::Int(2),
|
||||
},
|
||||
])
|
||||
);
|
||||
|
||||
let parsed =
|
||||
parse_source("{ fixed = Int; ...Int } as { fixed = Unknown; ...Unknown }").unwrap();
|
||||
let mut engine = Engine::from_parse(parsed.ast, parsed.root);
|
||||
assert!(matches!(
|
||||
engine.eval_root().unwrap(),
|
||||
RuntimeValue::Concrete(ConcreteValue::Object(_))
|
||||
));
|
||||
|
||||
let parsed =
|
||||
parse_source("{ fixed = Int; ...Unknown } as { fixed = Unknown; ...Int }").unwrap();
|
||||
let mut engine = Engine::from_parse(parsed.ast, parsed.root);
|
||||
let error = engine.eval_root().unwrap_err();
|
||||
assert!(error.message.contains("not known to be narrower"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn removed_array_primitive_reports_migration() {
|
||||
let parsed = parse_source("Array").unwrap();
|
||||
@@ -3100,11 +3490,11 @@ mod tests {
|
||||
.ok_or_else(|| {
|
||||
Diagnostic::new(DiagnosticKind::Import, Span::default(), "missing source")
|
||||
})?;
|
||||
Ok(LoadedImport::Source(LoadedSource {
|
||||
Ok(LoadedImport::Source {
|
||||
key: specifier.into(),
|
||||
name: specifier.into(),
|
||||
source,
|
||||
}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3147,7 +3537,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_test_markdown(source: &str) -> Result<HostValue> {
|
||||
fn parse_test_markdown(source: &str) -> Result<Value> {
|
||||
let source = source.strip_prefix("---\n").ok_or_else(|| {
|
||||
Diagnostic::new(
|
||||
DiagnosticKind::Import,
|
||||
@@ -3172,15 +3562,15 @@ mod tests {
|
||||
)
|
||||
})?;
|
||||
let value = match value.trim() {
|
||||
"true" => HostValue::bool(true),
|
||||
"false" => HostValue::bool(false),
|
||||
value => HostValue::string(value.trim_matches('"')),
|
||||
"true" => Value::bool(true),
|
||||
"false" => Value::bool(false),
|
||||
value => Value::string(value.trim_matches('"')),
|
||||
};
|
||||
fields.push((name.trim(), value));
|
||||
}
|
||||
Ok(HostValue::object([
|
||||
("frontmatter", HostValue::object(fields)),
|
||||
("body", HostValue::string(body)),
|
||||
Ok(Value::object([
|
||||
("frontmatter", Value::object(fields)),
|
||||
("body", Value::string(body)),
|
||||
]))
|
||||
}
|
||||
|
||||
@@ -3275,9 +3665,9 @@ mod tests {
|
||||
fn load(&mut self, _current_key: Option<&str>, _specifier: &str) -> Result<LoadedImport> {
|
||||
Ok(LoadedImport::value(
|
||||
"content/navigation.json",
|
||||
HostValue::object([(
|
||||
Value::object([(
|
||||
"items",
|
||||
HostValue::array([HostValue::string("home"), HostValue::int(2)]),
|
||||
Value::array([Value::string("home"), Value::int(2)]),
|
||||
)]),
|
||||
))
|
||||
}
|
||||
@@ -3308,7 +3698,7 @@ mod tests {
|
||||
fn load(&mut self, _current_key: Option<&str>, _specifier: &str) -> Result<LoadedImport> {
|
||||
Ok(LoadedImport::value(
|
||||
"shared-value",
|
||||
HostValue::int_type().default_int(1)?,
|
||||
Value::int_type().default_int(1)?,
|
||||
))
|
||||
}
|
||||
}
|
||||
@@ -3351,16 +3741,10 @@ mod tests {
|
||||
engine
|
||||
.bind_global(
|
||||
"Service",
|
||||
HostValue::object([
|
||||
("name", HostValue::string_type()),
|
||||
(
|
||||
"port",
|
||||
HostValue::int_type().gt(443).default_int(8443).unwrap(),
|
||||
),
|
||||
(
|
||||
"enabled",
|
||||
HostValue::bool_type().default_bool(true).unwrap(),
|
||||
),
|
||||
Value::object([
|
||||
("name", Value::string_type()),
|
||||
("port", Value::int_type().gt(443).default_int(8443).unwrap()),
|
||||
("enabled", Value::bool_type().default_bool(true).unwrap()),
|
||||
]),
|
||||
)
|
||||
.unwrap();
|
||||
@@ -3385,12 +3769,9 @@ mod tests {
|
||||
engine
|
||||
.bind_global(
|
||||
"Services",
|
||||
HostValue::map_of(HostValue::object([
|
||||
("port", HostValue::int_type()),
|
||||
(
|
||||
"enabled",
|
||||
HostValue::bool_type().default_bool(true).unwrap(),
|
||||
),
|
||||
Value::map_of(Value::object([
|
||||
("port", Value::int_type()),
|
||||
("enabled", Value::bool_type().default_bool(true).unwrap()),
|
||||
])),
|
||||
)
|
||||
.unwrap();
|
||||
@@ -3410,14 +3791,35 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_schema_errors_point_to_the_dcdl_application() {
|
||||
fn host_prelude_provides_object_rest_and_unknown_ranges() {
|
||||
let mut engine = Engine::new(EmptyLoader);
|
||||
engine
|
||||
.bind_global(
|
||||
"Service",
|
||||
HostValue::object([("port", HostValue::int_type())]),
|
||||
"OpenService",
|
||||
Value::object_with_rest([("name", Value::string_type())], Value::unknown()),
|
||||
)
|
||||
.unwrap();
|
||||
let module = engine
|
||||
.add_root_source(
|
||||
"main",
|
||||
"main",
|
||||
r#"{ name = "api"; metadata = { owner = "platform"; }; } as OpenService"#,
|
||||
)
|
||||
.unwrap();
|
||||
let value = engine.eval_module(module).unwrap();
|
||||
let data = engine.materialize(&value).unwrap();
|
||||
let Data::Object(fields) = data else { panic!() };
|
||||
assert_eq!(fields.len(), 2);
|
||||
assert_eq!(fields[0].value, Data::String(String::from("api")));
|
||||
assert!(matches!(fields[1].value, Data::Object(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_schema_errors_point_to_the_dcdl_application() {
|
||||
let mut engine = Engine::new(EmptyLoader);
|
||||
engine
|
||||
.bind_global("Service", Value::object([("port", Value::int_type())]))
|
||||
.unwrap();
|
||||
let module = engine
|
||||
.add_root_source("main", "main", r#"{ port = "not-an-int"; } as Service"#)
|
||||
.unwrap();
|
||||
@@ -3435,7 +3837,7 @@ mod tests {
|
||||
#[test]
|
||||
fn module_binding_shadows_host_prelude() {
|
||||
let mut engine = Engine::new(EmptyLoader);
|
||||
engine.bind_global("Service", HostValue::int(1)).unwrap();
|
||||
engine.bind_global("Service", Value::int(1)).unwrap();
|
||||
let module = engine
|
||||
.add_root_source("main", "main", r#"Service = "local"; result = Service;"#)
|
||||
.unwrap();
|
||||
|
||||
@@ -5,7 +5,6 @@ extern crate alloc;
|
||||
pub mod ast;
|
||||
pub mod constraints;
|
||||
pub mod diagnostic;
|
||||
pub mod embedding;
|
||||
pub mod environment;
|
||||
pub mod eval;
|
||||
mod lexer;
|
||||
@@ -14,29 +13,28 @@ pub mod parser;
|
||||
pub mod runtime;
|
||||
pub mod span;
|
||||
pub mod typed;
|
||||
pub mod value;
|
||||
|
||||
pub use ast::{Ast, BinaryOp, CompareOp, Expr, ExprId, Field, Literal, Param};
|
||||
pub use ast::{Ast, BinaryOp, CompareOp, Expr, ExprId, Field, Literal, ObjectRest, Param};
|
||||
pub use constraints::normalize_constraints;
|
||||
#[cfg(feature = "derive")]
|
||||
pub use decodal_derive::Decodal;
|
||||
pub use diagnostic::{Diagnostic, DiagnosticKind, Result};
|
||||
pub use embedding::{HostField, HostValue};
|
||||
pub use environment::HostEnvironment;
|
||||
pub use eval::{Engine, format_diagnostic_with};
|
||||
pub use lexer::{
|
||||
Token as SyntaxToken, TokenKind as SyntaxTokenKind, tokenize_source,
|
||||
tokenize_source_with_source_id,
|
||||
};
|
||||
pub use module::{
|
||||
EmptyLoader, ImportCandidate, ImportLoader, LoadedImport, LoadedSource, LoadedValue, Module,
|
||||
};
|
||||
pub use module::{EmptyLoader, ImportCandidate, ImportLoader, LoadedImport, Module};
|
||||
pub use parser::{ParseOutput, Parser, SourceForm, parse_source, parse_source_with_source_id};
|
||||
pub use runtime::{Constraint, Data, ExprRef, LiteralValue, ModuleId, PrimitiveType, RuntimeValue};
|
||||
pub use runtime::{Constraint, Data, ExprRef, LiteralValue, ModuleId, PrimitiveType};
|
||||
pub use span::{SourceId, Span};
|
||||
pub use typed::{
|
||||
DecodalDecode, DecodalSchema, DecodeError, DecodeResult, IntoHostValue, data_at_path,
|
||||
DecodalDecode, DecodalRest, DecodalSchema, DecodeError, DecodeResult, IntoValue, data_at_path,
|
||||
decode_path, prefix_decode_error,
|
||||
};
|
||||
pub use value::{Value, ValueField};
|
||||
|
||||
pub fn version() -> &'static str {
|
||||
env!("CARGO_PKG_VERSION")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use alloc::{string::String, vec::Vec};
|
||||
|
||||
use crate::{
|
||||
Ast, ExprId, HostValue, SourceForm, SourceId,
|
||||
Ast, ExprId, SourceForm, SourceId, Value,
|
||||
runtime::{EnvId, ThunkId},
|
||||
};
|
||||
|
||||
@@ -17,23 +17,18 @@ pub struct Module {
|
||||
pub root_thunk: ThunkId,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LoadedSource {
|
||||
pub key: String,
|
||||
pub name: String,
|
||||
pub source: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LoadedValue {
|
||||
pub key: String,
|
||||
pub value: HostValue,
|
||||
}
|
||||
|
||||
/// Content resolved by an [`ImportLoader`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum LoadedImport {
|
||||
Source(LoadedSource),
|
||||
Value(LoadedValue),
|
||||
Source {
|
||||
key: String,
|
||||
name: String,
|
||||
source: String,
|
||||
},
|
||||
Value {
|
||||
key: String,
|
||||
value: Value,
|
||||
},
|
||||
}
|
||||
|
||||
/// An import specifier offered by host-owned language tooling.
|
||||
@@ -63,18 +58,18 @@ impl LoadedImport {
|
||||
name: impl Into<String>,
|
||||
source: impl Into<String>,
|
||||
) -> Self {
|
||||
Self::Source(LoadedSource {
|
||||
Self::Source {
|
||||
key: key.into(),
|
||||
name: name.into(),
|
||||
source: source.into(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn value(key: impl Into<String>, value: HostValue) -> Self {
|
||||
Self::Value(LoadedValue {
|
||||
pub fn value(key: impl Into<String>, value: Value) -> Self {
|
||||
Self::Value {
|
||||
key: key.into(),
|
||||
value,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,10 @@ use alloc::{string::String, vec::Vec};
|
||||
|
||||
use crate::{
|
||||
SourceId, Span,
|
||||
ast::{Ast, BinaryOp, CompareOp, Expr, ExprId, Field, Literal, MatchArm, Param, UnaryOp},
|
||||
ast::{
|
||||
Ast, BinaryOp, CompareOp, Expr, ExprId, Field, Literal, MatchArm, ObjectRest, Param,
|
||||
UnaryOp,
|
||||
},
|
||||
diagnostic::{Diagnostic, Result},
|
||||
lexer::{Lexer, Token, TokenKind},
|
||||
};
|
||||
@@ -59,7 +62,7 @@ impl Parser {
|
||||
})
|
||||
.unwrap_or_else(|| self.peek().span);
|
||||
(
|
||||
self.ast.push(Expr::Object(fields), span),
|
||||
self.ast.push(Expr::Object { fields, rest: None }, span),
|
||||
SourceForm::Fields,
|
||||
)
|
||||
} else {
|
||||
@@ -338,23 +341,38 @@ impl Parser {
|
||||
|
||||
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())));
|
||||
return Ok(self.ast.push(
|
||||
Expr::Object { fields, rest: None },
|
||||
start_span.join(self.previous_span()),
|
||||
));
|
||||
}
|
||||
let mut rest = None;
|
||||
loop {
|
||||
fields.push(self.parse_field()?);
|
||||
if self.consume_kind(&TokenKind::Semicolon).is_some() {
|
||||
if self.consume_kind(&TokenKind::RBrace).is_some() {
|
||||
break;
|
||||
}
|
||||
if let Some(ellipsis) = self.consume_kind(&TokenKind::Ellipsis) {
|
||||
let value = self.parse_expr(0)?;
|
||||
rest = Some(ObjectRest {
|
||||
value,
|
||||
span: ellipsis.join(self.ast.span(value)),
|
||||
});
|
||||
let _ = self.consume_kind(&TokenKind::Semicolon);
|
||||
self.expect_kind(
|
||||
&TokenKind::RBrace,
|
||||
"expected '}' after object rest constraint",
|
||||
)?;
|
||||
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))
|
||||
Ok(self.ast.push(Expr::Object { fields, rest }, span))
|
||||
}
|
||||
|
||||
fn parse_array_after_lbracket(&mut self, start_span: Span) -> Result<ExprId> {
|
||||
@@ -697,13 +715,16 @@ mod tests {
|
||||
#[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(_)));
|
||||
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 {
|
||||
let Expr::Object { fields, .. } = &parsed.ast.get(parsed.root).expr else {
|
||||
panic!()
|
||||
};
|
||||
assert_eq!(fields[0].path, ["feature", "enable"]);
|
||||
@@ -726,7 +747,7 @@ mod tests {
|
||||
fn parser_accepts_the_public_lossless_token_stream() {
|
||||
let tokens = crate::tokenize_source("value = (# note\n 1);").unwrap();
|
||||
let parsed = Parser::new(tokens).parse().unwrap();
|
||||
let Expr::Object(fields) = &parsed.ast.get(parsed.root).expr else {
|
||||
let Expr::Object { fields, .. } = &parsed.ast.get(parsed.root).expr else {
|
||||
panic!()
|
||||
};
|
||||
assert!(matches!(
|
||||
@@ -749,6 +770,17 @@ mod tests {
|
||||
assert!(matches!(parsed.ast.get(value).expr, Expr::Ident(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_object_rest_constraint_after_named_fields() {
|
||||
let parsed = parse_source("{ hoge = Int; fuga = String; ...Unknown }").unwrap();
|
||||
let Expr::Object { fields, rest } = &parsed.ast.get(parsed.root).expr else {
|
||||
panic!()
|
||||
};
|
||||
assert_eq!(fields.len(), 2);
|
||||
let rest = rest.as_ref().expect("object has a rest constraint");
|
||||
assert!(matches!(parsed.ast.get(rest.value).expr, Expr::Ident(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_as_below_composition() {
|
||||
let parsed = parse_source("value & override as Schema").unwrap();
|
||||
|
||||
@@ -11,6 +11,9 @@ pub struct ExprRef {
|
||||
pub expr: ExprId,
|
||||
}
|
||||
|
||||
/// Evaluator-owned representation of a lazy concrete value or unresolved range.
|
||||
///
|
||||
/// Embedders normally construct [`crate::Value`] and consume [`Data`] instead.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum RuntimeValue {
|
||||
Concrete(ConcreteValue),
|
||||
@@ -31,6 +34,13 @@ pub enum ConcreteValue {
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ObjectValue {
|
||||
pub fields: Vec<ObjectField>,
|
||||
pub rest: Option<ObjectRest>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct ObjectRest {
|
||||
pub value: ThunkId,
|
||||
pub span: Span,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -67,6 +77,7 @@ pub struct ConstraintEntry {
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Constraint {
|
||||
Unknown,
|
||||
Type(PrimitiveType),
|
||||
ArrayItems(ThunkId),
|
||||
MapValues(ThunkId),
|
||||
@@ -91,6 +102,7 @@ pub enum LiteralValue {
|
||||
Bool(bool),
|
||||
}
|
||||
|
||||
/// Fully materialized Decodal data.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Data {
|
||||
String(String),
|
||||
|
||||
@@ -1,17 +1,24 @@
|
||||
use alloc::{collections::BTreeMap, format, string::String, vec::Vec};
|
||||
|
||||
use crate::{Data, HostValue};
|
||||
use crate::{Data, Value, runtime::DataField};
|
||||
|
||||
pub trait DecodalSchema {
|
||||
fn decodal_schema() -> HostValue;
|
||||
fn decodal_schema() -> Value;
|
||||
}
|
||||
|
||||
pub trait DecodalDecode: Sized {
|
||||
fn decodal_decode(data: &Data) -> DecodeResult<Self>;
|
||||
}
|
||||
|
||||
pub trait IntoHostValue {
|
||||
fn into_host_value(self) -> HostValue;
|
||||
pub trait IntoValue {
|
||||
fn into_value(self) -> Value;
|
||||
}
|
||||
|
||||
/// A map-like receiver for fields not named by a derived struct schema.
|
||||
pub trait DecodalRest: Sized {
|
||||
fn decodal_rest_schema() -> Value;
|
||||
|
||||
fn decodal_decode_rest(data: &Data, known_fields: &[&str]) -> DecodeResult<Self>;
|
||||
}
|
||||
|
||||
pub type DecodeResult<T> = core::result::Result<T, DecodeError>;
|
||||
@@ -80,9 +87,38 @@ fn prefix_error(path: &str, mut error: DecodeError) -> DecodeError {
|
||||
error
|
||||
}
|
||||
|
||||
impl DecodalSchema for Data {
|
||||
fn decodal_schema() -> Value {
|
||||
Value::unknown()
|
||||
}
|
||||
}
|
||||
|
||||
impl DecodalDecode for Data {
|
||||
fn decodal_decode(data: &Data) -> DecodeResult<Self> {
|
||||
Ok(data.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoValue for Data {
|
||||
fn into_value(self) -> Value {
|
||||
match self {
|
||||
Data::String(value) => Value::string(value),
|
||||
Data::Int(value) => Value::int(value),
|
||||
Data::Float(value) => Value::float(value),
|
||||
Data::Bool(value) => Value::bool(value),
|
||||
Data::Array(items) => Value::array(items.into_iter().map(IntoValue::into_value)),
|
||||
Data::Object(fields) => Value::object(
|
||||
fields
|
||||
.into_iter()
|
||||
.map(|field| (field.name, field.value.into_value())),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl DecodalSchema for String {
|
||||
fn decodal_schema() -> HostValue {
|
||||
HostValue::string_type()
|
||||
fn decodal_schema() -> Value {
|
||||
Value::string_type()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,15 +131,15 @@ impl DecodalDecode for String {
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoHostValue for String {
|
||||
fn into_host_value(self) -> HostValue {
|
||||
HostValue::string(self)
|
||||
impl IntoValue for String {
|
||||
fn into_value(self) -> Value {
|
||||
Value::string(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoHostValue for &str {
|
||||
fn into_host_value(self) -> HostValue {
|
||||
HostValue::string(self)
|
||||
impl IntoValue for &str {
|
||||
fn into_value(self) -> Value {
|
||||
Value::string(self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,8 +147,8 @@ macro_rules! impl_int_decode {
|
||||
($($ty:ty),* $(,)?) => {
|
||||
$(
|
||||
impl DecodalSchema for $ty {
|
||||
fn decodal_schema() -> HostValue {
|
||||
HostValue::int_type()
|
||||
fn decodal_schema() -> Value {
|
||||
Value::int_type()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,9 +162,9 @@ macro_rules! impl_int_decode {
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoHostValue for $ty {
|
||||
fn into_host_value(self) -> HostValue {
|
||||
HostValue::int(self as i64)
|
||||
impl IntoValue for $ty {
|
||||
fn into_value(self) -> Value {
|
||||
Value::int(self as i64)
|
||||
}
|
||||
}
|
||||
)*
|
||||
@@ -138,8 +174,8 @@ macro_rules! impl_int_decode {
|
||||
impl_int_decode!(i8, i16, i32, i64, u8, u16, u32);
|
||||
|
||||
impl DecodalSchema for f64 {
|
||||
fn decodal_schema() -> HostValue {
|
||||
HostValue::float_type()
|
||||
fn decodal_schema() -> Value {
|
||||
Value::float_type()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,15 +189,15 @@ impl DecodalDecode for f64 {
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoHostValue for f64 {
|
||||
fn into_host_value(self) -> HostValue {
|
||||
HostValue::float(self)
|
||||
impl IntoValue for f64 {
|
||||
fn into_value(self) -> Value {
|
||||
Value::float(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl DecodalSchema for f32 {
|
||||
fn decodal_schema() -> HostValue {
|
||||
HostValue::float_type()
|
||||
fn decodal_schema() -> Value {
|
||||
Value::float_type()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -171,15 +207,15 @@ impl DecodalDecode for f32 {
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoHostValue for f32 {
|
||||
fn into_host_value(self) -> HostValue {
|
||||
HostValue::float(f64::from(self))
|
||||
impl IntoValue for f32 {
|
||||
fn into_value(self) -> Value {
|
||||
Value::float(f64::from(self))
|
||||
}
|
||||
}
|
||||
|
||||
impl DecodalSchema for bool {
|
||||
fn decodal_schema() -> HostValue {
|
||||
HostValue::bool_type()
|
||||
fn decodal_schema() -> Value {
|
||||
Value::bool_type()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -192,15 +228,15 @@ impl DecodalDecode for bool {
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoHostValue for bool {
|
||||
fn into_host_value(self) -> HostValue {
|
||||
HostValue::bool(self)
|
||||
impl IntoValue for bool {
|
||||
fn into_value(self) -> Value {
|
||||
Value::bool(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: DecodalSchema> DecodalSchema for Vec<T> {
|
||||
fn decodal_schema() -> HostValue {
|
||||
HostValue::array_of(T::decodal_schema())
|
||||
fn decodal_schema() -> Value {
|
||||
Value::array_of(T::decodal_schema())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,15 +256,15 @@ impl<T: DecodalDecode> DecodalDecode for Vec<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: IntoHostValue> IntoHostValue for Vec<T> {
|
||||
fn into_host_value(self) -> HostValue {
|
||||
HostValue::array(self.into_iter().map(IntoHostValue::into_host_value))
|
||||
impl<T: IntoValue> IntoValue for Vec<T> {
|
||||
fn into_value(self) -> Value {
|
||||
Value::array(self.into_iter().map(IntoValue::into_value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: DecodalSchema> DecodalSchema for BTreeMap<String, T> {
|
||||
fn decodal_schema() -> HostValue {
|
||||
HostValue::map_of(T::decodal_schema())
|
||||
fn decodal_schema() -> Value {
|
||||
Value::map_of(T::decodal_schema())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,19 +284,36 @@ impl<T: DecodalDecode> DecodalDecode for BTreeMap<String, T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: IntoHostValue> IntoHostValue for BTreeMap<String, T> {
|
||||
fn into_host_value(self) -> HostValue {
|
||||
HostValue::object(
|
||||
impl<T: IntoValue> IntoValue for BTreeMap<String, T> {
|
||||
fn into_value(self) -> Value {
|
||||
Value::object(
|
||||
self.into_iter()
|
||||
.map(|(name, value)| (name, value.into_host_value())),
|
||||
.map(|(name, value)| (name, value.into_value())),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: DecodalSchema + DecodalDecode> DecodalRest for BTreeMap<String, T> {
|
||||
fn decodal_rest_schema() -> Value {
|
||||
T::decodal_schema()
|
||||
}
|
||||
|
||||
fn decodal_decode_rest(data: &Data, known_fields: &[&str]) -> DecodeResult<Self> {
|
||||
let Data::Object(fields) = data else {
|
||||
return Err(DecodeError::at_type("", "Object"));
|
||||
};
|
||||
fields
|
||||
.iter()
|
||||
.filter(|field| !known_fields.contains(&field.name.as_str()))
|
||||
.map(decode_rest_field::<T>)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl<T: DecodalSchema> DecodalSchema for std::collections::HashMap<String, T> {
|
||||
fn decodal_schema() -> HostValue {
|
||||
HostValue::map_of(T::decodal_schema())
|
||||
fn decodal_schema() -> Value {
|
||||
Value::map_of(T::decodal_schema())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,17 +335,41 @@ impl<T: DecodalDecode> DecodalDecode for std::collections::HashMap<String, T> {
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl<T: IntoHostValue> IntoHostValue for std::collections::HashMap<String, T> {
|
||||
fn into_host_value(self) -> HostValue {
|
||||
impl<T: IntoValue> IntoValue for std::collections::HashMap<String, T> {
|
||||
fn into_value(self) -> Value {
|
||||
let mut fields: Vec<_> = self
|
||||
.into_iter()
|
||||
.map(|(name, value)| (name, value.into_host_value()))
|
||||
.map(|(name, value)| (name, value.into_value()))
|
||||
.collect();
|
||||
fields.sort_by(|left, right| left.0.cmp(&right.0));
|
||||
HostValue::object(fields)
|
||||
Value::object(fields)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl<T: DecodalSchema + DecodalDecode> DecodalRest for std::collections::HashMap<String, T> {
|
||||
fn decodal_rest_schema() -> Value {
|
||||
T::decodal_schema()
|
||||
}
|
||||
|
||||
fn decodal_decode_rest(data: &Data, known_fields: &[&str]) -> DecodeResult<Self> {
|
||||
let Data::Object(fields) = data else {
|
||||
return Err(DecodeError::at_type("", "Object"));
|
||||
};
|
||||
fields
|
||||
.iter()
|
||||
.filter(|field| !known_fields.contains(&field.name.as_str()))
|
||||
.map(decode_rest_field::<T>)
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
fn decode_rest_field<T: DecodalDecode>(field: &DataField) -> DecodeResult<(String, T)> {
|
||||
T::decodal_decode(&field.value)
|
||||
.map(|value| (field.name.clone(), value))
|
||||
.map_err(|error| prefix_error(&field.name, error))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -302,10 +379,10 @@ mod tests {
|
||||
fn string_maps_expose_map_schemas_and_decode_objects() {
|
||||
assert!(matches!(
|
||||
BTreeMap::<String, i64>::decodal_schema(),
|
||||
HostValue::MapConstraint { value, .. }
|
||||
Value::MapRange { value, .. }
|
||||
if matches!(
|
||||
*value,
|
||||
HostValue::Abstract { ref constraints, .. }
|
||||
Value::Range { ref constraints, .. }
|
||||
if constraints == &[Constraint::Type(PrimitiveType::Int)]
|
||||
)
|
||||
));
|
||||
@@ -317,4 +394,52 @@ mod tests {
|
||||
let decoded = BTreeMap::<String, i64>::decodal_decode(&data).unwrap();
|
||||
assert_eq!(decoded.get("api"), Some(&8080));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_is_the_unknown_schema_and_round_trips_values() {
|
||||
assert!(matches!(
|
||||
Data::decodal_schema(),
|
||||
Value::Range { ref constraints, .. }
|
||||
if constraints == &[Constraint::Unknown]
|
||||
));
|
||||
|
||||
let data = Data::Object(alloc::vec![DataField {
|
||||
name: String::from("nested"),
|
||||
value: Data::Array(alloc::vec![Data::Bool(true), Data::Int(2)]),
|
||||
}]);
|
||||
assert_eq!(Data::decodal_decode(&data).unwrap(), data);
|
||||
assert!(matches!(data.into_value(), Value::Object { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rest_maps_decode_only_fields_outside_the_known_domain() {
|
||||
let data = Data::Object(alloc::vec![
|
||||
DataField {
|
||||
name: String::from("name"),
|
||||
value: Data::String(String::from("api")),
|
||||
},
|
||||
DataField {
|
||||
name: String::from("priority"),
|
||||
value: Data::Int(3),
|
||||
},
|
||||
]);
|
||||
let decoded = BTreeMap::<String, Data>::decodal_decode_rest(&data, &["name"]).unwrap();
|
||||
assert_eq!(decoded.len(), 1);
|
||||
assert_eq!(decoded.get("priority"), Some(&Data::Int(3)));
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
#[test]
|
||||
fn hash_maps_can_receive_rest_fields() {
|
||||
let data = Data::Object(alloc::vec![DataField {
|
||||
name: String::from("region"),
|
||||
value: Data::String(String::from("ap-northeast-1")),
|
||||
}]);
|
||||
let decoded =
|
||||
std::collections::HashMap::<String, String>::decodal_decode_rest(&data, &[]).unwrap();
|
||||
assert_eq!(
|
||||
decoded.get("region").map(String::as_str),
|
||||
Some("ap-northeast-1")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,37 +3,46 @@ use alloc::{boxed::Box, string::String, vec::Vec};
|
||||
use crate::runtime::{Constraint, LiteralValue, PrimitiveType};
|
||||
use crate::{CompareOp, Diagnostic, DiagnosticKind, Result, Span};
|
||||
|
||||
/// A Decodal value supplied through the embedding API.
|
||||
///
|
||||
/// Unlike [`crate::Data`], a `Value` may contain unresolved ranges and
|
||||
/// defaults. The evaluator converts it into its lazy runtime representation
|
||||
/// when it is bound as a global or returned from an import loader.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum HostValue {
|
||||
pub enum Value {
|
||||
String(String),
|
||||
Int(i64),
|
||||
Float(f64),
|
||||
Bool(bool),
|
||||
Array(Vec<HostValue>),
|
||||
ArrayConstraint {
|
||||
item: Box<HostValue>,
|
||||
Array(Vec<Value>),
|
||||
ArrayRange {
|
||||
item: Box<Value>,
|
||||
constraints: Vec<Constraint>,
|
||||
default: Option<Box<HostValue>>,
|
||||
default: Option<Box<Value>>,
|
||||
},
|
||||
MapConstraint {
|
||||
value: Box<HostValue>,
|
||||
MapRange {
|
||||
value: Box<Value>,
|
||||
constraints: Vec<Constraint>,
|
||||
default: Option<Box<HostValue>>,
|
||||
default: Option<Box<Value>>,
|
||||
},
|
||||
Object(Vec<HostField>),
|
||||
Abstract {
|
||||
Object {
|
||||
fields: Vec<ValueField>,
|
||||
rest: Option<Box<Value>>,
|
||||
},
|
||||
Range {
|
||||
constraints: Vec<Constraint>,
|
||||
default: Option<Box<HostValue>>,
|
||||
default: Option<Box<Value>>,
|
||||
},
|
||||
}
|
||||
|
||||
/// A named field in a [`Value::Object`].
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct HostField {
|
||||
pub struct ValueField {
|
||||
pub name: String,
|
||||
pub value: HostValue,
|
||||
pub value: Value,
|
||||
}
|
||||
|
||||
impl HostValue {
|
||||
impl Value {
|
||||
pub fn string(value: impl Into<String>) -> Self {
|
||||
Self::String(value.into())
|
||||
}
|
||||
@@ -52,65 +61,105 @@ impl HostValue {
|
||||
|
||||
pub fn array<I>(items: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = HostValue>,
|
||||
I: IntoIterator<Item = Value>,
|
||||
{
|
||||
Self::Array(items.into_iter().collect())
|
||||
}
|
||||
|
||||
pub fn object<I, N>(fields: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = (N, HostValue)>,
|
||||
I: IntoIterator<Item = (N, Value)>,
|
||||
N: Into<String>,
|
||||
{
|
||||
Self::Object(
|
||||
fields
|
||||
Self::Object {
|
||||
fields: fields
|
||||
.into_iter()
|
||||
.map(|(name, value)| HostField {
|
||||
.map(|(name, value)| ValueField {
|
||||
name: name.into(),
|
||||
value,
|
||||
})
|
||||
.collect(),
|
||||
)
|
||||
rest: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn object_from_paths<I, N>(fields: I) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = (N, HostValue)>,
|
||||
I: IntoIterator<Item = (N, Value)>,
|
||||
N: Into<String>,
|
||||
{
|
||||
let mut root = Vec::new();
|
||||
for (path, value) in fields {
|
||||
insert_path(&mut root, &path.into(), value);
|
||||
}
|
||||
Self::Object(root)
|
||||
Self::Object {
|
||||
fields: root,
|
||||
rest: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn object_from_paths_with_rest<I, N>(fields: I, rest: Value) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = (N, Value)>,
|
||||
N: Into<String>,
|
||||
{
|
||||
let mut root = Vec::new();
|
||||
for (path, value) in fields {
|
||||
insert_path(&mut root, &path.into(), value);
|
||||
}
|
||||
Self::Object {
|
||||
fields: root,
|
||||
rest: Some(Box::new(rest)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn object_with_rest<I, N>(fields: I, rest: Value) -> Self
|
||||
where
|
||||
I: IntoIterator<Item = (N, Value)>,
|
||||
N: Into<String>,
|
||||
{
|
||||
Self::Object {
|
||||
fields: fields
|
||||
.into_iter()
|
||||
.map(|(name, value)| ValueField {
|
||||
name: name.into(),
|
||||
value,
|
||||
})
|
||||
.collect(),
|
||||
rest: Some(Box::new(rest)),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unknown() -> Self {
|
||||
Self::range_with_constraint(Constraint::Unknown)
|
||||
}
|
||||
|
||||
pub fn string_type() -> Self {
|
||||
Self::abstract_with_constraint(Constraint::Type(PrimitiveType::String))
|
||||
Self::range_with_constraint(Constraint::Type(PrimitiveType::String))
|
||||
}
|
||||
|
||||
pub fn int_type() -> Self {
|
||||
Self::abstract_with_constraint(Constraint::Type(PrimitiveType::Int))
|
||||
Self::range_with_constraint(Constraint::Type(PrimitiveType::Int))
|
||||
}
|
||||
|
||||
pub fn float_type() -> Self {
|
||||
Self::abstract_with_constraint(Constraint::Type(PrimitiveType::Float))
|
||||
Self::range_with_constraint(Constraint::Type(PrimitiveType::Float))
|
||||
}
|
||||
|
||||
pub fn bool_type() -> Self {
|
||||
Self::abstract_with_constraint(Constraint::Type(PrimitiveType::Bool))
|
||||
Self::range_with_constraint(Constraint::Type(PrimitiveType::Bool))
|
||||
}
|
||||
|
||||
pub fn array_of(item: HostValue) -> Self {
|
||||
Self::ArrayConstraint {
|
||||
pub fn array_of(item: Value) -> Self {
|
||||
Self::ArrayRange {
|
||||
item: Box::new(item),
|
||||
constraints: Vec::new(),
|
||||
default: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn map_of(value: HostValue) -> Self {
|
||||
Self::MapConstraint {
|
||||
pub fn map_of(value: Value) -> Self {
|
||||
Self::MapRange {
|
||||
value: Box::new(value),
|
||||
constraints: Vec::new(),
|
||||
default: None,
|
||||
@@ -118,11 +167,11 @@ impl HostValue {
|
||||
}
|
||||
|
||||
pub fn builtin_predicate(name: impl Into<String>) -> Self {
|
||||
Self::abstract_with_constraint(Constraint::BuiltinPredicate(name.into()))
|
||||
Self::range_with_constraint(Constraint::BuiltinPredicate(name.into()))
|
||||
}
|
||||
|
||||
pub fn abstract_with_constraint(constraint: Constraint) -> Self {
|
||||
Self::Abstract {
|
||||
pub fn range_with_constraint(constraint: Constraint) -> Self {
|
||||
Self::Range {
|
||||
constraints: alloc::vec![constraint],
|
||||
default: None,
|
||||
}
|
||||
@@ -130,12 +179,12 @@ impl HostValue {
|
||||
|
||||
pub fn with_constraint(mut self, constraint: Constraint) -> Self {
|
||||
match &mut self {
|
||||
Self::Abstract { constraints, .. } => constraints.push(constraint),
|
||||
Self::ArrayConstraint { constraints, .. } | Self::MapConstraint { constraints, .. } => {
|
||||
Self::Range { constraints, .. } => constraints.push(constraint),
|
||||
Self::ArrayRange { constraints, .. } | Self::MapRange { constraints, .. } => {
|
||||
constraints.push(constraint)
|
||||
}
|
||||
_ => {
|
||||
self = Self::Abstract {
|
||||
self = Self::Range {
|
||||
constraints: alloc::vec![constraint],
|
||||
default: Some(Box::new(self)),
|
||||
};
|
||||
@@ -166,53 +215,53 @@ impl HostValue {
|
||||
))
|
||||
}
|
||||
|
||||
pub fn default(self, value: HostValue) -> Result<Self> {
|
||||
pub fn default(self, value: Value) -> Result<Self> {
|
||||
match self {
|
||||
Self::ArrayConstraint {
|
||||
Self::ArrayRange {
|
||||
item,
|
||||
constraints,
|
||||
default: None,
|
||||
} => Ok(Self::ArrayConstraint {
|
||||
} => Ok(Self::ArrayRange {
|
||||
item,
|
||||
constraints,
|
||||
default: Some(Box::new(value)),
|
||||
}),
|
||||
Self::ArrayConstraint {
|
||||
Self::ArrayRange {
|
||||
default: Some(_), ..
|
||||
} => Err(Diagnostic::new(
|
||||
DiagnosticKind::DefaultConflict,
|
||||
Span::default(),
|
||||
"host value already has a default",
|
||||
"value already has a default",
|
||||
)),
|
||||
Self::MapConstraint {
|
||||
Self::MapRange {
|
||||
value: map_value,
|
||||
constraints,
|
||||
default: None,
|
||||
} => Ok(Self::MapConstraint {
|
||||
} => Ok(Self::MapRange {
|
||||
value: map_value,
|
||||
constraints,
|
||||
default: Some(Box::new(value)),
|
||||
}),
|
||||
Self::MapConstraint {
|
||||
Self::MapRange {
|
||||
default: Some(_), ..
|
||||
} => Err(Diagnostic::new(
|
||||
DiagnosticKind::DefaultConflict,
|
||||
Span::default(),
|
||||
"host value already has a default",
|
||||
"value already has a default",
|
||||
)),
|
||||
Self::Abstract {
|
||||
Self::Range {
|
||||
constraints,
|
||||
default: None,
|
||||
} => Ok(Self::Abstract {
|
||||
} => Ok(Self::Range {
|
||||
constraints,
|
||||
default: Some(Box::new(value)),
|
||||
}),
|
||||
Self::Abstract { .. } => Err(Diagnostic::new(
|
||||
Self::Range { .. } => Err(Diagnostic::new(
|
||||
DiagnosticKind::DefaultConflict,
|
||||
Span::default(),
|
||||
"host value already has a default",
|
||||
"value already has a default",
|
||||
)),
|
||||
concrete => Ok(Self::Abstract {
|
||||
concrete => Ok(Self::Range {
|
||||
constraints: Vec::new(),
|
||||
default: Some(Box::new(concrete)),
|
||||
}),
|
||||
@@ -236,8 +285,8 @@ impl HostValue {
|
||||
}
|
||||
}
|
||||
|
||||
impl HostField {
|
||||
pub fn new(name: impl Into<String>, value: HostValue) -> Self {
|
||||
impl ValueField {
|
||||
pub fn new(name: impl Into<String>, value: Value) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
value,
|
||||
@@ -245,28 +294,40 @@ impl HostField {
|
||||
}
|
||||
}
|
||||
|
||||
fn insert_path(fields: &mut Vec<HostField>, path: &str, value: HostValue) {
|
||||
fn insert_path(fields: &mut Vec<ValueField>, path: &str, value: Value) {
|
||||
let mut parts = path.splitn(2, '.');
|
||||
let Some(head) = parts.next().filter(|part| !part.is_empty()) else {
|
||||
return;
|
||||
};
|
||||
if let Some(tail) = parts.next() {
|
||||
if let Some(field) = fields.iter_mut().find(|field| field.name == head) {
|
||||
if let HostValue::Object(children) = &mut field.value {
|
||||
if let Value::Object {
|
||||
fields: children, ..
|
||||
} = &mut field.value
|
||||
{
|
||||
insert_path(children, tail, value);
|
||||
} else {
|
||||
let mut children = Vec::new();
|
||||
insert_path(&mut children, tail, value);
|
||||
field.value = HostValue::Object(children);
|
||||
field.value = Value::Object {
|
||||
fields: children,
|
||||
rest: None,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
let mut children = Vec::new();
|
||||
insert_path(&mut children, tail, value);
|
||||
fields.push(HostField::new(head, HostValue::Object(children)));
|
||||
fields.push(ValueField::new(
|
||||
head,
|
||||
Value::Object {
|
||||
fields: children,
|
||||
rest: None,
|
||||
},
|
||||
));
|
||||
}
|
||||
} else if let Some(field) = fields.iter_mut().find(|field| field.name == head) {
|
||||
field.value = value;
|
||||
} else {
|
||||
fields.push(HostField::new(head, value));
|
||||
fields.push(ValueField::new(head, value));
|
||||
}
|
||||
}
|
||||
@@ -35,12 +35,38 @@ fn expand_decodal(input: DeriveInput) -> Result<TokenStream2> {
|
||||
|
||||
let mut schema_fields = Vec::new();
|
||||
let mut decode_fields = Vec::new();
|
||||
let mut known_field_roots = Vec::new();
|
||||
let mut rest_field = None;
|
||||
|
||||
for field in fields {
|
||||
let ident = field.ident.expect("named field");
|
||||
let ty = field.ty;
|
||||
let attrs = FieldAttrs::from_attrs(&field.attrs, &ident.to_string())?;
|
||||
|
||||
if attrs.rest {
|
||||
if attrs.rename_explicit || attrs.default.is_some() || !attrs.constraints.is_empty() {
|
||||
return Err(syn::Error::new(
|
||||
ident.span(),
|
||||
"`rest` cannot be combined with rename, default, or field constraints",
|
||||
));
|
||||
}
|
||||
if rest_field.is_some() {
|
||||
return Err(syn::Error::new(
|
||||
ident.span(),
|
||||
"Decodal derive supports only one `rest` field",
|
||||
));
|
||||
}
|
||||
rest_field = Some((ident, ty));
|
||||
continue;
|
||||
}
|
||||
|
||||
let path = attrs.rename.clone();
|
||||
known_field_roots.push(
|
||||
path.split('.')
|
||||
.next()
|
||||
.expect("field paths are non-empty")
|
||||
.to_owned(),
|
||||
);
|
||||
let schema_value = schema_expr(&ty, &attrs)?;
|
||||
let decode_value = decode_expr(&ty, &path, &attrs);
|
||||
|
||||
@@ -52,14 +78,40 @@ fn expand_decodal(input: DeriveInput) -> Result<TokenStream2> {
|
||||
});
|
||||
}
|
||||
|
||||
if let Some((ident, ty)) = &rest_field {
|
||||
decode_fields.push(quote! {
|
||||
#ident: <#ty as ::decodal::DecodalRest>::decodal_decode_rest(
|
||||
data,
|
||||
&[#(#known_field_roots),*],
|
||||
)?
|
||||
});
|
||||
}
|
||||
|
||||
let schema_entries = if schema_fields.is_empty() {
|
||||
quote! { ::core::iter::empty::<(&'static str, ::decodal::Value)>() }
|
||||
} else {
|
||||
quote! { [#(#schema_fields),*] }
|
||||
};
|
||||
|
||||
let build_schema = if let Some((_, ty)) = &rest_field {
|
||||
quote! {
|
||||
::decodal::Value::object_from_paths_with_rest(
|
||||
#schema_entries,
|
||||
<#ty as ::decodal::DecodalRest>::decodal_rest_schema(),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
quote! {
|
||||
::decodal::Value::object_from_paths(#schema_entries)
|
||||
}
|
||||
};
|
||||
|
||||
let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
|
||||
|
||||
Ok(quote! {
|
||||
impl #impl_generics ::decodal::DecodalSchema for #name #ty_generics #where_clause {
|
||||
fn decodal_schema() -> ::decodal::HostValue {
|
||||
::decodal::HostValue::object_from_paths([
|
||||
#(#schema_fields),*
|
||||
])
|
||||
fn decodal_schema() -> ::decodal::Value {
|
||||
#build_schema
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +128,8 @@ fn expand_decodal(input: DeriveInput) -> Result<TokenStream2> {
|
||||
#[derive(Default)]
|
||||
struct FieldAttrs {
|
||||
rename: String,
|
||||
rename_explicit: bool,
|
||||
rest: bool,
|
||||
default: Option<DefaultAttr>,
|
||||
constraints: Vec<ConstraintAttr>,
|
||||
}
|
||||
@@ -95,6 +149,14 @@ impl FieldAttrs {
|
||||
if meta.path.is_ident("rename") {
|
||||
let value: LitStr = meta.value()?.parse()?;
|
||||
output.rename = value.value();
|
||||
output.rename_explicit = true;
|
||||
return Ok(());
|
||||
}
|
||||
if meta.path.is_ident("rest") {
|
||||
if meta.input.peek(syn::Token![=]) || meta.input.peek(syn::token::Paren) {
|
||||
return Err(meta.error("`rest` does not accept a value"));
|
||||
}
|
||||
output.rest = true;
|
||||
return Ok(());
|
||||
}
|
||||
if meta.path.is_ident("default") {
|
||||
@@ -182,7 +244,7 @@ fn schema_expr(ty: &Type, attrs: &FieldAttrs) -> Result<TokenStream2> {
|
||||
};
|
||||
tokens.extend(quote! {
|
||||
schema = schema
|
||||
.default(::decodal::IntoHostValue::into_host_value(#default_expr))
|
||||
.default(::decodal::IntoValue::into_value(#default_expr))
|
||||
.expect("Decodal derive generated a valid default");
|
||||
});
|
||||
}
|
||||
@@ -210,3 +272,42 @@ fn decode_expr(ty: &Type, path: &str, attrs: &FieldAttrs) -> TokenStream2 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn rejects_multiple_rest_fields() {
|
||||
let input: DeriveInput = syn::parse_quote! {
|
||||
struct Invalid {
|
||||
#[decodal(rest)]
|
||||
first: std::collections::BTreeMap<String, String>,
|
||||
#[decodal(rest)]
|
||||
second: std::collections::BTreeMap<String, String>,
|
||||
}
|
||||
};
|
||||
assert!(
|
||||
expand_decodal(input)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("only one `rest` field")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_rest_field_modifiers() {
|
||||
let input: DeriveInput = syn::parse_quote! {
|
||||
struct Invalid {
|
||||
#[decodal(rest, default)]
|
||||
extra: std::collections::BTreeMap<String, String>,
|
||||
}
|
||||
};
|
||||
assert!(
|
||||
expand_decodal(input)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("cannot be combined")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
use decodal::{Data, DecodalDecode, DecodalSchema, EmptyLoader, Engine};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use decodal::{Constraint, Data, DecodalDecode, DecodalSchema, EmptyLoader, Engine, Value};
|
||||
use decodal_derive::Decodal;
|
||||
|
||||
#[derive(Debug, PartialEq, Decodal)]
|
||||
@@ -24,6 +26,28 @@ struct Fleet {
|
||||
workers: Vec<Worker>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Decodal)]
|
||||
struct OpenConfig {
|
||||
name: String,
|
||||
#[decodal(default = true, rename = "feature.enabled")]
|
||||
enabled: bool,
|
||||
#[decodal(rest)]
|
||||
extra: BTreeMap<String, Data>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Decodal)]
|
||||
struct StringLabels {
|
||||
name: String,
|
||||
#[decodal(rest)]
|
||||
labels: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Decodal)]
|
||||
struct OpenBag {
|
||||
#[decodal(rest)]
|
||||
values: BTreeMap<String, Data>,
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derives_schema_and_decode() {
|
||||
let mut engine = Engine::new(EmptyLoader);
|
||||
@@ -148,3 +172,97 @@ fn vec_schema_applies_nested_struct_defaults() {
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rest_map_opens_the_derived_object_and_collects_additional_fields() {
|
||||
let schema = OpenConfig::decodal_schema();
|
||||
let Value::Object {
|
||||
fields,
|
||||
rest: Some(rest),
|
||||
} = schema
|
||||
else {
|
||||
panic!("derived schema should have an object rest range")
|
||||
};
|
||||
assert_eq!(fields.len(), 2);
|
||||
assert!(matches!(
|
||||
*rest,
|
||||
Value::Range { ref constraints, .. }
|
||||
if constraints == &[Constraint::Unknown]
|
||||
));
|
||||
|
||||
let mut engine = Engine::new(EmptyLoader);
|
||||
engine
|
||||
.bind_global("OpenConfig", OpenConfig::decodal_schema())
|
||||
.unwrap();
|
||||
let module = engine
|
||||
.add_root_source(
|
||||
"test",
|
||||
"test",
|
||||
r#"
|
||||
{
|
||||
name = "api";
|
||||
plugin = { name = "cache"; };
|
||||
retries = 3;
|
||||
} as OpenConfig
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let value = engine.eval_module(module).unwrap();
|
||||
let data = engine.materialize(&value).unwrap();
|
||||
let config = OpenConfig::decodal_decode(&data).unwrap();
|
||||
|
||||
assert_eq!(config.name, "api");
|
||||
assert!(config.enabled);
|
||||
assert_eq!(config.extra.get("retries"), Some(&Data::Int(3)));
|
||||
assert!(matches!(config.extra.get("plugin"), Some(Data::Object(_))));
|
||||
assert!(!config.extra.contains_key("name"));
|
||||
assert!(!config.extra.contains_key("feature"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn typed_rest_map_constrains_each_additional_field() {
|
||||
let mut engine = Engine::new(EmptyLoader);
|
||||
engine
|
||||
.bind_global("StringLabels", StringLabels::decodal_schema())
|
||||
.unwrap();
|
||||
let module = engine
|
||||
.add_root_source(
|
||||
"test",
|
||||
"test",
|
||||
r#"{ name = "service"; region = "ap-northeast-1"; tier = "edge"; } as StringLabels"#,
|
||||
)
|
||||
.unwrap();
|
||||
let value = engine.eval_module(module).unwrap();
|
||||
let data = engine.materialize(&value).unwrap();
|
||||
let labels = StringLabels::decodal_decode(&data).unwrap();
|
||||
assert_eq!(labels.labels.get("region"), Some(&"ap-northeast-1".into()));
|
||||
assert_eq!(labels.labels.get("tier"), Some(&"edge".into()));
|
||||
|
||||
let module = engine
|
||||
.add_root_source(
|
||||
"invalid",
|
||||
"invalid",
|
||||
r#"{ name = "service"; priority = 1; } as StringLabels"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(engine.eval_module(module).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rest_only_struct_collects_every_field() {
|
||||
let schema = OpenBag::decodal_schema();
|
||||
assert!(matches!(
|
||||
schema,
|
||||
Value::Object {
|
||||
ref fields,
|
||||
rest: Some(_),
|
||||
} if fields.is_empty()
|
||||
));
|
||||
|
||||
let data = Data::Object(vec![decodal::runtime::DataField {
|
||||
name: "answer".into(),
|
||||
value: Data::Int(42),
|
||||
}]);
|
||||
let bag = OpenBag::decodal_decode(&data).unwrap();
|
||||
assert_eq!(bag.values.get("answer"), Some(&Data::Int(42)));
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
|
||||
use decodal::{
|
||||
Engine, HostEnvironment, HostValue, ImportLoader, LoadedImport, Result, RuntimeValue,
|
||||
Engine, HostEnvironment, ImportLoader, LoadedImport, Result, Value, runtime::RuntimeValue,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@@ -116,12 +116,12 @@ pub(crate) fn complete<E: HostEnvironment>(
|
||||
let mut loader = environment.create_loader();
|
||||
let loaded = loader.load(Some(key), specifier)?;
|
||||
detail = match &loaded {
|
||||
LoadedImport::Source(source) => source.name.clone(),
|
||||
LoadedImport::Value(value) => value.key.clone(),
|
||||
LoadedImport::Source { name, .. } => name.clone(),
|
||||
LoadedImport::Value { key, .. } => key.clone(),
|
||||
};
|
||||
fields = match loaded {
|
||||
LoadedImport::Source(source) => collect_fields(&tokenize(&source.source)),
|
||||
LoadedImport::Value(value) => fields_from_host_value(&value.value),
|
||||
LoadedImport::Source { source, .. } => collect_fields(&tokenize(&source)),
|
||||
LoadedImport::Value { value, .. } => fields_from_value(&value),
|
||||
};
|
||||
} else if let Some(local) = local_fields.get(root) {
|
||||
fields = local.clone();
|
||||
@@ -229,6 +229,7 @@ fn builtin_items() -> Vec<CompletionItem> {
|
||||
("Int", CompletionKind::Type, "integer constraint", 5),
|
||||
("Float", CompletionKind::Type, "float constraint", 5),
|
||||
("Bool", CompletionKind::Type, "boolean constraint", 5),
|
||||
("Unknown", CompletionKind::Type, "unknown top range", 5),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(label, kind, detail, priority)| CompletionItem {
|
||||
@@ -267,24 +268,24 @@ fn fields_from_runtime<L: ImportLoader>(
|
||||
Ok(tree)
|
||||
}
|
||||
|
||||
fn fields_from_host_value(value: &HostValue) -> FieldTree {
|
||||
fn fields_from_value(value: &Value) -> FieldTree {
|
||||
match value {
|
||||
HostValue::Object(fields) => fields
|
||||
Value::Object { fields, .. } => fields
|
||||
.iter()
|
||||
.map(|field| (field.name.clone(), fields_from_host_value(&field.value)))
|
||||
.map(|field| (field.name.clone(), fields_from_value(&field.value)))
|
||||
.collect(),
|
||||
HostValue::ArrayConstraint {
|
||||
Value::ArrayRange {
|
||||
default: Some(value),
|
||||
..
|
||||
}
|
||||
| HostValue::MapConstraint {
|
||||
| Value::MapRange {
|
||||
default: Some(value),
|
||||
..
|
||||
}
|
||||
| HostValue::Abstract {
|
||||
| Value::Range {
|
||||
default: Some(value),
|
||||
..
|
||||
} => fields_from_host_value(value),
|
||||
} => fields_from_value(value),
|
||||
_ => FieldTree::new(),
|
||||
}
|
||||
}
|
||||
@@ -688,7 +689,7 @@ mod tests {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use decodal::{
|
||||
Diagnostic, DiagnosticKind, EmptyLoader, HostValue, ImportCandidate, LoadedImport, Span,
|
||||
Diagnostic, DiagnosticKind, EmptyLoader, ImportCandidate, LoadedImport, Span, Value,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
@@ -708,15 +709,15 @@ mod tests {
|
||||
if specifier == "./post.md" {
|
||||
return Ok(LoadedImport::value(
|
||||
"post.md",
|
||||
HostValue::object([
|
||||
Value::object([
|
||||
(
|
||||
"frontmatter",
|
||||
HostValue::object([
|
||||
("title", HostValue::string("Hello")),
|
||||
("draft", HostValue::bool(false)),
|
||||
Value::object([
|
||||
("title", Value::string("Hello")),
|
||||
("draft", Value::bool(false)),
|
||||
]),
|
||||
),
|
||||
("body", HostValue::string("# Hello")),
|
||||
("body", Value::string("# Hello")),
|
||||
]),
|
||||
));
|
||||
}
|
||||
@@ -758,12 +759,9 @@ mod tests {
|
||||
fn configure_engine(&self, engine: &mut Engine<Self::Loader>) -> Result<()> {
|
||||
engine.bind_global(
|
||||
"App",
|
||||
HostValue::object([(
|
||||
Value::object([(
|
||||
"config",
|
||||
HostValue::object([
|
||||
("enabled", HostValue::bool_type()),
|
||||
("port", HostValue::int_type()),
|
||||
]),
|
||||
Value::object([("enabled", Value::bool_type()), ("port", Value::int_type())]),
|
||||
)]),
|
||||
)?;
|
||||
Ok(())
|
||||
@@ -854,6 +852,7 @@ mod tests {
|
||||
.unwrap();
|
||||
let labels = labels(result);
|
||||
assert!(labels.contains(&"String".into()));
|
||||
assert!(labels.contains(&"Unknown".into()));
|
||||
assert!(labels.contains(&"service".into()));
|
||||
assert!(labels.contains(&"value".into()));
|
||||
assert!(labels.contains(&"App".into()));
|
||||
@@ -881,6 +880,10 @@ mod tests {
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(labels(result).contains(&"String".into()));
|
||||
let result = complete(&EmptyEnvironment, "main.dcdl", "Unk", 3, false)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(labels(result).contains(&"Unknown".into()));
|
||||
}
|
||||
|
||||
struct EmptyEnvironment;
|
||||
|
||||
@@ -92,7 +92,7 @@ impl SemanticAnalysis {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use decodal::{
|
||||
Data, Diagnostic, DiagnosticKind, Engine, HostValue, ImportLoader, LoadedImport, Span,
|
||||
Data, Diagnostic, DiagnosticKind, Engine, ImportLoader, LoadedImport, Span, Value,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
@@ -100,11 +100,11 @@ mod tests {
|
||||
const ROOT: &str = r#"Post & import "./post.md""#;
|
||||
|
||||
struct ContentEnvironment {
|
||||
draft: HostValue,
|
||||
draft: Value,
|
||||
}
|
||||
|
||||
struct ContentLoader {
|
||||
draft: HostValue,
|
||||
draft: Value,
|
||||
}
|
||||
|
||||
impl ImportLoader for ContentLoader {
|
||||
@@ -122,15 +122,15 @@ mod tests {
|
||||
}
|
||||
Ok(LoadedImport::value(
|
||||
"content/post.md",
|
||||
HostValue::object([
|
||||
Value::object([
|
||||
(
|
||||
"frontmatter",
|
||||
HostValue::object([
|
||||
("title", HostValue::string("Hello")),
|
||||
Value::object([
|
||||
("title", Value::string("Hello")),
|
||||
("draft", self.draft.clone()),
|
||||
]),
|
||||
),
|
||||
("body", HostValue::string("# Hello")),
|
||||
("body", Value::string("# Hello")),
|
||||
]),
|
||||
))
|
||||
}
|
||||
@@ -148,15 +148,15 @@ mod tests {
|
||||
fn configure_engine(&self, engine: &mut Engine<Self::Loader>) -> decodal::Result<()> {
|
||||
engine.bind_global(
|
||||
"Post",
|
||||
HostValue::object([
|
||||
Value::object([
|
||||
(
|
||||
"frontmatter",
|
||||
HostValue::object([
|
||||
("title", HostValue::string_type()),
|
||||
("draft", HostValue::bool_type()),
|
||||
Value::object([
|
||||
("title", Value::string_type()),
|
||||
("draft", Value::bool_type()),
|
||||
]),
|
||||
),
|
||||
("body", HostValue::string_type()),
|
||||
("body", Value::string_type()),
|
||||
]),
|
||||
)?;
|
||||
Ok(())
|
||||
@@ -173,7 +173,7 @@ mod tests {
|
||||
#[test]
|
||||
fn injected_environment_matches_direct_evaluation() {
|
||||
let environment = ContentEnvironment {
|
||||
draft: HostValue::bool(false),
|
||||
draft: Value::bool(false),
|
||||
};
|
||||
let direct = evaluate_direct(&environment).unwrap();
|
||||
let service = LanguageService::new(&environment);
|
||||
@@ -186,7 +186,7 @@ mod tests {
|
||||
#[test]
|
||||
fn injected_environment_preserves_import_diagnostics() {
|
||||
let service = LanguageService::new(ContentEnvironment {
|
||||
draft: HostValue::string("maybe"),
|
||||
draft: Value::string("maybe"),
|
||||
});
|
||||
let analysis = service.analyze("main.dcdl", "main.dcdl", ROOT);
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::{error::Error, fmt};
|
||||
|
||||
use decodal::{
|
||||
Ast, BinaryOp, CompareOp, Expr, ExprId, Field, Param, SourceForm, Span, SyntaxToken,
|
||||
SyntaxTokenKind,
|
||||
Ast, BinaryOp, CompareOp, Expr, ExprId, Field, ObjectRest, Param, SourceForm, Span,
|
||||
SyntaxToken, SyntaxTokenKind,
|
||||
ast::{MatchArm, UnaryOp},
|
||||
parse_source, tokenize_source,
|
||||
};
|
||||
@@ -81,7 +81,7 @@ impl<'a> Formatter<'a> {
|
||||
fn format(&self, root: ExprId, source_form: SourceForm) -> String {
|
||||
let mut out = String::new();
|
||||
if source_form == SourceForm::Fields {
|
||||
let Expr::Object(fields) = &self.ast.get(root).expr else {
|
||||
let Expr::Object { fields, .. } = &self.ast.get(root).expr else {
|
||||
unreachable!("field-form sources parse to an object")
|
||||
};
|
||||
self.write_field_list(&mut out, fields, 0, self.source.len(), 0);
|
||||
@@ -145,7 +145,7 @@ impl<'a> Formatter<'a> {
|
||||
if self.has_comment(node.span)
|
||||
&& !matches!(
|
||||
node.expr,
|
||||
Expr::Object(_) | Expr::Array(_) | Expr::Let { .. } | Expr::Match { .. }
|
||||
Expr::Object { .. } | Expr::Array(_) | Expr::Let { .. } | Expr::Match { .. }
|
||||
)
|
||||
{
|
||||
out.push_str(self.raw(node.span));
|
||||
@@ -162,7 +162,9 @@ impl<'a> Formatter<'a> {
|
||||
Expr::Literal(_) | Expr::Ident(_) | Expr::RegexConstraint(_) | Expr::Wildcard => {
|
||||
out.push_str(self.raw(node.span));
|
||||
}
|
||||
Expr::Object(fields) => self.write_object(out, node.span, fields, indent),
|
||||
Expr::Object { fields, rest } => {
|
||||
self.write_object(out, node.span, fields, rest.as_ref(), indent)
|
||||
}
|
||||
Expr::Array(items) => self.write_array(out, node.span, items, indent),
|
||||
Expr::ArrayConstraint { item } => {
|
||||
out.push_str("[...");
|
||||
@@ -241,15 +243,50 @@ impl<'a> Formatter<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
fn write_object(&self, out: &mut String, span: Span, fields: &[Field], indent: usize) {
|
||||
fn write_object(
|
||||
&self,
|
||||
out: &mut String,
|
||||
span: Span,
|
||||
fields: &[Field],
|
||||
rest: Option<&ObjectRest>,
|
||||
indent: usize,
|
||||
) {
|
||||
let (start, end) =
|
||||
self.delimited_range(span, SyntaxTokenKind::LBrace, SyntaxTokenKind::RBrace);
|
||||
if fields.is_empty() && !self.has_comment_between(start, end) {
|
||||
if fields.is_empty() && rest.is_none() && !self.has_comment_between(start, end) {
|
||||
out.push_str("{}");
|
||||
return;
|
||||
}
|
||||
out.push_str("{\n");
|
||||
self.write_field_list(out, fields, start, end, indent + INDENT);
|
||||
let field_end = rest.map_or(end, |rest| rest.span.start as usize);
|
||||
self.write_field_list(out, fields, start, field_end, indent + INDENT);
|
||||
if let Some(rest) = rest {
|
||||
write_indent(out, indent + INDENT);
|
||||
out.push_str("...");
|
||||
let value_start = self.ast.span(rest.value).start as usize;
|
||||
let ellipsis = self
|
||||
.tokens_between(rest.span.start as usize, value_start)
|
||||
.find(|token| token.kind == SyntaxTokenKind::Ellipsis)
|
||||
.expect("parsed object rest constraints contain `...`");
|
||||
if self.has_comment_between(ellipsis.span.end as usize, value_start) {
|
||||
self.write_between(
|
||||
out,
|
||||
ellipsis.span.end as usize,
|
||||
value_start,
|
||||
Some(ellipsis.span.end as usize),
|
||||
indent + INDENT,
|
||||
);
|
||||
write_indent(out, indent + INDENT);
|
||||
}
|
||||
self.write_expr(out, rest.value, indent + INDENT, 0);
|
||||
self.write_between(
|
||||
out,
|
||||
rest.span.end as usize,
|
||||
end,
|
||||
Some(rest.span.end as usize),
|
||||
indent + INDENT,
|
||||
);
|
||||
}
|
||||
write_indent(out, indent);
|
||||
out.push('}');
|
||||
}
|
||||
@@ -524,7 +561,7 @@ impl<'a> Formatter<'a> {
|
||||
Expr::As { narrower, wider } => {
|
||||
self.is_inline_expr(*narrower) && self.is_inline_expr(*wider)
|
||||
}
|
||||
Expr::Object(_) | Expr::Let { .. } | Expr::Function { .. } | Expr::Match { .. } => {
|
||||
Expr::Object { .. } | Expr::Let { .. } | Expr::Function { .. } | Expr::Match { .. } => {
|
||||
false
|
||||
}
|
||||
}
|
||||
@@ -707,6 +744,17 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formats_unknown_and_object_rest_constraints() {
|
||||
let source =
|
||||
"value={hoge=Int;fuga=String;# remaining fields\n...# unconstrained\nUnknown};";
|
||||
let formatted = format_source(source).unwrap();
|
||||
assert_eq!(
|
||||
formatted,
|
||||
"value = {\n hoge = Int;\n fuga = String; # remaining fields\n ... # unconstrained\n Unknown\n};\n"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preserves_regex_and_escaped_strings() {
|
||||
let source = r#"value={pattern=/^api\/.+$/;text="a\n\"b";};"#;
|
||||
|
||||
@@ -7,7 +7,7 @@ use std::{
|
||||
|
||||
use decodal::{
|
||||
Diagnostic as DecodalDiagnostic, DiagnosticKind, HostEnvironment, ImportCandidate,
|
||||
ImportLoader, LoadedImport, LoadedSource, SourceId, Span,
|
||||
ImportLoader, LoadedImport, SourceId, Span,
|
||||
};
|
||||
use decodal_language_service::{
|
||||
CompletionKind as ServiceCompletionKind, CompletionResult, LanguageService, SemanticAnalysis,
|
||||
@@ -667,11 +667,11 @@ impl ImportLoader for FileSystemLoader {
|
||||
)
|
||||
})?
|
||||
};
|
||||
Ok(LoadedImport::Source(LoadedSource {
|
||||
Ok(LoadedImport::Source {
|
||||
key: key.clone(),
|
||||
name: key,
|
||||
source,
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
fn complete_import(
|
||||
@@ -743,7 +743,7 @@ mod tests {
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use decodal::{Engine, HostValue};
|
||||
use decodal::{Engine, Value};
|
||||
use lsp_server::RequestId;
|
||||
use serde_json::json;
|
||||
|
||||
@@ -772,12 +772,12 @@ mod tests {
|
||||
.is_some_and(|source| source == "draft: false");
|
||||
Ok(LoadedImport::value(
|
||||
"content/post.md",
|
||||
HostValue::object([(
|
||||
Value::object([(
|
||||
"draft",
|
||||
if draft {
|
||||
HostValue::bool(false)
|
||||
Value::bool(false)
|
||||
} else {
|
||||
HostValue::string("maybe")
|
||||
Value::string("maybe")
|
||||
},
|
||||
)]),
|
||||
))
|
||||
@@ -794,10 +794,7 @@ mod tests {
|
||||
}
|
||||
|
||||
fn configure_engine(&self, engine: &mut Engine<Self::Loader>) -> decodal::Result<()> {
|
||||
engine.bind_global(
|
||||
"Post",
|
||||
HostValue::object([("draft", HostValue::bool_type())]),
|
||||
)?;
|
||||
engine.bind_global("Post", Value::object([("draft", Value::bool_type())]))?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ use decodal_language_service::{CompletionKind, LanguageService};
|
||||
use wasm_bindgen::prelude::*;
|
||||
|
||||
#[cfg(any(target_arch = "wasm32", test))]
|
||||
mod host_value;
|
||||
mod value;
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod web_environment;
|
||||
|
||||
|
||||
@@ -1,41 +1,41 @@
|
||||
use decodal::{CompareOp, Constraint, HostValue, LiteralValue, PrimitiveType};
|
||||
use serde_json::{Map, Value};
|
||||
use decodal::{CompareOp, Constraint, LiteralValue, PrimitiveType, Value};
|
||||
use serde_json::{Map, Value as JsonValue};
|
||||
|
||||
pub(crate) fn from_json(value: &Value) -> Result<HostValue, String> {
|
||||
pub(crate) fn from_json(value: &JsonValue) -> Result<Value, String> {
|
||||
match value {
|
||||
Value::Null => Err(String::from("null is not a Decodal host value")),
|
||||
Value::Bool(value) => Ok(HostValue::bool(*value)),
|
||||
Value::Number(value) => number(value),
|
||||
Value::String(value) => Ok(HostValue::string(value)),
|
||||
Value::Array(items) => items
|
||||
JsonValue::Null => Err(String::from("null is not a Decodal value")),
|
||||
JsonValue::Bool(value) => Ok(Value::bool(*value)),
|
||||
JsonValue::Number(value) => number(value),
|
||||
JsonValue::String(value) => Ok(Value::string(value)),
|
||||
JsonValue::Array(items) => items
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, value)| {
|
||||
from_json(value).map_err(|error| format!("array item {index}: {error}"))
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map(HostValue::array),
|
||||
Value::Object(fields) => object(fields),
|
||||
.map(Value::array),
|
||||
JsonValue::Object(fields) => object(fields),
|
||||
}
|
||||
}
|
||||
|
||||
fn number(value: &serde_json::Number) -> Result<HostValue, String> {
|
||||
fn number(value: &serde_json::Number) -> Result<Value, String> {
|
||||
if let Some(value) = value.as_i64() {
|
||||
return Ok(HostValue::int(value));
|
||||
return Ok(Value::int(value));
|
||||
}
|
||||
if let Some(value) = value.as_u64() {
|
||||
return i64::try_from(value)
|
||||
.map(HostValue::int)
|
||||
.map_err(|_| String::from("integer host value is outside the signed 64-bit range"));
|
||||
.map(Value::int)
|
||||
.map_err(|_| String::from("integer value is outside the signed 64-bit range"));
|
||||
}
|
||||
value
|
||||
.as_f64()
|
||||
.filter(|value| value.is_finite())
|
||||
.map(HostValue::float)
|
||||
.ok_or_else(|| String::from("invalid numeric host value"))
|
||||
.map(Value::float)
|
||||
.ok_or_else(|| String::from("invalid numeric value"))
|
||||
}
|
||||
|
||||
fn object(fields: &Map<String, Value>) -> Result<HostValue, String> {
|
||||
fn object(fields: &Map<String, JsonValue>) -> Result<Value, String> {
|
||||
let Some(descriptor) = fields.get("$decodal") else {
|
||||
return fields
|
||||
.iter()
|
||||
@@ -45,7 +45,7 @@ fn object(fields: &Map<String, Value>) -> Result<HostValue, String> {
|
||||
.map_err(|error| format!("field `{name}`: {error}"))
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.map(HostValue::object);
|
||||
.map(Value::object);
|
||||
};
|
||||
|
||||
let descriptor = descriptor
|
||||
@@ -59,6 +59,14 @@ fn object(fields: &Map<String, Value>) -> Result<HostValue, String> {
|
||||
.map(Box::new);
|
||||
|
||||
match descriptor {
|
||||
"Unknown" => {
|
||||
let mut all_constraints = vec![Constraint::Unknown];
|
||||
all_constraints.extend(constraints);
|
||||
Ok(Value::Range {
|
||||
constraints: all_constraints,
|
||||
default,
|
||||
})
|
||||
}
|
||||
"String" | "Int" | "Float" | "Bool" => {
|
||||
let primitive = match descriptor {
|
||||
"String" => PrimitiveType::String,
|
||||
@@ -69,7 +77,7 @@ fn object(fields: &Map<String, Value>) -> Result<HostValue, String> {
|
||||
};
|
||||
let mut all_constraints = vec![Constraint::Type(primitive)];
|
||||
all_constraints.extend(constraints);
|
||||
Ok(HostValue::Abstract {
|
||||
Ok(Value::Range {
|
||||
constraints: all_constraints,
|
||||
default,
|
||||
})
|
||||
@@ -78,7 +86,7 @@ fn object(fields: &Map<String, Value>) -> Result<HostValue, String> {
|
||||
let item = fields
|
||||
.get("item")
|
||||
.ok_or_else(|| String::from("Array descriptor requires `item`"))?;
|
||||
Ok(HostValue::ArrayConstraint {
|
||||
Ok(Value::ArrayRange {
|
||||
item: Box::new(from_json(item)?),
|
||||
constraints,
|
||||
default,
|
||||
@@ -88,21 +96,49 @@ fn object(fields: &Map<String, Value>) -> Result<HostValue, String> {
|
||||
let value = fields
|
||||
.get("value")
|
||||
.ok_or_else(|| String::from("Map descriptor requires `value`"))?;
|
||||
Ok(HostValue::MapConstraint {
|
||||
Ok(Value::MapRange {
|
||||
value: Box::new(from_json(value)?),
|
||||
constraints,
|
||||
default,
|
||||
})
|
||||
}
|
||||
"Abstract" => Ok(HostValue::Abstract {
|
||||
"Object" => {
|
||||
if !constraints.is_empty() {
|
||||
return Err(String::from(
|
||||
"Object descriptor does not accept `constraints`; constrain its fields or rest range",
|
||||
));
|
||||
}
|
||||
if default.is_some() {
|
||||
return Err(String::from(
|
||||
"Object descriptor does not accept `default`; place defaults on its fields",
|
||||
));
|
||||
}
|
||||
let object_fields = fields
|
||||
.get("fields")
|
||||
.and_then(JsonValue::as_object)
|
||||
.ok_or_else(|| String::from("Object descriptor requires object `fields`"))?;
|
||||
let value_fields = object_fields
|
||||
.iter()
|
||||
.map(|(name, value)| {
|
||||
from_json(value)
|
||||
.map(|value| (name.clone(), value))
|
||||
.map_err(|error| format!("field `{name}`: {error}"))
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
let rest = fields
|
||||
.get("rest")
|
||||
.ok_or_else(|| String::from("Object descriptor requires `rest`"))?;
|
||||
Ok(Value::object_with_rest(value_fields, from_json(rest)?))
|
||||
}
|
||||
"Range" => Ok(Value::Range {
|
||||
constraints,
|
||||
default,
|
||||
}),
|
||||
name => Err(format!("unknown Decodal host descriptor `{name}`")),
|
||||
name => Err(format!("unknown Decodal value descriptor `{name}`")),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_constraints(value: Option<&Value>) -> Result<Vec<Constraint>, String> {
|
||||
fn parse_constraints(value: Option<&JsonValue>) -> Result<Vec<Constraint>, String> {
|
||||
let Some(value) = value else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
@@ -118,12 +154,13 @@ fn parse_constraints(value: Option<&Value>) -> Result<Vec<Constraint>, String> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn parse_constraint(value: &Value) -> Result<Constraint, String> {
|
||||
fn parse_constraint(value: &JsonValue) -> Result<Constraint, String> {
|
||||
let fields = value
|
||||
.as_object()
|
||||
.ok_or_else(|| String::from("constraint must be an object"))?;
|
||||
let kind = required_string(fields, "kind")?;
|
||||
match kind {
|
||||
"unknown" => Ok(Constraint::Unknown),
|
||||
"type" => match required_string(fields, "value")? {
|
||||
"String" => Ok(Constraint::Type(PrimitiveType::String)),
|
||||
"Int" => Ok(Constraint::Type(PrimitiveType::Int)),
|
||||
@@ -154,29 +191,29 @@ fn parse_constraint(value: &Value) -> Result<Constraint, String> {
|
||||
}
|
||||
}
|
||||
|
||||
fn literal(value: &Value) -> Result<LiteralValue, String> {
|
||||
fn literal(value: &JsonValue) -> Result<LiteralValue, String> {
|
||||
match value {
|
||||
Value::String(value) => Ok(LiteralValue::String(value.clone())),
|
||||
Value::Bool(value) => Ok(LiteralValue::Bool(*value)),
|
||||
Value::Number(value) => match number(value)? {
|
||||
HostValue::Int(value) => Ok(LiteralValue::Int(value)),
|
||||
HostValue::Float(value) => Ok(LiteralValue::Float(value)),
|
||||
JsonValue::String(value) => Ok(LiteralValue::String(value.clone())),
|
||||
JsonValue::Bool(value) => Ok(LiteralValue::Bool(*value)),
|
||||
JsonValue::Number(value) => match number(value)? {
|
||||
Value::Int(value) => Ok(LiteralValue::Int(value)),
|
||||
Value::Float(value) => Ok(LiteralValue::Float(value)),
|
||||
_ => unreachable!(),
|
||||
},
|
||||
_ => Err(String::from("comparison value must be a primitive literal")),
|
||||
}
|
||||
}
|
||||
|
||||
fn required_string<'a>(fields: &'a Map<String, Value>, name: &str) -> Result<&'a str, String> {
|
||||
fn required_string<'a>(fields: &'a Map<String, JsonValue>, name: &str) -> Result<&'a str, String> {
|
||||
fields
|
||||
.get(name)
|
||||
.and_then(Value::as_str)
|
||||
.and_then(JsonValue::as_str)
|
||||
.ok_or_else(|| format!("constraint requires string `{name}`"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use decodal::{Constraint, HostValue, LiteralValue, PrimitiveType};
|
||||
use decodal::{Constraint, LiteralValue, PrimitiveType, Value};
|
||||
|
||||
use super::from_json;
|
||||
|
||||
@@ -188,11 +225,11 @@ mod tests {
|
||||
});
|
||||
assert_eq!(
|
||||
from_json(&value).unwrap(),
|
||||
HostValue::object([
|
||||
("body", HostValue::string("# Hello")),
|
||||
Value::object([
|
||||
("body", Value::string("# Hello")),
|
||||
(
|
||||
"frontmatter",
|
||||
HostValue::object([("draft", HostValue::bool(false))]),
|
||||
Value::object([("draft", Value::bool(false))]),
|
||||
),
|
||||
])
|
||||
);
|
||||
@@ -210,8 +247,8 @@ mod tests {
|
||||
});
|
||||
assert_eq!(
|
||||
from_json(&value).unwrap(),
|
||||
HostValue::ArrayConstraint {
|
||||
item: Box::new(HostValue::Abstract {
|
||||
Value::ArrayRange {
|
||||
item: Box::new(Value::Range {
|
||||
constraints: vec![
|
||||
Constraint::Type(PrimitiveType::Int),
|
||||
Constraint::Compare(decodal::CompareOp::Gt, LiteralValue::Int(0)),
|
||||
@@ -219,10 +256,7 @@ mod tests {
|
||||
default: None,
|
||||
}),
|
||||
constraints: Vec::new(),
|
||||
default: Some(Box::new(HostValue::array([
|
||||
HostValue::int(1),
|
||||
HostValue::int(2),
|
||||
]))),
|
||||
default: Some(Box::new(Value::array([Value::int(1), Value::int(2),]))),
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -236,10 +270,41 @@ mod tests {
|
||||
});
|
||||
assert_eq!(
|
||||
from_json(&value).unwrap(),
|
||||
HostValue::MapConstraint {
|
||||
value: Box::new(HostValue::bool_type()),
|
||||
Value::MapRange {
|
||||
value: Box::new(Value::bool_type()),
|
||||
constraints: Vec::new(),
|
||||
default: Some(Box::new(HostValue::object([] as [(&str, HostValue); 0]))),
|
||||
default: Some(Box::new(Value::object([] as [(&str, Value); 0]))),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converts_unknown_and_open_object_descriptors() {
|
||||
let value = serde_json::json!({
|
||||
"$decodal": "Object",
|
||||
"fields": {
|
||||
"name": { "$decodal": "String" },
|
||||
},
|
||||
"rest": { "$decodal": "Unknown" },
|
||||
});
|
||||
assert_eq!(
|
||||
from_json(&value).unwrap(),
|
||||
Value::object_with_rest([("name", Value::string_type())], Value::unknown(),)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converts_general_range_descriptors() {
|
||||
let value = serde_json::json!({
|
||||
"$decodal": "Range",
|
||||
"constraints": [{ "kind": "predicate", "value": "available" }],
|
||||
"default": true,
|
||||
});
|
||||
assert_eq!(
|
||||
from_json(&value).unwrap(),
|
||||
Value::Range {
|
||||
constraints: vec![Constraint::BuiltinPredicate("available".into())],
|
||||
default: Some(Box::new(Value::bool(true))),
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use decodal::{
|
||||
Diagnostic, DiagnosticKind, Engine, HostEnvironment, HostValue, ImportCandidate, ImportLoader,
|
||||
LoadedImport, Span,
|
||||
Diagnostic, DiagnosticKind, Engine, HostEnvironment, ImportCandidate, ImportLoader,
|
||||
LoadedImport, Span, Value as DecodalValue,
|
||||
};
|
||||
use js_sys::{Function, Reflect};
|
||||
use serde_json::Value;
|
||||
use wasm_bindgen::{JsCast, JsValue};
|
||||
|
||||
use crate::host_value;
|
||||
use crate::value;
|
||||
|
||||
pub(crate) struct JsEnvironment {
|
||||
globals: BTreeMap<String, HostValue>,
|
||||
globals: BTreeMap<String, DecodalValue>,
|
||||
load_import: Option<Function>,
|
||||
complete_import: Option<Function>,
|
||||
}
|
||||
@@ -37,7 +37,7 @@ impl JsEnvironment {
|
||||
globals
|
||||
.iter()
|
||||
.map(|(name, value)| {
|
||||
host_value::from_json(value)
|
||||
value::from_json(value)
|
||||
.map(|value| (name.clone(), value))
|
||||
.map_err(|error| {
|
||||
JsValue::from_str(&format!("invalid global `{name}`: {error}"))
|
||||
@@ -133,7 +133,7 @@ fn loaded_import(value: &Value) -> Result<LoadedImport, String> {
|
||||
let value = fields
|
||||
.get("value")
|
||||
.ok_or_else(|| String::from("value import requires `value`"))?;
|
||||
let value = host_value::from_json(value)
|
||||
let value = value::from_json(value)
|
||||
.map_err(|error| format!("invalid imported value: {error}"))?;
|
||||
Ok(LoadedImport::value(key, value))
|
||||
}
|
||||
|
||||
@@ -75,5 +75,5 @@ fallback は有限で明示的な仕組みに限定する。
|
||||
- `default`: 未指定値の fallback。
|
||||
- `match`: 有限 pattern に基づく分岐。
|
||||
|
||||
Decodal は `unknown` / `any` を持たないため、field 不在や未解決 identifier を fallback 可能な通常値として扱わない。
|
||||
それらは diagnostic として報告する。
|
||||
`Unknown` は明示的に書く最上位のabstract rangeであり、評価エラーを包む値ではない。
|
||||
field 不在や未解決 identifier は `Unknown` へ変換せず、diagnostic として報告する。
|
||||
|
||||
@@ -17,23 +17,23 @@ let / function env
|
||||
```
|
||||
|
||||
Module top-level bindings shadow prelude bindings.
|
||||
Primitive type names such as `String`, `Int`, `Float`, and `Bool` are handled before environment lookup, so they are reserved and cannot be shadowed by host bindings.
|
||||
Primitive type names such as `String`, `Int`, `Float`, and `Bool`, plus the top range `Unknown`, 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::{EmptyLoader, Engine, HostValue};
|
||||
use decodal::{EmptyLoader, Engine, Value};
|
||||
|
||||
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)?),
|
||||
Value::object([
|
||||
("name", Value::string_type()),
|
||||
("port", Value::int_type().gt(443).default_int(8443)?),
|
||||
("enabled", Value::bool_type().default_bool(true)?),
|
||||
]),
|
||||
)?;
|
||||
```
|
||||
@@ -47,40 +47,49 @@ A user source can then refer to `Service` without importing it.
|
||||
} as Service
|
||||
```
|
||||
|
||||
## HostValue
|
||||
## Value
|
||||
|
||||
`HostValue` is the public builder-facing value representation for embedding.
|
||||
`Value` is the public builder-facing value representation for embedding.
|
||||
It keeps host code from constructing internal `ThunkId` or `ObjectValue` values directly.
|
||||
|
||||
```text
|
||||
HostValue =
|
||||
Value =
|
||||
String
|
||||
Int
|
||||
Float
|
||||
Bool
|
||||
Array(Vec<HostValue>)
|
||||
ArrayConstraint { item, constraints, default }
|
||||
MapConstraint { value, constraints, default }
|
||||
Object(Vec<HostField>)
|
||||
Abstract { constraints, default }
|
||||
Array(Vec<Value>)
|
||||
ArrayRange { item, constraints, default }
|
||||
MapRange { value, constraints, default }
|
||||
Object { fields, rest: Option<Value> }
|
||||
Range { 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.
|
||||
When a value is bound by the host, the engine internalizes it into `RuntimeValue` and allocates value thunks for object fields, array items, and defaults.
|
||||
|
||||
`HostValue::array_of(item)` builds an array constraint with a required element schema.
|
||||
There is no host API for an unconstrained abstract array.
|
||||
`Value::array_of(item)` builds an array constraint with a required element schema.
|
||||
There is no `Value` constructor for an unconstrained abstract array.
|
||||
|
||||
`HostValue::map_of(value)` builds a map constraint whose arbitrary object field values must satisfy `value`.
|
||||
`BTreeMap<String, T>` and, with `std`, `HashMap<String, T>` implement `DecodalSchema`, `DecodalDecode`, and `IntoHostValue` using this representation.
|
||||
`Value::map_of(value)` builds a map constraint whose arbitrary object field values must satisfy `value`.
|
||||
`BTreeMap<String, T>` and, with `std`, `HashMap<String, T>` implement `DecodalSchema`, `DecodalDecode`, and `IntoValue` using this representation.
|
||||
|
||||
## Abstract host objects
|
||||
|
||||
A host-provided schema object is represented as a concrete object structure whose fields may contain abstract values.
|
||||
`Value::unknown()` builds the top abstract range. `Value::object_with_rest(fields, rest)` builds an object with named fields and a range for all remaining fields.
|
||||
|
||||
```rust
|
||||
HostValue::object([
|
||||
("name", HostValue::string_type()),
|
||||
("port", HostValue::int_type().gt(443).default_int(8443)?),
|
||||
Value::object_with_rest(
|
||||
[("enabled", Value::bool_type().default_bool(true)?)],
|
||||
Value::unknown(),
|
||||
)
|
||||
```
|
||||
|
||||
## Objects containing ranges
|
||||
|
||||
A host-provided schema object is represented as a concrete object structure whose fields may contain ranges.
|
||||
|
||||
```rust
|
||||
Value::object([
|
||||
("name", Value::string_type()),
|
||||
("port", Value::int_type().gt(443).default_int(8443)?),
|
||||
])
|
||||
```
|
||||
|
||||
@@ -100,9 +109,28 @@ This matches the runtime model used for Decodal source-defined schema objects.
|
||||
Hosts can enable the `derive` feature on `decodal` to keep a Rust struct, the Decodal schema, and the decoded result in sync.
|
||||
The derive implements two traits from the `decodal` crate:
|
||||
|
||||
- `DecodalSchema`: builds a `HostValue` schema that can be passed to `Engine::bind_global`.
|
||||
- `DecodalSchema`: builds a `Value` schema that can be passed to `Engine::bind_global`.
|
||||
- `DecodalDecode`: decodes materialized `Data` back into the Rust type.
|
||||
|
||||
An explicitly marked map field can receive the open portion of an object.
|
||||
|
||||
```rust
|
||||
use std::collections::BTreeMap;
|
||||
use decodal::{Data, Decodal};
|
||||
|
||||
#[derive(Decodal)]
|
||||
struct OpenConfig {
|
||||
enabled: bool,
|
||||
|
||||
#[decodal(rest)]
|
||||
extra: BTreeMap<String, Data>,
|
||||
}
|
||||
```
|
||||
|
||||
The schema generated for `extra` is `...Unknown`. A typed receiver such as `BTreeMap<String, String>` generates `...String` instead. During decode, named top-level fields are excluded and all remaining fields are collected into the map.
|
||||
|
||||
Only one `#[decodal(rest)]` field is allowed. It must implement `DecodalRest`; the core provides implementations for `BTreeMap<String, T>` and, with `std`, `HashMap<String, T>`. `rename`, `default`, and field constraints cannot be combined with `rest`. Without a rest receiver, a derived struct remains a closed object range.
|
||||
|
||||
```rust
|
||||
use decodal::{Decodal, DecodalDecode, DecodalSchema, EmptyLoader, Engine};
|
||||
|
||||
@@ -202,20 +230,21 @@ run_stdio(|initialize| {
|
||||
`LspEnvironment` adds document lifecycle hooks on top of `HostEnvironment`.
|
||||
A `run_stdio` environment factory always receives the client's `InitializeParams`; the host decides whether to use its workspace folders, initialization options, and capabilities or ignore them.
|
||||
A host that keeps unsaved buffers in shared state can update them from `open_document`, `change_document`, and `close_document`; every subsequent diagnostic pass creates the normal import loader from that updated environment.
|
||||
Synchronized non-Decodal documents are passed through these hooks but are not evaluated as Decodal roots, so a loader can parse an unsaved Markdown file into `HostValue` and immediately revalidate the open Decodal documents that import it.
|
||||
Synchronized non-Decodal documents are passed through these hooks but are not evaluated as Decodal roots, so a loader can parse an unsaved Markdown file into `Value` and immediately revalidate the open Decodal documents that import it.
|
||||
|
||||
## Structured imports
|
||||
|
||||
`ImportLoader::load` returns either `LoadedImport::Source` or `LoadedImport::Value`.
|
||||
The value variant carries a `HostValue`, allowing a host to parse non-Decodal resources such as Markdown into an application-specific structure.
|
||||
The value variant carries a `Value`, allowing a host to parse non-Decodal resources such as Markdown into an application-specific structure.
|
||||
|
||||
```text
|
||||
import "./post.md"
|
||||
-> host parses content
|
||||
-> LoadedImport::Value(
|
||||
{ frontmatter: {...}, body: "..." }
|
||||
)
|
||||
-> Engine internalizes HostValue
|
||||
-> LoadedImport::Value {
|
||||
key: "content/post.md",
|
||||
value: { frontmatter: {...}, body: "..." }
|
||||
}
|
||||
-> Engine internalizes Value
|
||||
-> normal composition and materialization
|
||||
```
|
||||
|
||||
@@ -223,7 +252,7 @@ The core does not select content types or bundle Markdown/frontmatter parsers.
|
||||
The loader owns path resolution, media or extension dispatch, parsing rules, and parse diagnostics.
|
||||
The stable loader key is also used to cache structured imports.
|
||||
|
||||
When a structured value fails a Decodal constraint, the diagnostic keeps the Decodal constraint span and identifies the host value by its stable import key and logical value path, such as `content/post.md` and `frontmatter.draft`.
|
||||
`HostValue` does not need source spans: syntax diagnostics for the external format remain the loader's responsibility, while cross-value validation reports semantic provenance.
|
||||
When a structured value fails a Decodal constraint, the diagnostic keeps the Decodal constraint span and identifies the imported value by its stable key and logical value path, such as `content/post.md` and `frontmatter.draft`.
|
||||
`Value` does not need source spans: syntax diagnostics for the external format remain the loader's responsibility, while cross-value validation reports semantic provenance.
|
||||
|
||||
`load` is the single import hook: loaders dispatch by extension, media type, or another host-defined rule and return the appropriate variant directly.
|
||||
|
||||
@@ -71,10 +71,10 @@ ModuleRegistry:
|
||||
import 先 module は、この段階で全て読み込む必要はない。
|
||||
|
||||
import expression が評価されたとき、処理系は `ImportLoader::load` に現在の module key と import specifier を渡す。
|
||||
loader は module key、表示名、および DCDL source text または構造化済み `HostValue` を返す。
|
||||
loader は module key、表示名、および DCDL source text または構造化済み `Value` を返す。
|
||||
DCDL source の場合、module registry は key が未登録なら対象 module を parse / desugar して登録する。
|
||||
登録された source module は module root thunk を持つ。
|
||||
構造化値の場合、host value を runtime value に internalize した root thunk を key でキャッシュする。
|
||||
構造化値の場合、host が返した `Value` を runtime value に internalize した root thunk を key でキャッシュする。
|
||||
同じ key が複数回 import された場合は、同じ source module または structured root thunk を使う。
|
||||
|
||||
つまり source import は module を即時評価しない。
|
||||
|
||||
@@ -10,7 +10,7 @@ Core に入れる機能は、基本的に deterministic な value transformation
|
||||
- arithmetic / logical / comparison operators
|
||||
- array concat
|
||||
- object / constraint composition
|
||||
- asymmetric range refinement and homogeneous map constraints
|
||||
- asymmetric range refinement, `Unknown`, map constraints, and object rest constraints
|
||||
- default materialization
|
||||
- pure function evaluation
|
||||
- host supplied import evaluation
|
||||
@@ -24,7 +24,7 @@ Core に入れないものは以下である。
|
||||
- arbitrary host function calls
|
||||
- symbolic constraint solving beyond simple normalization
|
||||
|
||||
未解決 identifier や missing field は `unknown` として流れず、diagnostic になる。
|
||||
未解決 identifier や missing field は明示的な `Unknown` range とは異なり、diagnostic になる。
|
||||
この方針により、存在チェックや optional chaining のような dynamic object inspection は core language の対象外とする。
|
||||
|
||||
## Constraint reasoning
|
||||
|
||||
@@ -14,9 +14,9 @@ RuntimeValue =
|
||||
`Concrete` は明示的な値である。
|
||||
`Abstract` は、まだ具体値に確定していない制約付きの値である。
|
||||
|
||||
Decodal は `unknown`、`any`、`null` のような「存在するが意味が未確定な値」を runtime value として持たない。
|
||||
識別子や field が解決できない場合は、その場で diagnostic になる。
|
||||
未解決値を後続の演算へ流して推論することはしない。
|
||||
Decodal は `Unknown` を、すべてのDecodal値を包含する最上位の abstract range として持つ。
|
||||
これは存在する具体値の型情報を消す `Any` ではない。具体値を `Unknown` に対して検証した場合は具体値を保持し、`Unknown` のままmaterializeしようとした場合は diagnostic になる。
|
||||
識別子や field が解決できない状態とは区別し、それらは従来どおりその場で diagnostic になる。
|
||||
|
||||
## ConcreteValue
|
||||
|
||||
@@ -37,10 +37,15 @@ object は concrete structure として扱う。
|
||||
```text
|
||||
ObjectValue:
|
||||
fields: Map<Symbol, ObjectField>
|
||||
rest: Option<ObjectRest>
|
||||
|
||||
ObjectField:
|
||||
value: ThunkId
|
||||
span: Span
|
||||
|
||||
ObjectRest:
|
||||
value: ThunkId
|
||||
span: Span
|
||||
```
|
||||
|
||||
例えば以下の schema object は、object 自体は concrete だが、field の値は abstract value になる。
|
||||
@@ -122,6 +127,7 @@ constraint は concrete value とは別の型として扱う。
|
||||
|
||||
```text
|
||||
Constraint =
|
||||
Unknown
|
||||
Type(PrimitiveType)
|
||||
ArrayItems(ThunkId)
|
||||
MapValues(ThunkId)
|
||||
@@ -137,6 +143,8 @@ Constraint =
|
||||
`MapValues` は object の key 集合を制限せず、すべての field value へ適用する schema thunk を表す。
|
||||
連想配列は materialize 後も `Data::Object` になり、別の data variant は持たない。
|
||||
|
||||
`ObjectValue.rest` は名前付きfieldを持つobjectの残余field rangeを表す。rest自体はfieldを生成せず、materializeは実在するfieldだけを出力する。
|
||||
|
||||
初期実装では、object の形は主に `Concrete(Object)` の field に `Abstract` を置くことで表現する。
|
||||
object 全体にかかる constraint は必要になった時点で追加する。
|
||||
|
||||
|
||||
@@ -82,12 +82,27 @@ Int & >= 10 & <= 10 # OK
|
||||
最小の組み込み制約は以下である。
|
||||
|
||||
```dcdl
|
||||
Unknown
|
||||
String
|
||||
Int
|
||||
Float
|
||||
Bool
|
||||
```
|
||||
|
||||
`Unknown` はすべてのDecodal値を含む最上位rangeである。検査を無効化する `Any` ではなく、具体的な値またはより狭いrangeがまだ決まっていないことを表す。
|
||||
|
||||
```dcdl
|
||||
Int & Unknown # Int
|
||||
42 as Unknown # 42
|
||||
Unknown as Int # エラー
|
||||
```
|
||||
|
||||
`Unknown` 自体は具体値を持たないためmaterializeできない。defaultを与えるか、具体値で絞り込む必要がある。
|
||||
|
||||
```dcdl
|
||||
Unknown default {} # {}
|
||||
```
|
||||
|
||||
追加の述語制約はライブラリまたは組み込みとして提供できる。
|
||||
|
||||
```dcdl
|
||||
@@ -150,7 +165,16 @@ Services = {...{
|
||||
```
|
||||
|
||||
key は schema で列挙せず、空 object も許容する。
|
||||
固定 object field と任意 key の value constraint を混在させる構文は現在サポートしない。
|
||||
固定 object field と任意 key の value constraint は、末尾の `...T` で混在できる。
|
||||
|
||||
```dcdl
|
||||
{
|
||||
enabled = Bool default true;
|
||||
...Unknown
|
||||
}
|
||||
```
|
||||
|
||||
このrest constraintは明示されていないfieldだけに適用され、field自体は生成しない。
|
||||
|
||||
## default
|
||||
|
||||
|
||||
@@ -63,4 +63,26 @@ services = {
|
||||
右辺にしかない field は default の有無にかかわらず abstract のまま残る。
|
||||
host から渡した object は識別子構文に収まらない文字列 key も保持できるが、DCDL source の object field name は通常の識別子に限られる。
|
||||
|
||||
固定 field と任意 key を一つの object schema に混在させる rest-field 構文は、現在サポートしない。
|
||||
## Object rest constraint
|
||||
|
||||
固定 field と任意 key の value range は、一つの object に混在できる。
|
||||
|
||||
```dcdl
|
||||
OpenConfig = {
|
||||
enabled = Bool default true;
|
||||
...Unknown
|
||||
};
|
||||
```
|
||||
|
||||
`...T` は明示されていない残余 field にだけ適用する。明示 field にはそれぞれの field range を適用し、rest range を重ねて適用しない。
|
||||
rest constraint は field を生成せず、materialize 時には実際に存在する追加 field だけを出力する。
|
||||
|
||||
```dcdl
|
||||
{
|
||||
enabled = false;
|
||||
plugin = { name = "cache"; };
|
||||
} as OpenConfig
|
||||
```
|
||||
|
||||
`...T` は object の末尾に一つだけ書ける。省略した object は閉じており、`as` の左辺に未宣言 field があればエラーになる。
|
||||
名前付き field を持たない `{...T}` は従来どおり抽象的な map constraint であり、単独でmaterializeするにはdefaultまたは具体値が必要になる。
|
||||
|
||||
@@ -13,4 +13,4 @@ config.feature_hoge.enable
|
||||
参照先 field は必要になるまで評価されない。
|
||||
|
||||
存在しない field への参照は diagnostic になる。
|
||||
Decodal は unknown / Any のような値を伝播せず、field の有無を曖昧にしない。
|
||||
明示的な `Unknown` range は存在しないfieldを表さないため、fieldの有無を曖昧にはしない。
|
||||
|
||||
@@ -71,7 +71,9 @@ literal = string | integer | float | "true" | "false" | regex ;
|
||||
comparison_operator = "==" | "!=" | "<" | "<=" | ">" | ">=" ;
|
||||
comparison_constraint = ( "<" | "<=" | ">" | ">=" ) , expression ;
|
||||
|
||||
object = "{" , [ field_definition , { ";" , field_definition } , [ ";" ] ] , "}" ;
|
||||
object = "{" , [ field_definition , { ";" , field_definition }
|
||||
, [ ";" , object_rest ] , [ ";" ] ] , "}" ;
|
||||
object_rest = "..." , expression ;
|
||||
map_constraint = "{" , "..." , expression , "}" ;
|
||||
field_definition = field_path , "=" , expression ;
|
||||
field_path = identifier , { "." , identifier } ;
|
||||
|
||||
@@ -43,11 +43,11 @@ CLI では canonical path を key とする。
|
||||
|
||||
### 構造化 import
|
||||
|
||||
`ImportLoader::load` は DCDL source または host が構築した `HostValue` を import 結果として返す。
|
||||
`ImportLoader::load` は DCDL source または host が構築した `Value` を import 結果として返す。
|
||||
Markdown、JSON、TOML などの解釈規則は core に固定せず、loader がファイル種別を判定して構造化する。
|
||||
|
||||
```rust
|
||||
use decodal::{HostValue, ImportLoader, LoadedImport};
|
||||
use decodal::{Value, ImportLoader, LoadedImport};
|
||||
|
||||
struct ContentLoader;
|
||||
|
||||
@@ -62,9 +62,9 @@ impl ImportLoader for ContentLoader {
|
||||
let parsed = parse_frontmatter(&markdown)?;
|
||||
return Ok(LoadedImport::value(
|
||||
parsed.key,
|
||||
HostValue::object([
|
||||
Value::object([
|
||||
("frontmatter", parsed.frontmatter),
|
||||
("body", HostValue::string(parsed.body)),
|
||||
("body", Value::string(parsed.body)),
|
||||
]),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ tags = [...String];
|
||||
```
|
||||
|
||||
現在の primitive type は `String`、`Int`、`Float`、`Bool` である。
|
||||
`Unknown` はprimitive typeではなく、すべてのDecodal値を含む最上位の抽象rangeである。具体値またはdefaultがない `Unknown` はmaterializeできない。
|
||||
配列は primitive type ではなく、必須の要素制約を持つ `[...T]` で表現する。
|
||||
各型の個別仕様へのリンクは [Manual Index](../../index.md) に集約する。
|
||||
|
||||
|
||||
@@ -90,7 +90,12 @@ Literal { String | Integer | Float | True | False | Regex }
|
||||
CompareOperator { EqualEqual | BangEqual | Lt | Lte | Gt | Gte }
|
||||
ComparisonConstraint { (Lt | Lte | Gt | Gte) Expression }
|
||||
|
||||
Object { LBrace (FieldDefinition (Semicolon FieldDefinition)* Semicolon?)? RBrace }
|
||||
Object {
|
||||
LBrace
|
||||
(FieldDefinition (Semicolon FieldDefinition)* (Semicolon ObjectRest)? Semicolon?)?
|
||||
RBrace
|
||||
}
|
||||
ObjectRest { Ellipsis Expression }
|
||||
MapConstraint { LBrace Ellipsis Expression RBrace }
|
||||
FieldDefinition { FieldPath Equal Expression }
|
||||
FieldPath { Identifier !fieldPath (Dot Identifier)* }
|
||||
|
||||
@@ -210,3 +210,29 @@ Map constraint and ascription
|
||||
(field_definition
|
||||
path: (field_path (identifier))
|
||||
value: (identifier))))))))
|
||||
|
||||
==================
|
||||
Unknown object rest constraint
|
||||
==================
|
||||
{
|
||||
value = {
|
||||
hoge = Int;
|
||||
fuga = String;
|
||||
...Unknown
|
||||
};
|
||||
}
|
||||
---
|
||||
|
||||
(source_file
|
||||
(object
|
||||
(field_definition
|
||||
path: (field_path (identifier))
|
||||
value: (object
|
||||
(field_definition
|
||||
path: (field_path (identifier))
|
||||
value: (identifier))
|
||||
(field_definition
|
||||
path: (field_path (identifier))
|
||||
value: (identifier))
|
||||
(object_rest
|
||||
value: (identifier))))))
|
||||
|
||||
@@ -99,10 +99,20 @@ module.exports = grammar({
|
||||
|
||||
object: $ => seq(
|
||||
'{',
|
||||
semiSep($.field_definition),
|
||||
optional(seq(
|
||||
$.field_definition,
|
||||
repeat(seq(';', $.field_definition)),
|
||||
optional(seq(';', $.object_rest)),
|
||||
optional(';'),
|
||||
)),
|
||||
'}',
|
||||
),
|
||||
|
||||
object_rest: $ => seq(
|
||||
'...',
|
||||
field('value', $._expression),
|
||||
),
|
||||
|
||||
map_constraint: $ => seq(
|
||||
'{',
|
||||
'...',
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
] @punctuation.delimiter
|
||||
|
||||
((identifier) @type.builtin
|
||||
(#match? @type.builtin "^(String|Int|Float|Bool)$"))
|
||||
(#match? @type.builtin "^(String|Int|Float|Bool|Unknown)$"))
|
||||
|
||||
(field_definition
|
||||
path: (field_path (identifier) @property))
|
||||
|
||||
+38
@@ -302,6 +302,27 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "CHOICE",
|
||||
"members": [
|
||||
{
|
||||
"type": "SEQ",
|
||||
"members": [
|
||||
{
|
||||
"type": "STRING",
|
||||
"value": ";"
|
||||
},
|
||||
{
|
||||
"type": "SYMBOL",
|
||||
"name": "object_rest"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "BLANK"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "CHOICE",
|
||||
"members": [
|
||||
@@ -327,6 +348,23 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"object_rest": {
|
||||
"type": "SEQ",
|
||||
"members": [
|
||||
{
|
||||
"type": "STRING",
|
||||
"value": "..."
|
||||
},
|
||||
{
|
||||
"type": "FIELD",
|
||||
"name": "value",
|
||||
"content": {
|
||||
"type": "SYMBOL",
|
||||
"name": "_expression"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"map_constraint": {
|
||||
"type": "SEQ",
|
||||
"members": [
|
||||
|
||||
+92
@@ -1758,10 +1758,102 @@
|
||||
{
|
||||
"type": "field_definition",
|
||||
"named": true
|
||||
},
|
||||
{
|
||||
"type": "object_rest",
|
||||
"named": true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "object_rest",
|
||||
"named": true,
|
||||
"fields": {
|
||||
"value": {
|
||||
"multiple": false,
|
||||
"required": true,
|
||||
"types": [
|
||||
{
|
||||
"type": "array",
|
||||
"named": true
|
||||
},
|
||||
{
|
||||
"type": "array_constraint",
|
||||
"named": true
|
||||
},
|
||||
{
|
||||
"type": "as_expression",
|
||||
"named": true
|
||||
},
|
||||
{
|
||||
"type": "binary_expression",
|
||||
"named": true
|
||||
},
|
||||
{
|
||||
"type": "call_expression",
|
||||
"named": true
|
||||
},
|
||||
{
|
||||
"type": "comparison_constraint",
|
||||
"named": true
|
||||
},
|
||||
{
|
||||
"type": "default_expression",
|
||||
"named": true
|
||||
},
|
||||
{
|
||||
"type": "function_expression",
|
||||
"named": true
|
||||
},
|
||||
{
|
||||
"type": "identifier",
|
||||
"named": true
|
||||
},
|
||||
{
|
||||
"type": "import_expression",
|
||||
"named": true
|
||||
},
|
||||
{
|
||||
"type": "let_expression",
|
||||
"named": true
|
||||
},
|
||||
{
|
||||
"type": "literal",
|
||||
"named": true
|
||||
},
|
||||
{
|
||||
"type": "map_constraint",
|
||||
"named": true
|
||||
},
|
||||
{
|
||||
"type": "match_expression",
|
||||
"named": true
|
||||
},
|
||||
{
|
||||
"type": "object",
|
||||
"named": true
|
||||
},
|
||||
{
|
||||
"type": "parenthesized_expression",
|
||||
"named": true
|
||||
},
|
||||
{
|
||||
"type": "path_expression",
|
||||
"named": true
|
||||
},
|
||||
{
|
||||
"type": "regex_literal",
|
||||
"named": true
|
||||
},
|
||||
{
|
||||
"type": "unary_expression",
|
||||
"named": true
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"type": "parameter",
|
||||
"named": true,
|
||||
|
||||
Generated
+7222
-6054
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
||||
OpenConfig = {
|
||||
enabled = Bool default true;
|
||||
...Unknown
|
||||
};
|
||||
|
||||
config = {
|
||||
enabled = false;
|
||||
plugin = {
|
||||
name = "cache";
|
||||
};
|
||||
} as OpenConfig;
|
||||
@@ -1,18 +1,18 @@
|
||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||
import {LRParser} from "npm:@lezer/lr@^1.4.10"
|
||||
const spec_Identifier = {__proto__:null,as:182}
|
||||
const spec_Identifier = {__proto__:null,as:184}
|
||||
export const parser = LRParser.deserialize({
|
||||
version: 14,
|
||||
states: "9zQYQPOOO#qQPO'#CaOOQO'#Cs'#CsO$zQPO'#CzO&XQQO'#DTO&dQQO'#DZO&nQPO'#D[O&vQPO'#CrO$zQPO'#DgO'QQPO'#DlOOQO'#Cr'#CrOOQO'#Cq'#CqO'VQPO'#CpO$zQPO'#CpOOQO'#Co'#CoO)rQPO'#CnO.kQPO'#CmO0rQPO'#ClOOQO'#Ck'#CkO2mQPO'#CjO4eQPO'#CiO6YQPO'#ChO7wQPO'#CgOOQO'#Cf'#CfO8RQPO'#CeO9iQPO'#C`O9nQPO'#C_OOQO'#EP'#EPQYQPOOO;RQPO'#EQO;WQPO,58{OOQO,59f,59fO;`QPO'#CaO$zQPO,59kOOQO,59o,59oO;hQPO,59oO$zQPO,59qOOQO,59u,59uO;pQPO,59uO;xQPO'#ETO;}QPO'#D^O<VQPO,59vO<[QPO'#CrO<iQPO'#DbO<qQQO,59zO<vQPO,59zO<{QPO,59^O=QQPO,5:ROOQO,5:W,5:WO=VQPO'#DnOOQO,59],59]O=^QPO,59]OOQO,59[,59[O$zQPO,59ZO$zQPO,59YO$zQPO,59XOOQO'#Dv'#DvO$zQPO,59WO$zQPO,59VO$zQPO,59UO$zQPO,59TO$zQPO,59SO$zQPO,59ROOQO'#EO'#EOO$zQPO,59QO$zQPO,58zOOQO,58y,58yOOQO-E7}-E7}OOQO,5:l,5:lOOQO-E8O-E8OO=cQPO1G/VO=hQPO1G/ZO=pQPO1G/ZOOQO1G/Z1G/ZO=xQPO1G/]O>QQPO1G/aO>XQPO1G/aOOQO1G/a1G/aOOQO,5:o,5:oOOQO-E8R-E8RO$zQPO1G/bO$zQPO,59}O>aQPO,59|O>iQPO,59|O$zQPO1G/fO>qQQO1G/fOOQO1G.x1G.xO>vQPO1G/mO?QQPO'#DoOOQO,5:Y,5:YO?YQPO,5:YOOQO1G.w1G.wOOQO1G.u1G.uO?_QPO1G.tOB{QPO1G.sODjQPO1G.rOOQO1G.q1G.qOFeQPO1G.pOH]QPO1G.oOJQQPO1G.nOOQO1G.m1G.mOOQO1G.l1G.lOOQO1G.f1G.fOOQO1G.t1G.tOOQO1G.s1G.sOOQO1G.r1G.rOOQO1G.p1G.pOOQO1G.o1G.oOOQO1G.n1G.nOOQO7+$q7+$qOOQO,5:m,5:mOOQO7+$u7+$uOKdQPO7+$uOOQO-E8P-E8POOQO7+$w7+$wOKlQPO7+$wOOQO,5:n,5:nOOQO7+${7+${OKqQPO7+${OOQO-E8Q-E8QOOQO7+$|7+$|OOQO1G/i1G/iOKxQPO'#DcOOQO,5:p,5:pOK}QPO1G/hOOQO-E8S-E8SOOQO7+%Q7+%QO$zQPO7+%QOOQO'#Dj'#DjOLVQPO'#DiOOQO7+%X7+%XOL[QPO7+%XOLdQPO,5:ZOLkQPO,5:ZOOQO1G/t1G/tOOQO<<Ha<<HaPLsQPO'#EROOQO<<Hc<<HcOOQO<<Hg<<HgP$zQPO'#ESPLxQPO'#EUOOQO<<Hl<<HlO$zQPO,5:TOL}QPO<<HsOMXQPO<<HsOOQO<<Hs<<HsOMaQPO1G/uOOQO1G/o1G/oOOQO,5:q,5:qOOQOAN>_AN>_OMhQPOAN>_OOQO-E8T-E8TOOQOG23yG23yP>yQPO'#EVO$zQPO,59YO$zQPO,59XO$zQPO,59WO$zQPO,59UO$zQPO,59TO$zQPO,59SOMrQPO1G.sONYQPO'#CmONmQPO'#ClO! aQPO'#CiO!!aQPO'#ChO!#dQPO'#CgO$zQPO,59XO$zQPO,59VO$zQPO,59RO$zQPO,59QO!$mQPO1G.rO!%TQPO'#ClO!&eQPO'#CjO!'bQPO'#CeO$zQPO,59WO$zQPO,59VO$zQPO,59UO$zQPO,59TO$zQPO,59SO$zQPO,59RO$zQPO,59QO$zQPO,58zO!(kQPO1G.pO!)RQPO1G.oO!)iQPO1G.nO!*PQPO'#CjO!*zQPO'#CiO!+rQPO'#ChO!,gQPO'#CgO!-XQPO'#CeO!-sQPO'#C`",
|
||||
stateData: "!.V~O!|OSPOS~OUPOhQOiQOjQOkQOlQOmQOoROpROqROrROtSOzTO!PUO!TVO![WO!aXO!d]O!e]O~OofXpfXqfXrfX!TfX!efX!ffX!gfX!hfX!ifX!kfX!lfX!mfX!nfX!ofX!pfX!qfX!}fX~OVmOUfXWTXhfXifXjfXkfXlfXmfXtfXxfXzfX!PfX![fX!afX!dfX!zfX~P!gOUYOhQOiQOjQOkQOlQOmQOoROpROqROrROtSOzTO!PUO!TVO![WO!aXO!d]O!e]O~OUpOuqOvrO~OutO|uO~P$zOUpO!R!QP~OUzO!X|O~P]Oh!QO~OV!TO!T!ROUdXhdXidXjdXkdXldXmdXodXpdXqdXrdXtdXxdXzdX!PdX![dX!adX!ddX!edX!fdX!gdX!hdX!idX!kdX!ldX!mdX!ndX!odX!pdX!qdX!zdX!}dX{dX|dX!XdXvdX!WdX~O!f!VO!g!VOUbXhbXibXjbXkbXlbXmbXobXpbXqbXrbXtbXxbXzbX!PbX!TbX![bX!abX!dbX!ebX!hbX!ibX!kbX!lbX!mbX!nbX!obX!pbX!qbX!zbX!}bXVbX{bX|bX!XbXvbX!WbX~OUaXhaXiaXjaXkaXlaXmaXoaXpaXqaXraXtaXxaXzaX!PaX!TaX![aX!aaX!daX!iaX!kaX!laX!maX!naX!oaX!paX!qaX!zaX!}aX{aX|aX!XaXvaX!WaX~O!e!WO!h!WO~P,_Oo!YOp!YOq!YOr!YO!k!YO!l!YOU`Xh`Xi`Xj`Xk`Xl`Xm`Xt`Xx`Xz`X!P`X!T`X![`X!a`X!d`X!e`X!m`X!n`X!o`X!p`X!q`X!z`X!}`X~O!i!XO~P.uOU^Xh^Xi^Xj^Xk^Xl^Xm^Xo^Xp^Xq^Xr^Xt^Xx^Xz^X!P^X!T^X![^X!a^X!d^X!e^X!n^X!o^X!p^X!q^X!z^X!}^X~O!m![O~P0yOU]Xh]Xi]Xj]Xk]Xl]Xm]Xo]Xp]Xq]Xr]Xt]Xx]Xz]X!P]X!T]X![]X!a]X!d]X!e]X!o]X!p]X!q]X!z]X!}]X~O!n!]O~P2tOU[Xh[Xi[Xj[Xk[Xl[Xm[Xo[Xp[Xq[Xr[Xt[Xx[Xz[X!P[X!T[X![[X!a[X!d[X!e[X!p[X!q[X!z[X!}[X~O!o!^O~P4lOUZXhZXiZXjZXkZXlZXmZXoZXpZXqZXrZXtZXxZXzZX!PZX!TZX![ZX!aZX!dZX!eZX!zZX!}ZX~O!p!_O!q!`O~P6aO!}!aOUXXhXXiXXjXXkXXlXXmXXoXXpXXqXXrXXtXXxXXzXX!PXX!TXX![XX!aXX!dXX!eXX!zXX~OW!cO~Ox!dOURXhRXiRXjRXkRXlRXmRXoRXpRXqRXrRXtRXzRX!PRX!TRX![RX!aRX!dRX!eRX!zRX~OU!fO~OVmOWTa~OVmOWTX~Ov!kOx!iO~O{!mO|!oO~Ox!pO~OUpO!R!QX~O!R!rO~O!W!sOVfX!XfX~P!gO{!tO!X!UX~O!Y!vO~O!X!wO~O!X!xO~Ot!yO~O!X!{O~P$zOU!}O~Ov#aO~OUpOv#cO~Ov#cOx#dO~O{#gO|#fO~O|#iO~P$zO{#jO|#iO~OU#nO!X!Ua~O{#pO!X!Ua~O!Y#sO~Ov#vO!_#tO~P$zO{#xO!X!cX~O!X#zO~O!f!VO!g!VOUbihbiibijbikbilbimbiobipbiqbirbitbixbizbi!Pbi!Tbi![bi!abi!dbi!ebi!hbi!ibi!kbi!lbi!mbi!nbi!obi!pbi!qbi!zbi!}bi{bi|bi!Xbivbi!Wbi~O!h!WOoaipaiqairaitaixai!iai!kai!lai!mai!nai!oai!pai!qai!}ai~OUaihaiiaijaikailaimaizai!Pai!Tai![ai!aai!dai!eai!zai~PAwOt`ix`i!m`i!n`i!o`i!p`i!q`i!}`i~O!i!XOU`ih`ii`ij`ik`il`im`io`ip`iq`ir`iz`i!P`i!T`i![`i!a`i!d`i!e`i!z`i~PDOOt^ix^i!n^i!o^i!p^i!q^i!}^i~O!m![OU^ih^ii^ij^ik^il^im^io^ip^iq^ir^iz^i!P^i!T^i![^i!a^i!d^i!e^i!z^i~PE|Ot]ix]i!o]i!p]i!q]i!}]i~O!n!]OU]ih]ii]ij]ik]il]im]io]ip]iq]ir]iz]i!P]i!T]i![]i!a]i!d]i!e]i!z]i~PGwOt[ix[i!p[i!q[i!}[i~O!o!^OU[ih[ii[ij[ik[il[im[io[ip[iq[ir[iz[i!P[i!T[i![[i!a[i!d[i!e[i!z[i~PIoOUpOv#{O~O|#}O~O|$OO~P$zO!W!sO~OU#nO!X!Ui~O!W$SO~Ov$VOx$TO~O!X!ca~P$zO{$WO!X!ca~OUpO~OU#nO~Ov$ZO!_#tO~P$zOv$ZOx$[O~O!X!ci~P$zOv$^O!_#tO~P$zO!e!WO{ai|ai!Xaivai!Wai~PAwO!e$`O!h$`OVaX!faX!gaX~P,_O!i$aOV`X!f`X!g`X!h`X{`X|`X!X`Xv`X!W`X~P.uO!n$cOV]X!f]X!g]X!h]X!i]X!k]X!l]X!m]X{]X|]X!X]Xv]X!W]X~P2tO!o$dOV[X!f[X!g[X!h[X!i[X!k[X!l[X!m[X!n[X{[X|[X!X[Xv[X!W[X~P4lO!p$eO!q$nOVZX!fZX!gZX!hZX!iZX!kZX!lZX!mZX!nZX!oZX{ZX|ZX!XZXvZX!WZX~P6aO!i$lO{`i|`i!X`iv`i!W`i~PDOOo!YOp!YOq!YOr!YO!i$lO!k!YO!l!YO{`X|`X!m`X!n`X!o`X!p`X!q`X!}`X!X`Xt`Xv`Xx`X!W`X~O!m$mOV^X!f^X!g^X!h^X!i^X!k^X!l^X{^X|^X!X^Xv^X!W^X~P0yOVXX!fXX!gXX!hXX!iXX!kXX!lXX!mXX!nXX!oXX!pXX!qXX{XX|XX!XXXvXX!WXX~P8RO!m$uO{^i|^i!X^iv^i!W^i~PE|O!n$vO{]i|]i!X]iv]i!W]i~PGwO!o$wO{[i|[i!X[iv[i!W[i~PIoO!m$uO{^X|^X!n^X!o^X!p^X!q^X!}^X!X^Xt^Xv^Xx^X!W^X~O!n$vO{]X|]X!o]X!p]X!q]X!}]X!X]Xt]Xv]Xx]X!W]X~O!o$wO{[X|[X!p[X!q[X!}[X!X[Xt[Xv[Xx[X!W[X~O!p$xO!q$yO{ZX|ZX!}ZX!XZXtZXvZXxZX!WZX~O!}!aO{XX|XX!XXXtXXvXXxXX!WXX~OW${O~O!P!R![!a!qklji!_Uk~",
|
||||
goto: "6W!zPPP!{#P#aPPP#m$x%i&`'V(V)Y*`+a,m-z/V0d1m2vPPPPPP2vPPPP2vPPP2vP2vPPP2v2vP4PP2vP4S4VPPP2vP4_4gP2vP4m4pPPPPPP4sPPPPPPP4|5V5]5d5j5t5z6QTkOlSjOlQsSSwUxV#b!i#d#|SiOl]%USUx!i#d#|SjOlQoRQvTQ!OVQ!PWQ!hqQ!ltQ!z!RS#Y!c${Y#h!m#j#x$P$WQ#l!rQ#m!sQ#r!vW#t!y$T$[$_Q$R#sR$X$SUhOl!cW$sR!r!v#su%TTVWqt!R!m!s!y#j#x$P$S$T$W$[$_${!SgORTVWlqt!R!c!m!r!s!v!y#j#s#x$P$S$T$W$[$_${U#W!`$n$yV#X!b$o$zYfOl!`!b!c[$kR!r!v#s$n$oy%STVWqt!R!m!s!y#j#x$P$S$T$W$[$_$y$z${YeOl!`!b!cQ#V!_Q#`$e[$jR!r!v#s$n$oQ%O$xy%RTVWqt!R!m!s!y#j#x$P$S$T$W$[$_$y$z${[dOl!_!`!b!cQ#U!^Q#_$d^$iR!r!v#s$e$n$oQ$}$w{%QTVWqt!R!m!s!y#j#x$P$S$T$W$[$_$x$y$z${^cOl!^!_!`!b!cQ#T!]Q#^$c`$rR!r!v#s$d$e$n$oQ$|$v}%PTVWqt!R!m!s!y#j#x$P$S$T$W$[$_$w$x$y$z${!rbORTVWlqt!R!]!^!_!`!b!c!m!r!s!v!y#j#s#x$P$S$T$W$[$_$c$d$e$n$o$v$w$x$y$z${V#S![$m$ubaOl![!]!^!_!`!b!cQ#R!ZQ#]$bd$hR!r!v#s$c$d$e$m$n$oQ$p$t!R$qTVWqt!R!m!s!y#j#x$P$S$T$W$[$_$u$v$w$x$y$z${!h`OTVWlqt!R!Z![!]!^!_!`!b!c!m!s!y#j#x$P$S$T$W$[$_$t$u$v$w$x$y$z${Q#Q!XQ#[$aQ$f$lg$gR!r!v#s$b$c$d$e$m$n$o#U_ORTVWlqt!R!X!Z![!]!^!_!`!b!c!m!r!s!v!y#j#s#x$P$S$T$W$[$_$a$b$c$d$e$l$m$n$o$t$u$v$w$x$y$z${Q#P!WR#Z$`#Y^ORTVWlqt!R!W!X!Z![!]!^!_!`!b!c!m!r!s!v!y#j#s#x$P$S$T$W$[$_$`$a$b$c$d$e$l$m$n$o$t$u$v$w$x$y$z${Q!U]R#O!V#_[ORTVW]lqt!R!V!W!X!Z![!]!^!_!`!b!c!m!r!s!v!y#j#s#x$P$S$T$W$[$_$`$a$b$c$d$e$l$m$n$o$t$u$v$w$x$y$z${#_ZORTVW]lqt!R!V!W!X!Z![!]!^!_!`!b!c!m!r!s!v!y#j#s#x$P$S$T$W$[$_$`$a$b$c$d$e$l$m$n$o$t$u$v$w$x$y$z${#_YORTVW]lqt!R!V!W!X!Z![!]!^!_!`!b!c!m!r!s!v!y#j#s#x$P$S$T$W$[$_$`$a$b$c$d$e$l$m$n$o$t$u$v$w$x$y$z${RyUR}VQ{VV#o!t#p$QQ#w!yV$Y$T$[$_X#u!y$T$[$_R!S[R!|!RQ!ZaQ$b$hR$t$qQ!bhQ$o$sR$z%TQlOR!elSnPpR!gnQ!jsR#e!jQ!nvS#k!n#yR#y!zQxUR!qxQ!u{R#q!uQ$U#wR$]$U",
|
||||
nodeNames: "⚠ Comment Source Statement FieldDefinition FieldPath Identifier Dot Equal Expression AsExpression DefaultExpression PatchExpression ComposeExpression LogicalOrExpression LogicalAndExpression ComparisonExpression ConcatExpression AdditiveExpression MultiplicativeExpression UnaryExpression PostfixExpression PrimaryExpression Literal String Integer Float True False Regex ComparisonConstraint Lt Lte Gt Gte MapConstraint LBrace Ellipsis RBrace Object Semicolon ArrayConstraint LBracket Comma RBracket Array LetExpression Let FieldDefinitionList In FunctionExpression LParen ParameterList Parameter Colon RParen Arrow MatchExpression Match MatchArm Pattern Underscore ImportExpression Import CallSuffix ArgumentList Bang Minus Star Slash Plus PlusPlus CompareOperator EqualEqual BangEqual AmpAmp PipePipe Amp SlashSlash Default As",
|
||||
maxTerm: 91,
|
||||
states: ":|QYQPOOO#qQPO'#CaOOQO'#Cs'#CsO$zQPO'#CzO&XQQO'#DTO&dQQO'#D[O&nQPO'#D]O&vQPO'#CrO$zQPO'#DhO'QQPO'#DmOOQO'#Cr'#CrOOQO'#Cq'#CqO'VQPO'#CpO$zQPO'#CpOOQO'#Co'#CoO)rQPO'#CnO.kQPO'#CmO0rQPO'#ClOOQO'#Ck'#CkO2mQPO'#CjO4eQPO'#CiO6YQPO'#ChO7wQPO'#CgOOQO'#Cf'#CfO8RQPO'#CeO9iQPO'#C`O9nQPO'#C_OOQO'#EQ'#EQQYQPOOO;RQPO'#ERO;WQPO,58{OOQO,59f,59fO;`QPO'#CaO$zQPO,59kOOQO,59o,59oO;hQPO,59oO$zQPO,59rOOQO,59v,59vO;pQPO,59vO;xQPO'#EUO;}QPO'#D_O<VQPO,59wO<[QPO'#CrO<iQPO'#DcO<qQQO,59{O<vQPO,59{O<{QPO,59^O=QQPO,5:SOOQO,5:X,5:XO=VQPO'#DoOOQO,59],59]O=^QPO,59]OOQO,59[,59[O$zQPO,59ZO$zQPO,59YO$zQPO,59XOOQO'#Dw'#DwO$zQPO,59WO$zQPO,59VO$zQPO,59UO$zQPO,59TO$zQPO,59SO$zQPO,59ROOQO'#EP'#EPO$zQPO,59QO$zQPO,58zOOQO,58y,58yOOQO-E8O-E8OOOQO,5:m,5:mOOQO-E8P-E8PO=cQPO1G/VO=hQQO1G/ZO=sQPO1G/ZOOQO1G/Z1G/ZO={QPO1G/^O>TQPO1G/bO>[QPO1G/bOOQO1G/b1G/bOOQO,5:p,5:pOOQO-E8S-E8SO$zQPO1G/cO$zQPO,5:OO>dQPO,59}O>lQPO,59}O$zQPO1G/gO>tQQO1G/gOOQO1G.x1G.xO>yQPO1G/nO?TQPO'#DpOOQO,5:Z,5:ZO?]QPO,5:ZOOQO1G.w1G.wOOQO1G.u1G.uO?bQPO1G.tOCOQPO1G.sODmQPO1G.rOOQO1G.q1G.qOFhQPO1G.pOH`QPO1G.oOJTQPO1G.nOOQO1G.m1G.mOOQO1G.l1G.lOOQO1G.f1G.fOOQO1G.t1G.tOOQO1G.s1G.sOOQO1G.r1G.rOOQO1G.p1G.pOOQO1G.o1G.oOOQO1G.n1G.nOOQO7+$q7+$qOOQO,5:n,5:nO$zQPO'#DVOOQO7+$u7+$uOKgQPO7+$uOKoQQO7+$uOOQO-E8Q-E8QOOQO7+$x7+$xOKzQPO7+$xOOQO,5:o,5:oOOQO7+$|7+$|OLPQPO7+$|OOQO-E8R-E8ROOQO7+$}7+$}OOQO1G/j1G/jOLWQPO'#DdOOQO,5:q,5:qOL]QPO1G/iOOQO-E8T-E8TOOQO7+%R7+%RO$zQPO7+%ROOQO'#Dk'#DkOLeQPO'#DjOOQO7+%Y7+%YOLjQPO7+%YOLrQPO,5:[OLyQPO,5:[OOQO1G/u1G/uOOQO,59q,59qOOQO<<Ha<<HaOMRQPO<<HaOMWQPO<<HaPM`QPO'#ESOOQO<<Hd<<HdOOQO<<Hh<<HhP$zQPO'#ETPMeQPO'#EVOOQO<<Hm<<HmO$zQPO,5:UOMjQPO<<HtOMtQPO<<HtOOQO<<Ht<<HtOM|QPO1G/vOOQOAN={AN={ONTQPOAN={OOQO1G/p1G/pOOQO,5:r,5:rOOQOAN>`AN>`ONYQPOAN>`OOQO-E8U-E8UOOQOG23gG23gOOQOG23zG23zP>|QPO'#EWO$zQPO,59YO$zQPO,59XO$zQPO,59WO$zQPO,59UO$zQPO,59TO$zQPO,59SONdQPO1G.sONzQPO'#CmO! _QPO'#ClO!!RQPO'#CiO!#RQPO'#ChO!$UQPO'#CgO$zQPO,59XO$zQPO,59VO$zQPO,59RO$zQPO,59QO!%_QPO1G.rO!%uQPO'#ClO!'VQPO'#CjO!(SQPO'#CeO$zQPO,59WO$zQPO,59VO$zQPO,59UO$zQPO,59TO$zQPO,59SO$zQPO,59RO$zQPO,59QO$zQPO,58zO!)]QPO1G.pO!)sQPO1G.oO!*ZQPO1G.nO!*qQPO'#CjO!+lQPO'#CiO!,dQPO'#ChO!-XQPO'#CgO!-yQPO'#CeO!.eQPO'#C`",
|
||||
stateData: "!.w~O!}OSPOS~OUPOhQOiQOjQOkQOlQOmQOoROpROqROrROtSO{TO!QUO!UVO!]WO!bXO!e]O!f]O~OofXpfXqfXrfX!UfX!ffX!gfX!hfX!ifX!jfX!lfX!mfX!nfX!ofX!pfX!qfX!rfX#OfX~OVmOUfXWTXhfXifXjfXkfXlfXmfXtfXxfX{fX!QfX!]fX!bfX!efX!{fX~P!gOUYOhQOiQOjQOkQOlQOmQOoROpROqROrROtSO{TO!QUO!UVO!]WO!bXO!e]O!f]O~OUpOuqOvrO~OutO}uO~P$zOUpO!S!RP~OUzO!Y|O~P]Oh!QO~OV!TO!U!ROUdXhdXidXjdXkdXldXmdXodXpdXqdXrdXtdXxdX{dX!QdX!]dX!bdX!edX!fdX!gdX!hdX!idX!jdX!ldX!mdX!ndX!odX!pdX!qdX!rdX!{dX#OdX|dX}dX!YdXvdX!XdX~O!g!VO!h!VOUbXhbXibXjbXkbXlbXmbXobXpbXqbXrbXtbXxbX{bX!QbX!UbX!]bX!bbX!ebX!fbX!ibX!jbX!lbX!mbX!nbX!obX!pbX!qbX!rbX!{bX#ObXVbX|bX}bX!YbXvbX!XbX~OUaXhaXiaXjaXkaXlaXmaXoaXpaXqaXraXtaXxaX{aX!QaX!UaX!]aX!baX!eaX!jaX!laX!maX!naX!oaX!paX!qaX!raX!{aX#OaX|aX}aX!YaXvaX!XaX~O!f!WO!i!WO~P,_Oo!YOp!YOq!YOr!YO!l!YO!m!YOU`Xh`Xi`Xj`Xk`Xl`Xm`Xt`Xx`X{`X!Q`X!U`X!]`X!b`X!e`X!f`X!n`X!o`X!p`X!q`X!r`X!{`X#O`X~O!j!XO~P.uOU^Xh^Xi^Xj^Xk^Xl^Xm^Xo^Xp^Xq^Xr^Xt^Xx^X{^X!Q^X!U^X!]^X!b^X!e^X!f^X!o^X!p^X!q^X!r^X!{^X#O^X~O!n![O~P0yOU]Xh]Xi]Xj]Xk]Xl]Xm]Xo]Xp]Xq]Xr]Xt]Xx]X{]X!Q]X!U]X!]]X!b]X!e]X!f]X!p]X!q]X!r]X!{]X#O]X~O!o!]O~P2tOU[Xh[Xi[Xj[Xk[Xl[Xm[Xo[Xp[Xq[Xr[Xt[Xx[X{[X!Q[X!U[X!][X!b[X!e[X!f[X!q[X!r[X!{[X#O[X~O!p!^O~P4lOUZXhZXiZXjZXkZXlZXmZXoZXpZXqZXrZXtZXxZX{ZX!QZX!UZX!]ZX!bZX!eZX!fZX!{ZX#OZX~O!q!_O!r!`O~P6aO#O!aOUXXhXXiXXjXXkXXlXXmXXoXXpXXqXXrXXtXXxXX{XX!QXX!UXX!]XX!bXX!eXX!fXX!{XX~OW!cO~Ox!dOURXhRXiRXjRXkRXlRXmRXoRXpRXqRXrRXtRX{RX!QRX!URX!]RX!bRX!eRX!fRX!{RX~OU!fO~OVmOWTa~OVmOWTX~Ov!kOx!iO~O|!mO}!oO~Ox!pO~OUpO!S!RX~O!S!rO~O!X!sOVfX!YfX~P!gO|!tO!Y!VX~O!Z!vO~O!Y!wO~O!Y!xO~Ot!yO~O!Y!{O~P$zOU!}O~Ov#aO~OUpOu#cOv#dO~Ov#dOx#fO~O|#iO}#hO~O}#kO~P$zO|#lO}#kO~OU#pO!Y!Va~O|#rO!Y!Va~O!Z#uO~Ov#xO!`#vO~P$zO|#zO!Y!dX~O!Y#|O~O!g!VO!h!VOUbihbiibijbikbilbimbiobipbiqbirbitbixbi{bi!Qbi!Ubi!]bi!bbi!ebi!fbi!ibi!jbi!lbi!mbi!nbi!obi!pbi!qbi!rbi!{bi#Obi|bi}bi!Ybivbi!Xbi~O!i!WOoaipaiqairaitaixai!jai!lai!mai!nai!oai!pai!qai!rai#Oai~OUaihaiiaijaikailaimai{ai!Qai!Uai!]ai!bai!eai!fai!{ai~PAzOt`ix`i!n`i!o`i!p`i!q`i!r`i#O`i~O!j!XOU`ih`ii`ij`ik`il`im`io`ip`iq`ir`i{`i!Q`i!U`i!]`i!b`i!e`i!f`i!{`i~PDROt^ix^i!o^i!p^i!q^i!r^i#O^i~O!n![OU^ih^ii^ij^ik^il^im^io^ip^iq^ir^i{^i!Q^i!U^i!]^i!b^i!e^i!f^i!{^i~PFPOt]ix]i!p]i!q]i!r]i#O]i~O!o!]OU]ih]ii]ij]ik]il]im]io]ip]iq]ir]i{]i!Q]i!U]i!]]i!b]i!e]i!f]i!{]i~PGzOt[ix[i!q[i!r[i#O[i~O!p!^OU[ih[ii[ij[ik[il[im[io[ip[iq[ir[i{[i!Q[i!U[i!][i!b[i!e[i!f[i!{[i~PIrOv$OOx$PO~OUpOu#cOv$OO~O}$SO~O}$TO~P$zO!X!sO~OU#pO!Y!Vi~O!X$XO~Ov$[Ox$YO~O!Y!da~P$zO|$]O!Y!da~Ov$^O~Ov$^Ox$_O~OUpO~OU#pO~Ov$bO!`#vO~P$zOv$bOx$cO~O!Y!di~P$zOv$eO~Ov$fO!`#vO~P$zO!f!WO|ai}ai!Yaivai!Xai~PAzO!f$hO!i$hOVaX!gaX!haX~P,_O!j$iOV`X!g`X!h`X!i`X|`X}`X!Y`Xv`X!X`X~P.uO!o$kOV]X!g]X!h]X!i]X!j]X!l]X!m]X!n]X|]X}]X!Y]Xv]X!X]X~P2tO!p$lOV[X!g[X!h[X!i[X!j[X!l[X!m[X!n[X!o[X|[X}[X!Y[Xv[X!X[X~P4lO!q$mO!r$vOVZX!gZX!hZX!iZX!jZX!lZX!mZX!nZX!oZX!pZX|ZX}ZX!YZXvZX!XZX~P6aO!j$tO|`i}`i!Y`iv`i!X`i~PDROo!YOp!YOq!YOr!YO!j$tO!l!YO!m!YO|`X}`X!n`X!o`X!p`X!q`X!r`X#O`X!Y`Xt`Xv`Xx`X!X`X~O!n$uOV^X!g^X!h^X!i^X!j^X!l^X!m^X|^X}^X!Y^Xv^X!X^X~P0yOVXX!gXX!hXX!iXX!jXX!lXX!mXX!nXX!oXX!pXX!qXX!rXX|XX}XX!YXXvXX!XXX~P8RO!n$}O|^i}^i!Y^iv^i!X^i~PFPO!o%OO|]i}]i!Y]iv]i!X]i~PGzO!p%PO|[i}[i!Y[iv[i!X[i~PIrO!n$}O|^X}^X!o^X!p^X!q^X!r^X#O^X!Y^Xt^Xv^Xx^X!X^X~O!o%OO|]X}]X!p]X!q]X!r]X#O]X!Y]Xt]Xv]Xx]X!X]X~O!p%PO|[X}[X!q[X!r[X#O[X!Y[Xt[Xv[Xx[X!X[X~O!q%QO!r%RO|ZX}ZX#OZX!YZXtZXvZXxZX!XZX~O#O!aO|XX}XX!YXXtXXvXXxXX!XXX~OW%TO~O!Q!S!]!b!rklji!`Uk~",
|
||||
goto: "6p!{PPP!|#Q#bPPP#n$|%n&f'^(_)c*j+l,y.X/e0s1}3XPPPPPP3XPPPP3XPPP3XP4c3XPPP3X3XP4iP3XP4l4oPPP3XP4w5PP3XP5V5YPPPPPP5]PPPPPPP5f5o5u5|6S6^6d6jTkOlSjOlQsSSwUxV#b!i#f$RSiOl]%^SUx!i#f$RSjOlQoRQvTQ!OVQ!PWQ!hqQ!ltQ!z!RS#Y!c%TY#j!m#l#z$U$]Q#n!rQ#o!sQ#t!vW#v!y$Y$c$gQ#}#cQ$W#uR$`$XUhOl!cW${R!r!v#uw%]TVWqt!R!m!s!y#c#l#z$U$X$Y$]$c$g%T!UgORTVWlqt!R!c!m!r!s!v!y#c#l#u#z$U$X$Y$]$c$g%TU#W!`$v%RV#X!b$w%SYfOl!`!b!c[$sR!r!v#u$v$w{%[TVWqt!R!m!s!y#c#l#z$U$X$Y$]$c$g%R%S%TYeOl!`!b!cQ#V!_Q#`$m[$rR!r!v#u$v$wQ%W%Q{%ZTVWqt!R!m!s!y#c#l#z$U$X$Y$]$c$g%R%S%T[dOl!_!`!b!cQ#U!^Q#_$l^$qR!r!v#u$m$v$wQ%V%P}%YTVWqt!R!m!s!y#c#l#z$U$X$Y$]$c$g%Q%R%S%T^cOl!^!_!`!b!cQ#T!]Q#^$k`$zR!r!v#u$l$m$v$wQ%U%O!P%XTVWqt!R!m!s!y#c#l#z$U$X$Y$]$c$g%P%Q%R%S%T!tbORTVWlqt!R!]!^!_!`!b!c!m!r!s!v!y#c#l#u#z$U$X$Y$]$c$g$k$l$m$v$w%O%P%Q%R%S%TV#S![$u$}baOl![!]!^!_!`!b!cQ#R!ZQ#]$jd$pR!r!v#u$k$l$m$u$v$wQ$x$|!T$yTVWqt!R!m!s!y#c#l#z$U$X$Y$]$c$g$}%O%P%Q%R%S%T!j`OTVWlqt!R!Z![!]!^!_!`!b!c!m!s!y#c#l#z$U$X$Y$]$c$g$|$}%O%P%Q%R%S%TQ#Q!XQ#[$iQ$n$tg$oR!r!v#u$j$k$l$m$u$v$w#W_ORTVWlqt!R!X!Z![!]!^!_!`!b!c!m!r!s!v!y#c#l#u#z$U$X$Y$]$c$g$i$j$k$l$m$t$u$v$w$|$}%O%P%Q%R%S%TQ#P!WR#Z$h#[^ORTVWlqt!R!W!X!Z![!]!^!_!`!b!c!m!r!s!v!y#c#l#u#z$U$X$Y$]$c$g$h$i$j$k$l$m$t$u$v$w$|$}%O%P%Q%R%S%TQ!U]R#O!V#a[ORTVW]lqt!R!V!W!X!Z![!]!^!_!`!b!c!m!r!s!v!y#c#l#u#z$U$X$Y$]$c$g$h$i$j$k$l$m$t$u$v$w$|$}%O%P%Q%R%S%T#aZORTVW]lqt!R!V!W!X!Z![!]!^!_!`!b!c!m!r!s!v!y#c#l#u#z$U$X$Y$]$c$g$h$i$j$k$l$m$t$u$v$w$|$}%O%P%Q%R%S%T#aYORTVW]lqt!R!V!W!X!Z![!]!^!_!`!b!c!m!r!s!v!y#c#l#u#z$U$X$Y$]$c$g$h$i$j$k$l$m$t$u$v$w$|$}%O%P%Q%R%S%TQ#e!iR$Q#fRyUR}VQ{VV#q!t#r$VQ#y!yV$a$Y$c$gX#w!y$Y$c$gR!S[R!|!RQ!ZaQ$j$pR$|$yQ!bhQ$w${R%S%]QlOR!elSnPpR!gnQ!jsR#g!jQ!nvS#m!n#{R#{!zQxUR!qxQ!u{R#s!uQ$Z#yR$d$Z",
|
||||
nodeNames: "⚠ Comment Source Statement FieldDefinition FieldPath Identifier Dot Equal Expression AsExpression DefaultExpression PatchExpression ComposeExpression LogicalOrExpression LogicalAndExpression ComparisonExpression ConcatExpression AdditiveExpression MultiplicativeExpression UnaryExpression PostfixExpression PrimaryExpression Literal String Integer Float True False Regex ComparisonConstraint Lt Lte Gt Gte MapConstraint LBrace Ellipsis RBrace Object Semicolon ObjectRest ArrayConstraint LBracket Comma RBracket Array LetExpression Let FieldDefinitionList In FunctionExpression LParen ParameterList Parameter Colon RParen Arrow MatchExpression Match MatchArm Pattern Underscore ImportExpression Import CallSuffix ArgumentList Bang Minus Star Slash Plus PlusPlus CompareOperator EqualEqual BangEqual AmpAmp PipePipe Amp SlashSlash Default As",
|
||||
maxTerm: 92,
|
||||
skippedNodes: [0,1],
|
||||
repeatNodeCount: 7,
|
||||
tokenData: "=c~R!PX^$Upq$Uqr$yrs%Wst&zvw'cxy'pyz'uz{'z{|(P|}(^}!O(c!O!P(h!P!Q({!Q![+c![!]+|!]!^,R!^!_,W!_!`,e!`!a,z!c!}-X!}#O-j#P#Q-o#R#S-t#T#W-X#W#X.X#X#Y-X#Y#Z1i#Z#]-X#]#^3y#^#`-X#`#a7]#a#b8p#b#h-X#h#i;Q#i#o-X#o#p<|#p#q=R#q#r=^#y#z$U$f$g$U#BY#BZ$U$IS$I_$U$I|$JO$U$JT$JU$U$KV$KW$U&FU&FV$U~$ZY!|~X^$Upq$U#y#z$U$f$g$U#BY#BZ$U$IS$I_$U$I|$JO$U$JT$JU$U$KV$KW$U&FU&FV$U~%OP!d~!_!`%R~%WO!l~~%ZWOY%WZr%Wrs%ss#O%W#O#P%x#P;'S%W;'S;=`&t<%lO%W~%xOh~~%{RO;'S%W;'S;=`&U;=`O%W~&XXOY%WZr%Wrs%ss#O%W#O#P%x#P;'S%W;'S;=`&t;=`<%l%W<%lO%W~&wP;=`<%l%W~'PSP~OY&zZ;'S&z;'S;=`']<%lO&z~'`P;=`<%l&z~'hP!o~vw'k~'pO!m~~'uO!T~~'zO!X~~(PO!f~~(UP!h~{|(X~(^O!i~~(cO{~~(hO!e~R(mPVP!O!P(pQ(sP!O!P(vQ({OuQ~)QW!g~OY)jZ!P)j!P!Q+^!Q#O)j#O#P*[#P;'S)j;'S;=`+W<%lO)j~)mWOY)jZ!P)j!P!Q*V!Q#O)j#O#P*[#P;'S)j;'S;=`+W<%lO)j~*[Om~~*_RO;'S)j;'S;=`*h;=`O)j~*kXOY)jZ!P)j!P!Q*V!Q#O)j#O#P*[#P;'S)j;'S;=`+W;=`<%l)j<%lO)j~+ZP;=`<%l)j~+cO!p~~+hQi~!O!P+n!Q![+c~+qP!Q![+t~+yPj~!Q![+t~,RO!W~~,WOx~~,]Po~!_!`,`~,eOp~~,jQWP!_!`,p!`!a,u~,uO!k~Q,zO!YQ~-PPq~!_!`-S~-XOr~~-^SU~!Q![-X!c!}-X#R#S-X#T#o-X~-oOz~~-tO|~~-{S!_~U~!Q![-X!c!}-X#R#S-X#T#o-X~.^UU~!Q![-X!c!}-X#R#S-X#T#X-X#X#Y.p#Y#o-X~.uUU~!Q![-X!c!}-X#R#S-X#T#Y-X#Y#Z/X#Z#o-X~/^TU~!Q![-X!c!}-X#R#S-X#T#U/m#U#o-X~/rUU~!Q![-X!c!}-X#R#S-X#T#i-X#i#j0U#j#o-X~0ZUU~!Q![-X!c!}-X#R#S-X#T#`-X#`#a0m#a#o-X~0rUU~!Q![-X!c!}-X#R#S-X#T#h-X#h#i1U#i#o-X~1]S!q~U~!Q![-X!c!}-X#R#S-X#T#o-X~1nTU~!Q![-X!c!}-X#R#S-X#T#U1}#U#o-X~2SUU~!Q![-X!c!}-X#R#S-X#T#`-X#`#a2f#a#o-X~2kUU~!Q![-X!c!}-X#R#S-X#T#g-X#g#h2}#h#o-X~3SUU~!Q![-X!c!}-X#R#S-X#T#X-X#X#Y3f#Y#o-X~3mSl~U~!Q![-X!c!}-X#R#S-X#T#o-X~4OVU~!Q![-X!c!}-X#R#S-X#T#a-X#a#b4e#b#c6x#c#o-X~4jUU~!Q![-X!c!}-X#R#S-X#T#d-X#d#e4|#e#o-X~5RUU~!Q![-X!c!}-X#R#S-X#T#c-X#c#d5e#d#o-X~5jUU~!Q![-X!c!}-X#R#S-X#T#f-X#f#g5|#g#o-X~6RUU~!Q![-X!c!}-X#R#S-X#T#h-X#h#i6e#i#o-X~6lS!a~U~!Q![-X!c!}-X#R#S-X#T#o-X~7PS!R~U~!Q![-X!c!}-X#R#S-X#T#o-X~7bUU~!Q![-X!c!}-X#R#S-X#T#X-X#X#Y7t#Y#o-X~7yUU~!Q![-X!c!}-X#R#S-X#T#h-X#h#i8]#i#o-X~8dS!P~U~!Q![-X!c!}-X#R#S-X#T#o-X~8uTU~!Q![-X!c!}-X#R#S-X#T#U9U#U#o-X~9ZUU~!Q![-X!c!}-X#R#S-X#T#h-X#h#i9m#i#o-X~9rUU~!Q![-X!c!}-X#R#S-X#T#V-X#V#W:U#W#o-X~:ZUU~!Q![-X!c!}-X#R#S-X#T#[-X#[#]:m#]#o-X~:tS![~U~!Q![-X!c!}-X#R#S-X#T#o-X~;VUU~!Q![-X!c!}-X#R#S-X#T#f-X#f#g;i#g#o-X~;nUU~!Q![-X!c!}-X#R#S-X#T#i-X#i#j<Q#j#o-X~<VUU~!Q![-X!c!}-X#R#S-X#T#X-X#X#Y<i#Y#o-X~<pSk~U~!Q![-X!c!}-X#R#S-X#T#o-X~=ROt~~=UP#p#q=X~=^O!n~~=cOv~",
|
||||
tokenData: "=c~R!PX^$Upq$Uqr$yrs%Wst&zvw'cxy'pyz'uz{'z{|(P|}(^}!O(c!O!P(h!P!Q({!Q![+c![!]+|!]!^,R!^!_,W!_!`,e!`!a,z!c!}-X!}#O-j#P#Q-o#R#S-t#T#W-X#W#X.X#X#Y-X#Y#Z1i#Z#]-X#]#^3y#^#`-X#`#a7]#a#b8p#b#h-X#h#i;Q#i#o-X#o#p<|#p#q=R#q#r=^#y#z$U$f$g$U#BY#BZ$U$IS$I_$U$I|$JO$U$JT$JU$U$KV$KW$U&FU&FV$U~$ZY!}~X^$Upq$U#y#z$U$f$g$U#BY#BZ$U$IS$I_$U$I|$JO$U$JT$JU$U$KV$KW$U&FU&FV$U~%OP!e~!_!`%R~%WO!m~~%ZWOY%WZr%Wrs%ss#O%W#O#P%x#P;'S%W;'S;=`&t<%lO%W~%xOh~~%{RO;'S%W;'S;=`&U;=`O%W~&XXOY%WZr%Wrs%ss#O%W#O#P%x#P;'S%W;'S;=`&t;=`<%l%W<%lO%W~&wP;=`<%l%W~'PSP~OY&zZ;'S&z;'S;=`']<%lO&z~'`P;=`<%l&z~'hP!p~vw'k~'pO!n~~'uO!U~~'zO!Y~~(PO!g~~(UP!i~{|(X~(^O!j~~(cO|~~(hO!f~R(mPVP!O!P(pQ(sP!O!P(vQ({OuQ~)QW!h~OY)jZ!P)j!P!Q+^!Q#O)j#O#P*[#P;'S)j;'S;=`+W<%lO)j~)mWOY)jZ!P)j!P!Q*V!Q#O)j#O#P*[#P;'S)j;'S;=`+W<%lO)j~*[Om~~*_RO;'S)j;'S;=`*h;=`O)j~*kXOY)jZ!P)j!P!Q*V!Q#O)j#O#P*[#P;'S)j;'S;=`+W;=`<%l)j<%lO)j~+ZP;=`<%l)j~+cO!q~~+hQi~!O!P+n!Q![+c~+qP!Q![+t~+yPj~!Q![+t~,RO!X~~,WOx~~,]Po~!_!`,`~,eOp~~,jQWP!_!`,p!`!a,u~,uO!l~Q,zO!ZQ~-PPq~!_!`-S~-XOr~~-^SU~!Q![-X!c!}-X#R#S-X#T#o-X~-oO{~~-tO}~~-{S!`~U~!Q![-X!c!}-X#R#S-X#T#o-X~.^UU~!Q![-X!c!}-X#R#S-X#T#X-X#X#Y.p#Y#o-X~.uUU~!Q![-X!c!}-X#R#S-X#T#Y-X#Y#Z/X#Z#o-X~/^TU~!Q![-X!c!}-X#R#S-X#T#U/m#U#o-X~/rUU~!Q![-X!c!}-X#R#S-X#T#i-X#i#j0U#j#o-X~0ZUU~!Q![-X!c!}-X#R#S-X#T#`-X#`#a0m#a#o-X~0rUU~!Q![-X!c!}-X#R#S-X#T#h-X#h#i1U#i#o-X~1]S!r~U~!Q![-X!c!}-X#R#S-X#T#o-X~1nTU~!Q![-X!c!}-X#R#S-X#T#U1}#U#o-X~2SUU~!Q![-X!c!}-X#R#S-X#T#`-X#`#a2f#a#o-X~2kUU~!Q![-X!c!}-X#R#S-X#T#g-X#g#h2}#h#o-X~3SUU~!Q![-X!c!}-X#R#S-X#T#X-X#X#Y3f#Y#o-X~3mSl~U~!Q![-X!c!}-X#R#S-X#T#o-X~4OVU~!Q![-X!c!}-X#R#S-X#T#a-X#a#b4e#b#c6x#c#o-X~4jUU~!Q![-X!c!}-X#R#S-X#T#d-X#d#e4|#e#o-X~5RUU~!Q![-X!c!}-X#R#S-X#T#c-X#c#d5e#d#o-X~5jUU~!Q![-X!c!}-X#R#S-X#T#f-X#f#g5|#g#o-X~6RUU~!Q![-X!c!}-X#R#S-X#T#h-X#h#i6e#i#o-X~6lS!b~U~!Q![-X!c!}-X#R#S-X#T#o-X~7PS!S~U~!Q![-X!c!}-X#R#S-X#T#o-X~7bUU~!Q![-X!c!}-X#R#S-X#T#X-X#X#Y7t#Y#o-X~7yUU~!Q![-X!c!}-X#R#S-X#T#h-X#h#i8]#i#o-X~8dS!Q~U~!Q![-X!c!}-X#R#S-X#T#o-X~8uTU~!Q![-X!c!}-X#R#S-X#T#U9U#U#o-X~9ZUU~!Q![-X!c!}-X#R#S-X#T#h-X#h#i9m#i#o-X~9rUU~!Q![-X!c!}-X#R#S-X#T#V-X#V#W:U#W#o-X~:ZUU~!Q![-X!c!}-X#R#S-X#T#[-X#[#]:m#]#o-X~:tS!]~U~!Q![-X!c!}-X#R#S-X#T#o-X~;VUU~!Q![-X!c!}-X#R#S-X#T#f-X#f#g;i#g#o-X~;nUU~!Q![-X!c!}-X#R#S-X#T#i-X#i#j<Q#j#o-X~<VUU~!Q![-X!c!}-X#R#S-X#T#X-X#X#Y<i#Y#o-X~<pSk~U~!Q![-X!c!}-X#R#S-X#T#o-X~=ROt~~=UP#p#q=X~=^O!o~~=cOv~",
|
||||
tokenizers: [0, 1],
|
||||
topRules: {"Source":[0,2]},
|
||||
specialized: [{term: 6, get: (value) => spec_Identifier[value] || -1}],
|
||||
tokenPrec: 2708
|
||||
tokenPrec: 2740
|
||||
})
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
// This file was generated by lezer-generator. You probably shouldn't edit it.
|
||||
import {LRParser} from "@lezer/lr"
|
||||
const spec_Identifier = {__proto__:null,as:182}
|
||||
const spec_Identifier = {__proto__:null,as:184}
|
||||
export const parser = LRParser.deserialize({
|
||||
version: 14,
|
||||
states: "9zQYQPOOO#qQPO'#CaOOQO'#Cs'#CsO$zQPO'#CzO&XQQO'#DTO&dQQO'#DZO&nQPO'#D[O&vQPO'#CrO$zQPO'#DgO'QQPO'#DlOOQO'#Cr'#CrOOQO'#Cq'#CqO'VQPO'#CpO$zQPO'#CpOOQO'#Co'#CoO)rQPO'#CnO.kQPO'#CmO0rQPO'#ClOOQO'#Ck'#CkO2mQPO'#CjO4eQPO'#CiO6YQPO'#ChO7wQPO'#CgOOQO'#Cf'#CfO8RQPO'#CeO9iQPO'#C`O9nQPO'#C_OOQO'#EP'#EPQYQPOOO;RQPO'#EQO;WQPO,58{OOQO,59f,59fO;`QPO'#CaO$zQPO,59kOOQO,59o,59oO;hQPO,59oO$zQPO,59qOOQO,59u,59uO;pQPO,59uO;xQPO'#ETO;}QPO'#D^O<VQPO,59vO<[QPO'#CrO<iQPO'#DbO<qQQO,59zO<vQPO,59zO<{QPO,59^O=QQPO,5:ROOQO,5:W,5:WO=VQPO'#DnOOQO,59],59]O=^QPO,59]OOQO,59[,59[O$zQPO,59ZO$zQPO,59YO$zQPO,59XOOQO'#Dv'#DvO$zQPO,59WO$zQPO,59VO$zQPO,59UO$zQPO,59TO$zQPO,59SO$zQPO,59ROOQO'#EO'#EOO$zQPO,59QO$zQPO,58zOOQO,58y,58yOOQO-E7}-E7}OOQO,5:l,5:lOOQO-E8O-E8OO=cQPO1G/VO=hQPO1G/ZO=pQPO1G/ZOOQO1G/Z1G/ZO=xQPO1G/]O>QQPO1G/aO>XQPO1G/aOOQO1G/a1G/aOOQO,5:o,5:oOOQO-E8R-E8RO$zQPO1G/bO$zQPO,59}O>aQPO,59|O>iQPO,59|O$zQPO1G/fO>qQQO1G/fOOQO1G.x1G.xO>vQPO1G/mO?QQPO'#DoOOQO,5:Y,5:YO?YQPO,5:YOOQO1G.w1G.wOOQO1G.u1G.uO?_QPO1G.tOB{QPO1G.sODjQPO1G.rOOQO1G.q1G.qOFeQPO1G.pOH]QPO1G.oOJQQPO1G.nOOQO1G.m1G.mOOQO1G.l1G.lOOQO1G.f1G.fOOQO1G.t1G.tOOQO1G.s1G.sOOQO1G.r1G.rOOQO1G.p1G.pOOQO1G.o1G.oOOQO1G.n1G.nOOQO7+$q7+$qOOQO,5:m,5:mOOQO7+$u7+$uOKdQPO7+$uOOQO-E8P-E8POOQO7+$w7+$wOKlQPO7+$wOOQO,5:n,5:nOOQO7+${7+${OKqQPO7+${OOQO-E8Q-E8QOOQO7+$|7+$|OOQO1G/i1G/iOKxQPO'#DcOOQO,5:p,5:pOK}QPO1G/hOOQO-E8S-E8SOOQO7+%Q7+%QO$zQPO7+%QOOQO'#Dj'#DjOLVQPO'#DiOOQO7+%X7+%XOL[QPO7+%XOLdQPO,5:ZOLkQPO,5:ZOOQO1G/t1G/tOOQO<<Ha<<HaPLsQPO'#EROOQO<<Hc<<HcOOQO<<Hg<<HgP$zQPO'#ESPLxQPO'#EUOOQO<<Hl<<HlO$zQPO,5:TOL}QPO<<HsOMXQPO<<HsOOQO<<Hs<<HsOMaQPO1G/uOOQO1G/o1G/oOOQO,5:q,5:qOOQOAN>_AN>_OMhQPOAN>_OOQO-E8T-E8TOOQOG23yG23yP>yQPO'#EVO$zQPO,59YO$zQPO,59XO$zQPO,59WO$zQPO,59UO$zQPO,59TO$zQPO,59SOMrQPO1G.sONYQPO'#CmONmQPO'#ClO! aQPO'#CiO!!aQPO'#ChO!#dQPO'#CgO$zQPO,59XO$zQPO,59VO$zQPO,59RO$zQPO,59QO!$mQPO1G.rO!%TQPO'#ClO!&eQPO'#CjO!'bQPO'#CeO$zQPO,59WO$zQPO,59VO$zQPO,59UO$zQPO,59TO$zQPO,59SO$zQPO,59RO$zQPO,59QO$zQPO,58zO!(kQPO1G.pO!)RQPO1G.oO!)iQPO1G.nO!*PQPO'#CjO!*zQPO'#CiO!+rQPO'#ChO!,gQPO'#CgO!-XQPO'#CeO!-sQPO'#C`",
|
||||
stateData: "!.V~O!|OSPOS~OUPOhQOiQOjQOkQOlQOmQOoROpROqROrROtSOzTO!PUO!TVO![WO!aXO!d]O!e]O~OofXpfXqfXrfX!TfX!efX!ffX!gfX!hfX!ifX!kfX!lfX!mfX!nfX!ofX!pfX!qfX!}fX~OVmOUfXWTXhfXifXjfXkfXlfXmfXtfXxfXzfX!PfX![fX!afX!dfX!zfX~P!gOUYOhQOiQOjQOkQOlQOmQOoROpROqROrROtSOzTO!PUO!TVO![WO!aXO!d]O!e]O~OUpOuqOvrO~OutO|uO~P$zOUpO!R!QP~OUzO!X|O~P]Oh!QO~OV!TO!T!ROUdXhdXidXjdXkdXldXmdXodXpdXqdXrdXtdXxdXzdX!PdX![dX!adX!ddX!edX!fdX!gdX!hdX!idX!kdX!ldX!mdX!ndX!odX!pdX!qdX!zdX!}dX{dX|dX!XdXvdX!WdX~O!f!VO!g!VOUbXhbXibXjbXkbXlbXmbXobXpbXqbXrbXtbXxbXzbX!PbX!TbX![bX!abX!dbX!ebX!hbX!ibX!kbX!lbX!mbX!nbX!obX!pbX!qbX!zbX!}bXVbX{bX|bX!XbXvbX!WbX~OUaXhaXiaXjaXkaXlaXmaXoaXpaXqaXraXtaXxaXzaX!PaX!TaX![aX!aaX!daX!iaX!kaX!laX!maX!naX!oaX!paX!qaX!zaX!}aX{aX|aX!XaXvaX!WaX~O!e!WO!h!WO~P,_Oo!YOp!YOq!YOr!YO!k!YO!l!YOU`Xh`Xi`Xj`Xk`Xl`Xm`Xt`Xx`Xz`X!P`X!T`X![`X!a`X!d`X!e`X!m`X!n`X!o`X!p`X!q`X!z`X!}`X~O!i!XO~P.uOU^Xh^Xi^Xj^Xk^Xl^Xm^Xo^Xp^Xq^Xr^Xt^Xx^Xz^X!P^X!T^X![^X!a^X!d^X!e^X!n^X!o^X!p^X!q^X!z^X!}^X~O!m![O~P0yOU]Xh]Xi]Xj]Xk]Xl]Xm]Xo]Xp]Xq]Xr]Xt]Xx]Xz]X!P]X!T]X![]X!a]X!d]X!e]X!o]X!p]X!q]X!z]X!}]X~O!n!]O~P2tOU[Xh[Xi[Xj[Xk[Xl[Xm[Xo[Xp[Xq[Xr[Xt[Xx[Xz[X!P[X!T[X![[X!a[X!d[X!e[X!p[X!q[X!z[X!}[X~O!o!^O~P4lOUZXhZXiZXjZXkZXlZXmZXoZXpZXqZXrZXtZXxZXzZX!PZX!TZX![ZX!aZX!dZX!eZX!zZX!}ZX~O!p!_O!q!`O~P6aO!}!aOUXXhXXiXXjXXkXXlXXmXXoXXpXXqXXrXXtXXxXXzXX!PXX!TXX![XX!aXX!dXX!eXX!zXX~OW!cO~Ox!dOURXhRXiRXjRXkRXlRXmRXoRXpRXqRXrRXtRXzRX!PRX!TRX![RX!aRX!dRX!eRX!zRX~OU!fO~OVmOWTa~OVmOWTX~Ov!kOx!iO~O{!mO|!oO~Ox!pO~OUpO!R!QX~O!R!rO~O!W!sOVfX!XfX~P!gO{!tO!X!UX~O!Y!vO~O!X!wO~O!X!xO~Ot!yO~O!X!{O~P$zOU!}O~Ov#aO~OUpOv#cO~Ov#cOx#dO~O{#gO|#fO~O|#iO~P$zO{#jO|#iO~OU#nO!X!Ua~O{#pO!X!Ua~O!Y#sO~Ov#vO!_#tO~P$zO{#xO!X!cX~O!X#zO~O!f!VO!g!VOUbihbiibijbikbilbimbiobipbiqbirbitbixbizbi!Pbi!Tbi![bi!abi!dbi!ebi!hbi!ibi!kbi!lbi!mbi!nbi!obi!pbi!qbi!zbi!}bi{bi|bi!Xbivbi!Wbi~O!h!WOoaipaiqairaitaixai!iai!kai!lai!mai!nai!oai!pai!qai!}ai~OUaihaiiaijaikailaimaizai!Pai!Tai![ai!aai!dai!eai!zai~PAwOt`ix`i!m`i!n`i!o`i!p`i!q`i!}`i~O!i!XOU`ih`ii`ij`ik`il`im`io`ip`iq`ir`iz`i!P`i!T`i![`i!a`i!d`i!e`i!z`i~PDOOt^ix^i!n^i!o^i!p^i!q^i!}^i~O!m![OU^ih^ii^ij^ik^il^im^io^ip^iq^ir^iz^i!P^i!T^i![^i!a^i!d^i!e^i!z^i~PE|Ot]ix]i!o]i!p]i!q]i!}]i~O!n!]OU]ih]ii]ij]ik]il]im]io]ip]iq]ir]iz]i!P]i!T]i![]i!a]i!d]i!e]i!z]i~PGwOt[ix[i!p[i!q[i!}[i~O!o!^OU[ih[ii[ij[ik[il[im[io[ip[iq[ir[iz[i!P[i!T[i![[i!a[i!d[i!e[i!z[i~PIoOUpOv#{O~O|#}O~O|$OO~P$zO!W!sO~OU#nO!X!Ui~O!W$SO~Ov$VOx$TO~O!X!ca~P$zO{$WO!X!ca~OUpO~OU#nO~Ov$ZO!_#tO~P$zOv$ZOx$[O~O!X!ci~P$zOv$^O!_#tO~P$zO!e!WO{ai|ai!Xaivai!Wai~PAwO!e$`O!h$`OVaX!faX!gaX~P,_O!i$aOV`X!f`X!g`X!h`X{`X|`X!X`Xv`X!W`X~P.uO!n$cOV]X!f]X!g]X!h]X!i]X!k]X!l]X!m]X{]X|]X!X]Xv]X!W]X~P2tO!o$dOV[X!f[X!g[X!h[X!i[X!k[X!l[X!m[X!n[X{[X|[X!X[Xv[X!W[X~P4lO!p$eO!q$nOVZX!fZX!gZX!hZX!iZX!kZX!lZX!mZX!nZX!oZX{ZX|ZX!XZXvZX!WZX~P6aO!i$lO{`i|`i!X`iv`i!W`i~PDOOo!YOp!YOq!YOr!YO!i$lO!k!YO!l!YO{`X|`X!m`X!n`X!o`X!p`X!q`X!}`X!X`Xt`Xv`Xx`X!W`X~O!m$mOV^X!f^X!g^X!h^X!i^X!k^X!l^X{^X|^X!X^Xv^X!W^X~P0yOVXX!fXX!gXX!hXX!iXX!kXX!lXX!mXX!nXX!oXX!pXX!qXX{XX|XX!XXXvXX!WXX~P8RO!m$uO{^i|^i!X^iv^i!W^i~PE|O!n$vO{]i|]i!X]iv]i!W]i~PGwO!o$wO{[i|[i!X[iv[i!W[i~PIoO!m$uO{^X|^X!n^X!o^X!p^X!q^X!}^X!X^Xt^Xv^Xx^X!W^X~O!n$vO{]X|]X!o]X!p]X!q]X!}]X!X]Xt]Xv]Xx]X!W]X~O!o$wO{[X|[X!p[X!q[X!}[X!X[Xt[Xv[Xx[X!W[X~O!p$xO!q$yO{ZX|ZX!}ZX!XZXtZXvZXxZX!WZX~O!}!aO{XX|XX!XXXtXXvXXxXX!WXX~OW${O~O!P!R![!a!qklji!_Uk~",
|
||||
goto: "6W!zPPP!{#P#aPPP#m$x%i&`'V(V)Y*`+a,m-z/V0d1m2vPPPPPP2vPPPP2vPPP2vP2vPPP2v2vP4PP2vP4S4VPPP2vP4_4gP2vP4m4pPPPPPP4sPPPPPPP4|5V5]5d5j5t5z6QTkOlSjOlQsSSwUxV#b!i#d#|SiOl]%USUx!i#d#|SjOlQoRQvTQ!OVQ!PWQ!hqQ!ltQ!z!RS#Y!c${Y#h!m#j#x$P$WQ#l!rQ#m!sQ#r!vW#t!y$T$[$_Q$R#sR$X$SUhOl!cW$sR!r!v#su%TTVWqt!R!m!s!y#j#x$P$S$T$W$[$_${!SgORTVWlqt!R!c!m!r!s!v!y#j#s#x$P$S$T$W$[$_${U#W!`$n$yV#X!b$o$zYfOl!`!b!c[$kR!r!v#s$n$oy%STVWqt!R!m!s!y#j#x$P$S$T$W$[$_$y$z${YeOl!`!b!cQ#V!_Q#`$e[$jR!r!v#s$n$oQ%O$xy%RTVWqt!R!m!s!y#j#x$P$S$T$W$[$_$y$z${[dOl!_!`!b!cQ#U!^Q#_$d^$iR!r!v#s$e$n$oQ$}$w{%QTVWqt!R!m!s!y#j#x$P$S$T$W$[$_$x$y$z${^cOl!^!_!`!b!cQ#T!]Q#^$c`$rR!r!v#s$d$e$n$oQ$|$v}%PTVWqt!R!m!s!y#j#x$P$S$T$W$[$_$w$x$y$z${!rbORTVWlqt!R!]!^!_!`!b!c!m!r!s!v!y#j#s#x$P$S$T$W$[$_$c$d$e$n$o$v$w$x$y$z${V#S![$m$ubaOl![!]!^!_!`!b!cQ#R!ZQ#]$bd$hR!r!v#s$c$d$e$m$n$oQ$p$t!R$qTVWqt!R!m!s!y#j#x$P$S$T$W$[$_$u$v$w$x$y$z${!h`OTVWlqt!R!Z![!]!^!_!`!b!c!m!s!y#j#x$P$S$T$W$[$_$t$u$v$w$x$y$z${Q#Q!XQ#[$aQ$f$lg$gR!r!v#s$b$c$d$e$m$n$o#U_ORTVWlqt!R!X!Z![!]!^!_!`!b!c!m!r!s!v!y#j#s#x$P$S$T$W$[$_$a$b$c$d$e$l$m$n$o$t$u$v$w$x$y$z${Q#P!WR#Z$`#Y^ORTVWlqt!R!W!X!Z![!]!^!_!`!b!c!m!r!s!v!y#j#s#x$P$S$T$W$[$_$`$a$b$c$d$e$l$m$n$o$t$u$v$w$x$y$z${Q!U]R#O!V#_[ORTVW]lqt!R!V!W!X!Z![!]!^!_!`!b!c!m!r!s!v!y#j#s#x$P$S$T$W$[$_$`$a$b$c$d$e$l$m$n$o$t$u$v$w$x$y$z${#_ZORTVW]lqt!R!V!W!X!Z![!]!^!_!`!b!c!m!r!s!v!y#j#s#x$P$S$T$W$[$_$`$a$b$c$d$e$l$m$n$o$t$u$v$w$x$y$z${#_YORTVW]lqt!R!V!W!X!Z![!]!^!_!`!b!c!m!r!s!v!y#j#s#x$P$S$T$W$[$_$`$a$b$c$d$e$l$m$n$o$t$u$v$w$x$y$z${RyUR}VQ{VV#o!t#p$QQ#w!yV$Y$T$[$_X#u!y$T$[$_R!S[R!|!RQ!ZaQ$b$hR$t$qQ!bhQ$o$sR$z%TQlOR!elSnPpR!gnQ!jsR#e!jQ!nvS#k!n#yR#y!zQxUR!qxQ!u{R#q!uQ$U#wR$]$U",
|
||||
nodeNames: "⚠ Comment Source Statement FieldDefinition FieldPath Identifier Dot Equal Expression AsExpression DefaultExpression PatchExpression ComposeExpression LogicalOrExpression LogicalAndExpression ComparisonExpression ConcatExpression AdditiveExpression MultiplicativeExpression UnaryExpression PostfixExpression PrimaryExpression Literal String Integer Float True False Regex ComparisonConstraint Lt Lte Gt Gte MapConstraint LBrace Ellipsis RBrace Object Semicolon ArrayConstraint LBracket Comma RBracket Array LetExpression Let FieldDefinitionList In FunctionExpression LParen ParameterList Parameter Colon RParen Arrow MatchExpression Match MatchArm Pattern Underscore ImportExpression Import CallSuffix ArgumentList Bang Minus Star Slash Plus PlusPlus CompareOperator EqualEqual BangEqual AmpAmp PipePipe Amp SlashSlash Default As",
|
||||
maxTerm: 91,
|
||||
states: ":|QYQPOOO#qQPO'#CaOOQO'#Cs'#CsO$zQPO'#CzO&XQQO'#DTO&dQQO'#D[O&nQPO'#D]O&vQPO'#CrO$zQPO'#DhO'QQPO'#DmOOQO'#Cr'#CrOOQO'#Cq'#CqO'VQPO'#CpO$zQPO'#CpOOQO'#Co'#CoO)rQPO'#CnO.kQPO'#CmO0rQPO'#ClOOQO'#Ck'#CkO2mQPO'#CjO4eQPO'#CiO6YQPO'#ChO7wQPO'#CgOOQO'#Cf'#CfO8RQPO'#CeO9iQPO'#C`O9nQPO'#C_OOQO'#EQ'#EQQYQPOOO;RQPO'#ERO;WQPO,58{OOQO,59f,59fO;`QPO'#CaO$zQPO,59kOOQO,59o,59oO;hQPO,59oO$zQPO,59rOOQO,59v,59vO;pQPO,59vO;xQPO'#EUO;}QPO'#D_O<VQPO,59wO<[QPO'#CrO<iQPO'#DcO<qQQO,59{O<vQPO,59{O<{QPO,59^O=QQPO,5:SOOQO,5:X,5:XO=VQPO'#DoOOQO,59],59]O=^QPO,59]OOQO,59[,59[O$zQPO,59ZO$zQPO,59YO$zQPO,59XOOQO'#Dw'#DwO$zQPO,59WO$zQPO,59VO$zQPO,59UO$zQPO,59TO$zQPO,59SO$zQPO,59ROOQO'#EP'#EPO$zQPO,59QO$zQPO,58zOOQO,58y,58yOOQO-E8O-E8OOOQO,5:m,5:mOOQO-E8P-E8PO=cQPO1G/VO=hQQO1G/ZO=sQPO1G/ZOOQO1G/Z1G/ZO={QPO1G/^O>TQPO1G/bO>[QPO1G/bOOQO1G/b1G/bOOQO,5:p,5:pOOQO-E8S-E8SO$zQPO1G/cO$zQPO,5:OO>dQPO,59}O>lQPO,59}O$zQPO1G/gO>tQQO1G/gOOQO1G.x1G.xO>yQPO1G/nO?TQPO'#DpOOQO,5:Z,5:ZO?]QPO,5:ZOOQO1G.w1G.wOOQO1G.u1G.uO?bQPO1G.tOCOQPO1G.sODmQPO1G.rOOQO1G.q1G.qOFhQPO1G.pOH`QPO1G.oOJTQPO1G.nOOQO1G.m1G.mOOQO1G.l1G.lOOQO1G.f1G.fOOQO1G.t1G.tOOQO1G.s1G.sOOQO1G.r1G.rOOQO1G.p1G.pOOQO1G.o1G.oOOQO1G.n1G.nOOQO7+$q7+$qOOQO,5:n,5:nO$zQPO'#DVOOQO7+$u7+$uOKgQPO7+$uOKoQQO7+$uOOQO-E8Q-E8QOOQO7+$x7+$xOKzQPO7+$xOOQO,5:o,5:oOOQO7+$|7+$|OLPQPO7+$|OOQO-E8R-E8ROOQO7+$}7+$}OOQO1G/j1G/jOLWQPO'#DdOOQO,5:q,5:qOL]QPO1G/iOOQO-E8T-E8TOOQO7+%R7+%RO$zQPO7+%ROOQO'#Dk'#DkOLeQPO'#DjOOQO7+%Y7+%YOLjQPO7+%YOLrQPO,5:[OLyQPO,5:[OOQO1G/u1G/uOOQO,59q,59qOOQO<<Ha<<HaOMRQPO<<HaOMWQPO<<HaPM`QPO'#ESOOQO<<Hd<<HdOOQO<<Hh<<HhP$zQPO'#ETPMeQPO'#EVOOQO<<Hm<<HmO$zQPO,5:UOMjQPO<<HtOMtQPO<<HtOOQO<<Ht<<HtOM|QPO1G/vOOQOAN={AN={ONTQPOAN={OOQO1G/p1G/pOOQO,5:r,5:rOOQOAN>`AN>`ONYQPOAN>`OOQO-E8U-E8UOOQOG23gG23gOOQOG23zG23zP>|QPO'#EWO$zQPO,59YO$zQPO,59XO$zQPO,59WO$zQPO,59UO$zQPO,59TO$zQPO,59SONdQPO1G.sONzQPO'#CmO! _QPO'#ClO!!RQPO'#CiO!#RQPO'#ChO!$UQPO'#CgO$zQPO,59XO$zQPO,59VO$zQPO,59RO$zQPO,59QO!%_QPO1G.rO!%uQPO'#ClO!'VQPO'#CjO!(SQPO'#CeO$zQPO,59WO$zQPO,59VO$zQPO,59UO$zQPO,59TO$zQPO,59SO$zQPO,59RO$zQPO,59QO$zQPO,58zO!)]QPO1G.pO!)sQPO1G.oO!*ZQPO1G.nO!*qQPO'#CjO!+lQPO'#CiO!,dQPO'#ChO!-XQPO'#CgO!-yQPO'#CeO!.eQPO'#C`",
|
||||
stateData: "!.w~O!}OSPOS~OUPOhQOiQOjQOkQOlQOmQOoROpROqROrROtSO{TO!QUO!UVO!]WO!bXO!e]O!f]O~OofXpfXqfXrfX!UfX!ffX!gfX!hfX!ifX!jfX!lfX!mfX!nfX!ofX!pfX!qfX!rfX#OfX~OVmOUfXWTXhfXifXjfXkfXlfXmfXtfXxfX{fX!QfX!]fX!bfX!efX!{fX~P!gOUYOhQOiQOjQOkQOlQOmQOoROpROqROrROtSO{TO!QUO!UVO!]WO!bXO!e]O!f]O~OUpOuqOvrO~OutO}uO~P$zOUpO!S!RP~OUzO!Y|O~P]Oh!QO~OV!TO!U!ROUdXhdXidXjdXkdXldXmdXodXpdXqdXrdXtdXxdX{dX!QdX!]dX!bdX!edX!fdX!gdX!hdX!idX!jdX!ldX!mdX!ndX!odX!pdX!qdX!rdX!{dX#OdX|dX}dX!YdXvdX!XdX~O!g!VO!h!VOUbXhbXibXjbXkbXlbXmbXobXpbXqbXrbXtbXxbX{bX!QbX!UbX!]bX!bbX!ebX!fbX!ibX!jbX!lbX!mbX!nbX!obX!pbX!qbX!rbX!{bX#ObXVbX|bX}bX!YbXvbX!XbX~OUaXhaXiaXjaXkaXlaXmaXoaXpaXqaXraXtaXxaX{aX!QaX!UaX!]aX!baX!eaX!jaX!laX!maX!naX!oaX!paX!qaX!raX!{aX#OaX|aX}aX!YaXvaX!XaX~O!f!WO!i!WO~P,_Oo!YOp!YOq!YOr!YO!l!YO!m!YOU`Xh`Xi`Xj`Xk`Xl`Xm`Xt`Xx`X{`X!Q`X!U`X!]`X!b`X!e`X!f`X!n`X!o`X!p`X!q`X!r`X!{`X#O`X~O!j!XO~P.uOU^Xh^Xi^Xj^Xk^Xl^Xm^Xo^Xp^Xq^Xr^Xt^Xx^X{^X!Q^X!U^X!]^X!b^X!e^X!f^X!o^X!p^X!q^X!r^X!{^X#O^X~O!n![O~P0yOU]Xh]Xi]Xj]Xk]Xl]Xm]Xo]Xp]Xq]Xr]Xt]Xx]X{]X!Q]X!U]X!]]X!b]X!e]X!f]X!p]X!q]X!r]X!{]X#O]X~O!o!]O~P2tOU[Xh[Xi[Xj[Xk[Xl[Xm[Xo[Xp[Xq[Xr[Xt[Xx[X{[X!Q[X!U[X!][X!b[X!e[X!f[X!q[X!r[X!{[X#O[X~O!p!^O~P4lOUZXhZXiZXjZXkZXlZXmZXoZXpZXqZXrZXtZXxZX{ZX!QZX!UZX!]ZX!bZX!eZX!fZX!{ZX#OZX~O!q!_O!r!`O~P6aO#O!aOUXXhXXiXXjXXkXXlXXmXXoXXpXXqXXrXXtXXxXX{XX!QXX!UXX!]XX!bXX!eXX!fXX!{XX~OW!cO~Ox!dOURXhRXiRXjRXkRXlRXmRXoRXpRXqRXrRXtRX{RX!QRX!URX!]RX!bRX!eRX!fRX!{RX~OU!fO~OVmOWTa~OVmOWTX~Ov!kOx!iO~O|!mO}!oO~Ox!pO~OUpO!S!RX~O!S!rO~O!X!sOVfX!YfX~P!gO|!tO!Y!VX~O!Z!vO~O!Y!wO~O!Y!xO~Ot!yO~O!Y!{O~P$zOU!}O~Ov#aO~OUpOu#cOv#dO~Ov#dOx#fO~O|#iO}#hO~O}#kO~P$zO|#lO}#kO~OU#pO!Y!Va~O|#rO!Y!Va~O!Z#uO~Ov#xO!`#vO~P$zO|#zO!Y!dX~O!Y#|O~O!g!VO!h!VOUbihbiibijbikbilbimbiobipbiqbirbitbixbi{bi!Qbi!Ubi!]bi!bbi!ebi!fbi!ibi!jbi!lbi!mbi!nbi!obi!pbi!qbi!rbi!{bi#Obi|bi}bi!Ybivbi!Xbi~O!i!WOoaipaiqairaitaixai!jai!lai!mai!nai!oai!pai!qai!rai#Oai~OUaihaiiaijaikailaimai{ai!Qai!Uai!]ai!bai!eai!fai!{ai~PAzOt`ix`i!n`i!o`i!p`i!q`i!r`i#O`i~O!j!XOU`ih`ii`ij`ik`il`im`io`ip`iq`ir`i{`i!Q`i!U`i!]`i!b`i!e`i!f`i!{`i~PDROt^ix^i!o^i!p^i!q^i!r^i#O^i~O!n![OU^ih^ii^ij^ik^il^im^io^ip^iq^ir^i{^i!Q^i!U^i!]^i!b^i!e^i!f^i!{^i~PFPOt]ix]i!p]i!q]i!r]i#O]i~O!o!]OU]ih]ii]ij]ik]il]im]io]ip]iq]ir]i{]i!Q]i!U]i!]]i!b]i!e]i!f]i!{]i~PGzOt[ix[i!q[i!r[i#O[i~O!p!^OU[ih[ii[ij[ik[il[im[io[ip[iq[ir[i{[i!Q[i!U[i!][i!b[i!e[i!f[i!{[i~PIrOv$OOx$PO~OUpOu#cOv$OO~O}$SO~O}$TO~P$zO!X!sO~OU#pO!Y!Vi~O!X$XO~Ov$[Ox$YO~O!Y!da~P$zO|$]O!Y!da~Ov$^O~Ov$^Ox$_O~OUpO~OU#pO~Ov$bO!`#vO~P$zOv$bOx$cO~O!Y!di~P$zOv$eO~Ov$fO!`#vO~P$zO!f!WO|ai}ai!Yaivai!Xai~PAzO!f$hO!i$hOVaX!gaX!haX~P,_O!j$iOV`X!g`X!h`X!i`X|`X}`X!Y`Xv`X!X`X~P.uO!o$kOV]X!g]X!h]X!i]X!j]X!l]X!m]X!n]X|]X}]X!Y]Xv]X!X]X~P2tO!p$lOV[X!g[X!h[X!i[X!j[X!l[X!m[X!n[X!o[X|[X}[X!Y[Xv[X!X[X~P4lO!q$mO!r$vOVZX!gZX!hZX!iZX!jZX!lZX!mZX!nZX!oZX!pZX|ZX}ZX!YZXvZX!XZX~P6aO!j$tO|`i}`i!Y`iv`i!X`i~PDROo!YOp!YOq!YOr!YO!j$tO!l!YO!m!YO|`X}`X!n`X!o`X!p`X!q`X!r`X#O`X!Y`Xt`Xv`Xx`X!X`X~O!n$uOV^X!g^X!h^X!i^X!j^X!l^X!m^X|^X}^X!Y^Xv^X!X^X~P0yOVXX!gXX!hXX!iXX!jXX!lXX!mXX!nXX!oXX!pXX!qXX!rXX|XX}XX!YXXvXX!XXX~P8RO!n$}O|^i}^i!Y^iv^i!X^i~PFPO!o%OO|]i}]i!Y]iv]i!X]i~PGzO!p%PO|[i}[i!Y[iv[i!X[i~PIrO!n$}O|^X}^X!o^X!p^X!q^X!r^X#O^X!Y^Xt^Xv^Xx^X!X^X~O!o%OO|]X}]X!p]X!q]X!r]X#O]X!Y]Xt]Xv]Xx]X!X]X~O!p%PO|[X}[X!q[X!r[X#O[X!Y[Xt[Xv[Xx[X!X[X~O!q%QO!r%RO|ZX}ZX#OZX!YZXtZXvZXxZX!XZX~O#O!aO|XX}XX!YXXtXXvXXxXX!XXX~OW%TO~O!Q!S!]!b!rklji!`Uk~",
|
||||
goto: "6p!{PPP!|#Q#bPPP#n$|%n&f'^(_)c*j+l,y.X/e0s1}3XPPPPPP3XPPPP3XPPP3XP4c3XPPP3X3XP4iP3XP4l4oPPP3XP4w5PP3XP5V5YPPPPPP5]PPPPPPP5f5o5u5|6S6^6d6jTkOlSjOlQsSSwUxV#b!i#f$RSiOl]%^SUx!i#f$RSjOlQoRQvTQ!OVQ!PWQ!hqQ!ltQ!z!RS#Y!c%TY#j!m#l#z$U$]Q#n!rQ#o!sQ#t!vW#v!y$Y$c$gQ#}#cQ$W#uR$`$XUhOl!cW${R!r!v#uw%]TVWqt!R!m!s!y#c#l#z$U$X$Y$]$c$g%T!UgORTVWlqt!R!c!m!r!s!v!y#c#l#u#z$U$X$Y$]$c$g%TU#W!`$v%RV#X!b$w%SYfOl!`!b!c[$sR!r!v#u$v$w{%[TVWqt!R!m!s!y#c#l#z$U$X$Y$]$c$g%R%S%TYeOl!`!b!cQ#V!_Q#`$m[$rR!r!v#u$v$wQ%W%Q{%ZTVWqt!R!m!s!y#c#l#z$U$X$Y$]$c$g%R%S%T[dOl!_!`!b!cQ#U!^Q#_$l^$qR!r!v#u$m$v$wQ%V%P}%YTVWqt!R!m!s!y#c#l#z$U$X$Y$]$c$g%Q%R%S%T^cOl!^!_!`!b!cQ#T!]Q#^$k`$zR!r!v#u$l$m$v$wQ%U%O!P%XTVWqt!R!m!s!y#c#l#z$U$X$Y$]$c$g%P%Q%R%S%T!tbORTVWlqt!R!]!^!_!`!b!c!m!r!s!v!y#c#l#u#z$U$X$Y$]$c$g$k$l$m$v$w%O%P%Q%R%S%TV#S![$u$}baOl![!]!^!_!`!b!cQ#R!ZQ#]$jd$pR!r!v#u$k$l$m$u$v$wQ$x$|!T$yTVWqt!R!m!s!y#c#l#z$U$X$Y$]$c$g$}%O%P%Q%R%S%T!j`OTVWlqt!R!Z![!]!^!_!`!b!c!m!s!y#c#l#z$U$X$Y$]$c$g$|$}%O%P%Q%R%S%TQ#Q!XQ#[$iQ$n$tg$oR!r!v#u$j$k$l$m$u$v$w#W_ORTVWlqt!R!X!Z![!]!^!_!`!b!c!m!r!s!v!y#c#l#u#z$U$X$Y$]$c$g$i$j$k$l$m$t$u$v$w$|$}%O%P%Q%R%S%TQ#P!WR#Z$h#[^ORTVWlqt!R!W!X!Z![!]!^!_!`!b!c!m!r!s!v!y#c#l#u#z$U$X$Y$]$c$g$h$i$j$k$l$m$t$u$v$w$|$}%O%P%Q%R%S%TQ!U]R#O!V#a[ORTVW]lqt!R!V!W!X!Z![!]!^!_!`!b!c!m!r!s!v!y#c#l#u#z$U$X$Y$]$c$g$h$i$j$k$l$m$t$u$v$w$|$}%O%P%Q%R%S%T#aZORTVW]lqt!R!V!W!X!Z![!]!^!_!`!b!c!m!r!s!v!y#c#l#u#z$U$X$Y$]$c$g$h$i$j$k$l$m$t$u$v$w$|$}%O%P%Q%R%S%T#aYORTVW]lqt!R!V!W!X!Z![!]!^!_!`!b!c!m!r!s!v!y#c#l#u#z$U$X$Y$]$c$g$h$i$j$k$l$m$t$u$v$w$|$}%O%P%Q%R%S%TQ#e!iR$Q#fRyUR}VQ{VV#q!t#r$VQ#y!yV$a$Y$c$gX#w!y$Y$c$gR!S[R!|!RQ!ZaQ$j$pR$|$yQ!bhQ$w${R%S%]QlOR!elSnPpR!gnQ!jsR#g!jQ!nvS#m!n#{R#{!zQxUR!qxQ!u{R#s!uQ$Z#yR$d$Z",
|
||||
nodeNames: "⚠ Comment Source Statement FieldDefinition FieldPath Identifier Dot Equal Expression AsExpression DefaultExpression PatchExpression ComposeExpression LogicalOrExpression LogicalAndExpression ComparisonExpression ConcatExpression AdditiveExpression MultiplicativeExpression UnaryExpression PostfixExpression PrimaryExpression Literal String Integer Float True False Regex ComparisonConstraint Lt Lte Gt Gte MapConstraint LBrace Ellipsis RBrace Object Semicolon ObjectRest ArrayConstraint LBracket Comma RBracket Array LetExpression Let FieldDefinitionList In FunctionExpression LParen ParameterList Parameter Colon RParen Arrow MatchExpression Match MatchArm Pattern Underscore ImportExpression Import CallSuffix ArgumentList Bang Minus Star Slash Plus PlusPlus CompareOperator EqualEqual BangEqual AmpAmp PipePipe Amp SlashSlash Default As",
|
||||
maxTerm: 92,
|
||||
skippedNodes: [0,1],
|
||||
repeatNodeCount: 7,
|
||||
tokenData: "=c~R!PX^$Upq$Uqr$yrs%Wst&zvw'cxy'pyz'uz{'z{|(P|}(^}!O(c!O!P(h!P!Q({!Q![+c![!]+|!]!^,R!^!_,W!_!`,e!`!a,z!c!}-X!}#O-j#P#Q-o#R#S-t#T#W-X#W#X.X#X#Y-X#Y#Z1i#Z#]-X#]#^3y#^#`-X#`#a7]#a#b8p#b#h-X#h#i;Q#i#o-X#o#p<|#p#q=R#q#r=^#y#z$U$f$g$U#BY#BZ$U$IS$I_$U$I|$JO$U$JT$JU$U$KV$KW$U&FU&FV$U~$ZY!|~X^$Upq$U#y#z$U$f$g$U#BY#BZ$U$IS$I_$U$I|$JO$U$JT$JU$U$KV$KW$U&FU&FV$U~%OP!d~!_!`%R~%WO!l~~%ZWOY%WZr%Wrs%ss#O%W#O#P%x#P;'S%W;'S;=`&t<%lO%W~%xOh~~%{RO;'S%W;'S;=`&U;=`O%W~&XXOY%WZr%Wrs%ss#O%W#O#P%x#P;'S%W;'S;=`&t;=`<%l%W<%lO%W~&wP;=`<%l%W~'PSP~OY&zZ;'S&z;'S;=`']<%lO&z~'`P;=`<%l&z~'hP!o~vw'k~'pO!m~~'uO!T~~'zO!X~~(PO!f~~(UP!h~{|(X~(^O!i~~(cO{~~(hO!e~R(mPVP!O!P(pQ(sP!O!P(vQ({OuQ~)QW!g~OY)jZ!P)j!P!Q+^!Q#O)j#O#P*[#P;'S)j;'S;=`+W<%lO)j~)mWOY)jZ!P)j!P!Q*V!Q#O)j#O#P*[#P;'S)j;'S;=`+W<%lO)j~*[Om~~*_RO;'S)j;'S;=`*h;=`O)j~*kXOY)jZ!P)j!P!Q*V!Q#O)j#O#P*[#P;'S)j;'S;=`+W;=`<%l)j<%lO)j~+ZP;=`<%l)j~+cO!p~~+hQi~!O!P+n!Q![+c~+qP!Q![+t~+yPj~!Q![+t~,RO!W~~,WOx~~,]Po~!_!`,`~,eOp~~,jQWP!_!`,p!`!a,u~,uO!k~Q,zO!YQ~-PPq~!_!`-S~-XOr~~-^SU~!Q![-X!c!}-X#R#S-X#T#o-X~-oOz~~-tO|~~-{S!_~U~!Q![-X!c!}-X#R#S-X#T#o-X~.^UU~!Q![-X!c!}-X#R#S-X#T#X-X#X#Y.p#Y#o-X~.uUU~!Q![-X!c!}-X#R#S-X#T#Y-X#Y#Z/X#Z#o-X~/^TU~!Q![-X!c!}-X#R#S-X#T#U/m#U#o-X~/rUU~!Q![-X!c!}-X#R#S-X#T#i-X#i#j0U#j#o-X~0ZUU~!Q![-X!c!}-X#R#S-X#T#`-X#`#a0m#a#o-X~0rUU~!Q![-X!c!}-X#R#S-X#T#h-X#h#i1U#i#o-X~1]S!q~U~!Q![-X!c!}-X#R#S-X#T#o-X~1nTU~!Q![-X!c!}-X#R#S-X#T#U1}#U#o-X~2SUU~!Q![-X!c!}-X#R#S-X#T#`-X#`#a2f#a#o-X~2kUU~!Q![-X!c!}-X#R#S-X#T#g-X#g#h2}#h#o-X~3SUU~!Q![-X!c!}-X#R#S-X#T#X-X#X#Y3f#Y#o-X~3mSl~U~!Q![-X!c!}-X#R#S-X#T#o-X~4OVU~!Q![-X!c!}-X#R#S-X#T#a-X#a#b4e#b#c6x#c#o-X~4jUU~!Q![-X!c!}-X#R#S-X#T#d-X#d#e4|#e#o-X~5RUU~!Q![-X!c!}-X#R#S-X#T#c-X#c#d5e#d#o-X~5jUU~!Q![-X!c!}-X#R#S-X#T#f-X#f#g5|#g#o-X~6RUU~!Q![-X!c!}-X#R#S-X#T#h-X#h#i6e#i#o-X~6lS!a~U~!Q![-X!c!}-X#R#S-X#T#o-X~7PS!R~U~!Q![-X!c!}-X#R#S-X#T#o-X~7bUU~!Q![-X!c!}-X#R#S-X#T#X-X#X#Y7t#Y#o-X~7yUU~!Q![-X!c!}-X#R#S-X#T#h-X#h#i8]#i#o-X~8dS!P~U~!Q![-X!c!}-X#R#S-X#T#o-X~8uTU~!Q![-X!c!}-X#R#S-X#T#U9U#U#o-X~9ZUU~!Q![-X!c!}-X#R#S-X#T#h-X#h#i9m#i#o-X~9rUU~!Q![-X!c!}-X#R#S-X#T#V-X#V#W:U#W#o-X~:ZUU~!Q![-X!c!}-X#R#S-X#T#[-X#[#]:m#]#o-X~:tS![~U~!Q![-X!c!}-X#R#S-X#T#o-X~;VUU~!Q![-X!c!}-X#R#S-X#T#f-X#f#g;i#g#o-X~;nUU~!Q![-X!c!}-X#R#S-X#T#i-X#i#j<Q#j#o-X~<VUU~!Q![-X!c!}-X#R#S-X#T#X-X#X#Y<i#Y#o-X~<pSk~U~!Q![-X!c!}-X#R#S-X#T#o-X~=ROt~~=UP#p#q=X~=^O!n~~=cOv~",
|
||||
tokenData: "=c~R!PX^$Upq$Uqr$yrs%Wst&zvw'cxy'pyz'uz{'z{|(P|}(^}!O(c!O!P(h!P!Q({!Q![+c![!]+|!]!^,R!^!_,W!_!`,e!`!a,z!c!}-X!}#O-j#P#Q-o#R#S-t#T#W-X#W#X.X#X#Y-X#Y#Z1i#Z#]-X#]#^3y#^#`-X#`#a7]#a#b8p#b#h-X#h#i;Q#i#o-X#o#p<|#p#q=R#q#r=^#y#z$U$f$g$U#BY#BZ$U$IS$I_$U$I|$JO$U$JT$JU$U$KV$KW$U&FU&FV$U~$ZY!}~X^$Upq$U#y#z$U$f$g$U#BY#BZ$U$IS$I_$U$I|$JO$U$JT$JU$U$KV$KW$U&FU&FV$U~%OP!e~!_!`%R~%WO!m~~%ZWOY%WZr%Wrs%ss#O%W#O#P%x#P;'S%W;'S;=`&t<%lO%W~%xOh~~%{RO;'S%W;'S;=`&U;=`O%W~&XXOY%WZr%Wrs%ss#O%W#O#P%x#P;'S%W;'S;=`&t;=`<%l%W<%lO%W~&wP;=`<%l%W~'PSP~OY&zZ;'S&z;'S;=`']<%lO&z~'`P;=`<%l&z~'hP!p~vw'k~'pO!n~~'uO!U~~'zO!Y~~(PO!g~~(UP!i~{|(X~(^O!j~~(cO|~~(hO!f~R(mPVP!O!P(pQ(sP!O!P(vQ({OuQ~)QW!h~OY)jZ!P)j!P!Q+^!Q#O)j#O#P*[#P;'S)j;'S;=`+W<%lO)j~)mWOY)jZ!P)j!P!Q*V!Q#O)j#O#P*[#P;'S)j;'S;=`+W<%lO)j~*[Om~~*_RO;'S)j;'S;=`*h;=`O)j~*kXOY)jZ!P)j!P!Q*V!Q#O)j#O#P*[#P;'S)j;'S;=`+W;=`<%l)j<%lO)j~+ZP;=`<%l)j~+cO!q~~+hQi~!O!P+n!Q![+c~+qP!Q![+t~+yPj~!Q![+t~,RO!X~~,WOx~~,]Po~!_!`,`~,eOp~~,jQWP!_!`,p!`!a,u~,uO!l~Q,zO!ZQ~-PPq~!_!`-S~-XOr~~-^SU~!Q![-X!c!}-X#R#S-X#T#o-X~-oO{~~-tO}~~-{S!`~U~!Q![-X!c!}-X#R#S-X#T#o-X~.^UU~!Q![-X!c!}-X#R#S-X#T#X-X#X#Y.p#Y#o-X~.uUU~!Q![-X!c!}-X#R#S-X#T#Y-X#Y#Z/X#Z#o-X~/^TU~!Q![-X!c!}-X#R#S-X#T#U/m#U#o-X~/rUU~!Q![-X!c!}-X#R#S-X#T#i-X#i#j0U#j#o-X~0ZUU~!Q![-X!c!}-X#R#S-X#T#`-X#`#a0m#a#o-X~0rUU~!Q![-X!c!}-X#R#S-X#T#h-X#h#i1U#i#o-X~1]S!r~U~!Q![-X!c!}-X#R#S-X#T#o-X~1nTU~!Q![-X!c!}-X#R#S-X#T#U1}#U#o-X~2SUU~!Q![-X!c!}-X#R#S-X#T#`-X#`#a2f#a#o-X~2kUU~!Q![-X!c!}-X#R#S-X#T#g-X#g#h2}#h#o-X~3SUU~!Q![-X!c!}-X#R#S-X#T#X-X#X#Y3f#Y#o-X~3mSl~U~!Q![-X!c!}-X#R#S-X#T#o-X~4OVU~!Q![-X!c!}-X#R#S-X#T#a-X#a#b4e#b#c6x#c#o-X~4jUU~!Q![-X!c!}-X#R#S-X#T#d-X#d#e4|#e#o-X~5RUU~!Q![-X!c!}-X#R#S-X#T#c-X#c#d5e#d#o-X~5jUU~!Q![-X!c!}-X#R#S-X#T#f-X#f#g5|#g#o-X~6RUU~!Q![-X!c!}-X#R#S-X#T#h-X#h#i6e#i#o-X~6lS!b~U~!Q![-X!c!}-X#R#S-X#T#o-X~7PS!S~U~!Q![-X!c!}-X#R#S-X#T#o-X~7bUU~!Q![-X!c!}-X#R#S-X#T#X-X#X#Y7t#Y#o-X~7yUU~!Q![-X!c!}-X#R#S-X#T#h-X#h#i8]#i#o-X~8dS!Q~U~!Q![-X!c!}-X#R#S-X#T#o-X~8uTU~!Q![-X!c!}-X#R#S-X#T#U9U#U#o-X~9ZUU~!Q![-X!c!}-X#R#S-X#T#h-X#h#i9m#i#o-X~9rUU~!Q![-X!c!}-X#R#S-X#T#V-X#V#W:U#W#o-X~:ZUU~!Q![-X!c!}-X#R#S-X#T#[-X#[#]:m#]#o-X~:tS!]~U~!Q![-X!c!}-X#R#S-X#T#o-X~;VUU~!Q![-X!c!}-X#R#S-X#T#f-X#f#g;i#g#o-X~;nUU~!Q![-X!c!}-X#R#S-X#T#i-X#i#j<Q#j#o-X~<VUU~!Q![-X!c!}-X#R#S-X#T#X-X#X#Y<i#Y#o-X~<pSk~U~!Q![-X!c!}-X#R#S-X#T#o-X~=ROt~~=UP#p#q=X~=^O!o~~=cOv~",
|
||||
tokenizers: [0, 1],
|
||||
topRules: {"Source":[0,2]},
|
||||
specialized: [{term: 6, get: (value) => spec_Identifier[value] || -1}],
|
||||
tokenPrec: 2708
|
||||
tokenPrec: 2740
|
||||
})
|
||||
|
||||
@@ -40,43 +40,44 @@ export const
|
||||
RBrace = 38,
|
||||
Object = 39,
|
||||
Semicolon = 40,
|
||||
ArrayConstraint = 41,
|
||||
LBracket = 42,
|
||||
Comma = 43,
|
||||
RBracket = 44,
|
||||
Array = 45,
|
||||
LetExpression = 46,
|
||||
Let = 47,
|
||||
FieldDefinitionList = 48,
|
||||
In = 49,
|
||||
FunctionExpression = 50,
|
||||
LParen = 51,
|
||||
ParameterList = 52,
|
||||
Parameter = 53,
|
||||
Colon = 54,
|
||||
RParen = 55,
|
||||
Arrow = 56,
|
||||
MatchExpression = 57,
|
||||
Match = 58,
|
||||
MatchArm = 59,
|
||||
Pattern = 60,
|
||||
Underscore = 61,
|
||||
ImportExpression = 62,
|
||||
Import = 63,
|
||||
CallSuffix = 64,
|
||||
ArgumentList = 65,
|
||||
Bang = 66,
|
||||
Minus = 67,
|
||||
Star = 68,
|
||||
Slash = 69,
|
||||
Plus = 70,
|
||||
PlusPlus = 71,
|
||||
CompareOperator = 72,
|
||||
EqualEqual = 73,
|
||||
BangEqual = 74,
|
||||
AmpAmp = 75,
|
||||
PipePipe = 76,
|
||||
Amp = 77,
|
||||
SlashSlash = 78,
|
||||
Default = 79,
|
||||
As = 80
|
||||
ObjectRest = 41,
|
||||
ArrayConstraint = 42,
|
||||
LBracket = 43,
|
||||
Comma = 44,
|
||||
RBracket = 45,
|
||||
Array = 46,
|
||||
LetExpression = 47,
|
||||
Let = 48,
|
||||
FieldDefinitionList = 49,
|
||||
In = 50,
|
||||
FunctionExpression = 51,
|
||||
LParen = 52,
|
||||
ParameterList = 53,
|
||||
Parameter = 54,
|
||||
Colon = 55,
|
||||
RParen = 56,
|
||||
Arrow = 57,
|
||||
MatchExpression = 58,
|
||||
Match = 59,
|
||||
MatchArm = 60,
|
||||
Pattern = 61,
|
||||
Underscore = 62,
|
||||
ImportExpression = 63,
|
||||
Import = 64,
|
||||
CallSuffix = 65,
|
||||
ArgumentList = 66,
|
||||
Bang = 67,
|
||||
Minus = 68,
|
||||
Star = 69,
|
||||
Slash = 70,
|
||||
Plus = 71,
|
||||
PlusPlus = 72,
|
||||
CompareOperator = 73,
|
||||
EqualEqual = 74,
|
||||
BangEqual = 75,
|
||||
AmpAmp = 76,
|
||||
PipePipe = 77,
|
||||
Amp = 78,
|
||||
SlashSlash = 79,
|
||||
Default = 80,
|
||||
As = 81
|
||||
|
||||
@@ -52,6 +52,16 @@ test('parses map constraints and range refinement', () => {
|
||||
assert.deepEqual(parseErrors(source), []);
|
||||
});
|
||||
|
||||
test('parses unknown and object rest constraints', () => {
|
||||
const source = `value = {
|
||||
hoge = Int;
|
||||
fuga = String;
|
||||
...Unknown
|
||||
};`;
|
||||
|
||||
assert.deepEqual(parseErrors(source), []);
|
||||
});
|
||||
|
||||
test('only treats the exact as keyword as an operator', () => {
|
||||
assert.deepEqual(parseErrors('asset = 1; value = asset as Int;'), []);
|
||||
});
|
||||
|
||||
Binary file not shown.
@@ -64,6 +64,10 @@ console.log(JSON.parse(completion));
|
||||
|
||||
`globals`, `loadImport`, and `completeImport` are host-owned. The package does not assume a filesystem or virtual project model. Import callbacks are synchronous; preload or cache remote content before evaluation.
|
||||
|
||||
Plain strings, numbers, booleans, arrays, and objects are concrete host values. Use `{ $decodal: 'String' | 'Int' | 'Float' | 'Bool' }` for primitive schemas, `{ $decodal: 'Array', item: value }` for array schemas, and `{ $decodal: 'Map', value: value }` for associative-array schemas. Descriptors also accept `constraints` and `default`.
|
||||
Plain strings, numbers, booleans, arrays, and objects are concrete values. Use `{ $decodal: 'String' | 'Int' | 'Float' | 'Bool' }` for primitive ranges, `{ $decodal: 'Unknown' }` for the top range, `{ $decodal: 'Array', item: value }` for array ranges, and `{ $decodal: 'Map', value: value }` for associative-array ranges. Primitive, unknown, array, and map descriptors also accept `constraints` and `default`.
|
||||
|
||||
Use `{ $decodal: 'Range', constraints, default }` when constructing a general range directly.
|
||||
|
||||
An object with named fields and an open rest range uses `{ $decodal: 'Object', fields, rest }`. The `fields` property is a record of values and `rest` is the range applied to every other field.
|
||||
|
||||
Methods return JSON strings so callers can handle diagnostics and successful results without depending on Rust data structures.
|
||||
|
||||
+30
-15
@@ -1,21 +1,24 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
|
||||
/** A concrete JavaScript value or Decodal schema descriptor supplied by the host. */
|
||||
export type DecodalHostValue =
|
||||
/** A concrete JavaScript value or Decodal range descriptor. */
|
||||
export type DecodalValue =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| DecodalHostValue[]
|
||||
| { [field: string]: DecodalHostValue }
|
||||
| DecodalValue[]
|
||||
| { [field: string]: DecodalValue }
|
||||
| DecodalPrimitiveDescriptor
|
||||
| DecodalUnknownDescriptor
|
||||
| DecodalArrayDescriptor
|
||||
| DecodalMapDescriptor
|
||||
| DecodalAbstractDescriptor;
|
||||
| DecodalObjectDescriptor
|
||||
| DecodalRangeDescriptor;
|
||||
|
||||
export type DecodalPrimitiveType = 'String' | 'Int' | 'Float' | 'Bool';
|
||||
|
||||
export type DecodalConstraint =
|
||||
| { kind: 'unknown' }
|
||||
| { kind: 'type'; value: DecodalPrimitiveType }
|
||||
| { kind: 'compare'; op: '>' | '>=' | '<' | '<='; value: string | number | boolean }
|
||||
| { kind: 'regex'; value: string }
|
||||
@@ -24,32 +27,44 @@ export type DecodalConstraint =
|
||||
export interface DecodalPrimitiveDescriptor {
|
||||
$decodal: DecodalPrimitiveType;
|
||||
constraints?: DecodalConstraint[];
|
||||
default?: DecodalHostValue;
|
||||
default?: DecodalValue;
|
||||
}
|
||||
|
||||
export interface DecodalUnknownDescriptor {
|
||||
$decodal: 'Unknown';
|
||||
constraints?: DecodalConstraint[];
|
||||
default?: DecodalValue;
|
||||
}
|
||||
|
||||
export interface DecodalArrayDescriptor {
|
||||
$decodal: 'Array';
|
||||
item: DecodalHostValue;
|
||||
item: DecodalValue;
|
||||
constraints?: DecodalConstraint[];
|
||||
default?: DecodalHostValue;
|
||||
default?: DecodalValue;
|
||||
}
|
||||
|
||||
export interface DecodalMapDescriptor {
|
||||
$decodal: 'Map';
|
||||
value: DecodalHostValue;
|
||||
value: DecodalValue;
|
||||
constraints?: DecodalConstraint[];
|
||||
default?: DecodalHostValue;
|
||||
default?: DecodalValue;
|
||||
}
|
||||
|
||||
export interface DecodalAbstractDescriptor {
|
||||
$decodal: 'Abstract';
|
||||
export interface DecodalObjectDescriptor {
|
||||
$decodal: 'Object';
|
||||
fields: Record<string, DecodalValue>;
|
||||
rest: DecodalValue;
|
||||
}
|
||||
|
||||
export interface DecodalRangeDescriptor {
|
||||
$decodal: 'Range';
|
||||
constraints?: DecodalConstraint[];
|
||||
default?: DecodalHostValue;
|
||||
default?: DecodalValue;
|
||||
}
|
||||
|
||||
export type DecodalLoadedImport =
|
||||
| { kind: 'source'; key: string; name?: string; source: string }
|
||||
| { kind: 'value'; key: string; value: DecodalHostValue };
|
||||
| { kind: 'value'; key: string; value: DecodalValue };
|
||||
|
||||
export type DecodalImportCandidate =
|
||||
| string
|
||||
@@ -57,7 +72,7 @@ export type DecodalImportCandidate =
|
||||
|
||||
/** Host-owned environment shared by evaluation and language tooling. */
|
||||
export interface DecodalEnvironment {
|
||||
globals?: Record<string, DecodalHostValue>;
|
||||
globals?: Record<string, DecodalValue>;
|
||||
/** Synchronous import callback. Preload or cache asynchronous resources first. */
|
||||
loadImport?: (currentKey: string | null, specifier: string) => DecodalLoadedImport;
|
||||
/** Synchronous import completion callback. */
|
||||
|
||||
Binary file not shown.
@@ -11,16 +11,18 @@ export {
|
||||
} from './decodal_wasm.js';
|
||||
|
||||
export type {
|
||||
DecodalAbstractDescriptor,
|
||||
DecodalArrayDescriptor,
|
||||
DecodalMapDescriptor,
|
||||
DecodalConstraint,
|
||||
DecodalEnvironment,
|
||||
DecodalHostValue,
|
||||
DecodalImportCandidate,
|
||||
DecodalLoadedImport,
|
||||
DecodalMapDescriptor,
|
||||
DecodalObjectDescriptor,
|
||||
DecodalPrimitiveDescriptor,
|
||||
DecodalPrimitiveType,
|
||||
DecodalRangeDescriptor,
|
||||
DecodalUnknownDescriptor,
|
||||
DecodalValue,
|
||||
InitInput,
|
||||
InitOutput,
|
||||
SyncInitInput,
|
||||
|
||||
@@ -8,22 +8,25 @@ copyFileSync(resolve(packageDir, '../../LICENSE-APACHE'), resolve(packageDir, 'L
|
||||
|
||||
const declarationPath = resolve(packageDir, 'decodal_wasm.d.ts');
|
||||
let declarations = readFileSync(declarationPath, 'utf8');
|
||||
const hostTypes = `
|
||||
/** A concrete JavaScript value or Decodal schema descriptor supplied by the host. */
|
||||
export type DecodalHostValue =
|
||||
const valueTypes = `
|
||||
/** A concrete JavaScript value or Decodal range descriptor. */
|
||||
export type DecodalValue =
|
||||
| string
|
||||
| number
|
||||
| boolean
|
||||
| DecodalHostValue[]
|
||||
| { [field: string]: DecodalHostValue }
|
||||
| DecodalValue[]
|
||||
| { [field: string]: DecodalValue }
|
||||
| DecodalPrimitiveDescriptor
|
||||
| DecodalUnknownDescriptor
|
||||
| DecodalArrayDescriptor
|
||||
| DecodalMapDescriptor
|
||||
| DecodalAbstractDescriptor;
|
||||
| DecodalObjectDescriptor
|
||||
| DecodalRangeDescriptor;
|
||||
|
||||
export type DecodalPrimitiveType = 'String' | 'Int' | 'Float' | 'Bool';
|
||||
|
||||
export type DecodalConstraint =
|
||||
| { kind: 'unknown' }
|
||||
| { kind: 'type'; value: DecodalPrimitiveType }
|
||||
| { kind: 'compare'; op: '>' | '>=' | '<' | '<='; value: string | number | boolean }
|
||||
| { kind: 'regex'; value: string }
|
||||
@@ -32,32 +35,44 @@ export type DecodalConstraint =
|
||||
export interface DecodalPrimitiveDescriptor {
|
||||
$decodal: DecodalPrimitiveType;
|
||||
constraints?: DecodalConstraint[];
|
||||
default?: DecodalHostValue;
|
||||
default?: DecodalValue;
|
||||
}
|
||||
|
||||
export interface DecodalUnknownDescriptor {
|
||||
$decodal: 'Unknown';
|
||||
constraints?: DecodalConstraint[];
|
||||
default?: DecodalValue;
|
||||
}
|
||||
|
||||
export interface DecodalArrayDescriptor {
|
||||
$decodal: 'Array';
|
||||
item: DecodalHostValue;
|
||||
item: DecodalValue;
|
||||
constraints?: DecodalConstraint[];
|
||||
default?: DecodalHostValue;
|
||||
default?: DecodalValue;
|
||||
}
|
||||
|
||||
export interface DecodalMapDescriptor {
|
||||
$decodal: 'Map';
|
||||
value: DecodalHostValue;
|
||||
value: DecodalValue;
|
||||
constraints?: DecodalConstraint[];
|
||||
default?: DecodalHostValue;
|
||||
default?: DecodalValue;
|
||||
}
|
||||
|
||||
export interface DecodalAbstractDescriptor {
|
||||
$decodal: 'Abstract';
|
||||
export interface DecodalObjectDescriptor {
|
||||
$decodal: 'Object';
|
||||
fields: Record<string, DecodalValue>;
|
||||
rest: DecodalValue;
|
||||
}
|
||||
|
||||
export interface DecodalRangeDescriptor {
|
||||
$decodal: 'Range';
|
||||
constraints?: DecodalConstraint[];
|
||||
default?: DecodalHostValue;
|
||||
default?: DecodalValue;
|
||||
}
|
||||
|
||||
export type DecodalLoadedImport =
|
||||
| { kind: 'source'; key: string; name?: string; source: string }
|
||||
| { kind: 'value'; key: string; value: DecodalHostValue };
|
||||
| { kind: 'value'; key: string; value: DecodalValue };
|
||||
|
||||
export type DecodalImportCandidate =
|
||||
| string
|
||||
@@ -65,7 +80,7 @@ export type DecodalImportCandidate =
|
||||
|
||||
/** Host-owned environment shared by evaluation and language tooling. */
|
||||
export interface DecodalEnvironment {
|
||||
globals?: Record<string, DecodalHostValue>;
|
||||
globals?: Record<string, DecodalValue>;
|
||||
/** Synchronous import callback. Preload or cache asynchronous resources first. */
|
||||
loadImport?: (currentKey: string | null, specifier: string) => DecodalLoadedImport;
|
||||
/** Synchronous import completion callback. */
|
||||
@@ -73,7 +88,7 @@ export interface DecodalEnvironment {
|
||||
}
|
||||
`;
|
||||
if (!declarations.includes('export interface DecodalEnvironment')) {
|
||||
declarations = declarations.replace('/* eslint-disable */\n', `/* eslint-disable */\n${hostTypes}`);
|
||||
declarations = declarations.replace('/* eslint-disable */\n', `/* eslint-disable */\n${valueTypes}`);
|
||||
}
|
||||
declarations = declarations.replace(
|
||||
'constructor(options: any);',
|
||||
@@ -175,7 +190,11 @@ console.log(JSON.parse(completion));
|
||||
|
||||
\`globals\`, \`loadImport\`, and \`completeImport\` are host-owned. The package does not assume a filesystem or virtual project model. Import callbacks are synchronous; preload or cache remote content before evaluation.
|
||||
|
||||
Plain strings, numbers, booleans, arrays, and objects are concrete host values. Use \`{ $decodal: 'String' | 'Int' | 'Float' | 'Bool' }\` for primitive schemas, \`{ $decodal: 'Array', item: value }\` for array schemas, and \`{ $decodal: 'Map', value: value }\` for associative-array schemas. Descriptors also accept \`constraints\` and \`default\`.
|
||||
Plain strings, numbers, booleans, arrays, and objects are concrete values. Use \`{ $decodal: 'String' | 'Int' | 'Float' | 'Bool' }\` for primitive ranges, \`{ $decodal: 'Unknown' }\` for the top range, \`{ $decodal: 'Array', item: value }\` for array ranges, and \`{ $decodal: 'Map', value: value }\` for associative-array ranges. Primitive, unknown, array, and map descriptors also accept \`constraints\` and \`default\`.
|
||||
|
||||
Use \`{ $decodal: 'Range', constraints, default }\` when constructing a general range directly.
|
||||
|
||||
An object with named fields and an open rest range uses \`{ $decodal: 'Object', fields, rest }\`. The \`fields\` property is a record of values and \`rest\` is the range applied to every other field.
|
||||
|
||||
Methods return JSON strings so callers can handle diagnostics and successful results without depending on Rust data structures.
|
||||
`,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
const DECODAL_KEYWORDS = new Set(['let', 'in', 'fn', 'match', 'import', 'default']);
|
||||
const DECODAL_TYPES = new Set(['String', 'Int', 'Float', 'Bool']);
|
||||
const DECODAL_TYPES = new Set(['String', 'Int', 'Float', 'Bool', 'Unknown']);
|
||||
const DECODAL_LITERALS = new Set(['true', 'false']);
|
||||
|
||||
const HTML_ESCAPE = {
|
||||
@@ -41,7 +41,7 @@ export function highlightDecodalTokens(source, tokens) {
|
||||
|
||||
function tokenClass(kind, text) {
|
||||
if (['let', 'in', 'fn', 'match', 'import', 'default'].includes(kind)) return 'tok-keyword';
|
||||
if (kind === 'ident' && ['String', 'Int', 'Float', 'Bool'].includes(text)) return 'tok-type';
|
||||
if (kind === 'ident' && ['String', 'Int', 'Float', 'Bool', 'Unknown'].includes(text)) return 'tok-type';
|
||||
if (kind === 'ident') return '';
|
||||
if (['true', 'false'].includes(kind)) return 'tok-literal';
|
||||
if (['int', 'float'].includes(kind)) return 'tok-number';
|
||||
|
||||
@@ -22,6 +22,13 @@ test('injects one JavaScript host environment into evaluation and completion', a
|
||||
active: { $decodal: 'Bool', default: true },
|
||||
},
|
||||
},
|
||||
OpenConfig: {
|
||||
$decodal: 'Object',
|
||||
fields: {
|
||||
enabled: { $decodal: 'Bool', default: true },
|
||||
},
|
||||
rest: { $decodal: 'Unknown' },
|
||||
},
|
||||
},
|
||||
},
|
||||
loadImport(_currentKey, specifier) {
|
||||
@@ -46,11 +53,12 @@ test('injects one JavaScript host environment into evaluation and completion', a
|
||||
const evaluated = JSON.parse(service.evaluate(
|
||||
'main.dcdl',
|
||||
'main.dcdl',
|
||||
'let s = import "./schema.dcdl"; in { server = { port = 8080; } as s.Server; services = { api = { port = 8081; }; } as App.Services; enabled = App.enabled; post = import "./post.md" as { frontmatter = { draft = Bool; }; body = String; }; }',
|
||||
'let s = import "./schema.dcdl"; in { server = { port = 8080; } as s.Server; services = { api = { port = 8081; }; } as App.Services; open = { enabled = false; plugin = { name = "cache"; }; } as App.OpenConfig; enabled = App.enabled; post = import "./post.md" as { frontmatter = { draft = Bool; }; body = String; }; }',
|
||||
));
|
||||
assert.equal(evaluated.ok, true, evaluated.error);
|
||||
assert.match(evaluated.output, /"port": 8080/);
|
||||
assert.match(evaluated.output, /"active": true/);
|
||||
assert.match(evaluated.output, /"plugin": \{/);
|
||||
assert.match(evaluated.output, /"body": "# Hello"/);
|
||||
|
||||
const member = JSON.parse(service.complete('main.dcdl', 'App.en', 6, false));
|
||||
@@ -61,5 +69,9 @@ test('injects one JavaScript host environment into evaluation and completion', a
|
||||
assert.equal(imported.ok, true, imported.error);
|
||||
assert.ok(imported.completion.options.some((item) => item.label === './schema.dcdl'));
|
||||
|
||||
const unknown = JSON.parse(service.complete('main.dcdl', 'Unk', 3, false));
|
||||
assert.equal(unknown.ok, true, unknown.error);
|
||||
assert.ok(unknown.completion.options.some((item) => item.label === 'Unknown'));
|
||||
|
||||
service.free();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user