Add unknown ranges and open object values

This commit is contained in:
2026-08-14 08:00:22 +09:00
parent 8c50dd202d
commit 848c7f169f
53 changed files with 9047 additions and 6572 deletions
+4 -2
View File
@@ -9,7 +9,8 @@ It is designed around a lightweight Rust library:
- no filesystem access in the library core - no filesystem access in the library core
- concrete and abstract values with constraints and defaults - concrete and abstract values with constraints and defaults
- asymmetric range refinement with `narrower as wider` - 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 - deterministic expression evaluation
- optional regex support behind a Cargo feature - optional regex support behind a Cargo feature
- browser playground support through WebAssembly - browser playground support through WebAssembly
@@ -26,6 +27,7 @@ decodal = "0.3.0"
## Derive support ## Derive support
For embedded Rust applications, Decodal can generate a schema and typed decoder from a Rust struct with the `derive` feature. 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 ```toml
[dependencies] [dependencies]
@@ -47,7 +49,7 @@ struct Service {
The derive implements: 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 - `DecodalDecode`, which converts materialized `Data` into the Rust struct
## CLI ## CLI
+13 -7
View File
@@ -5,10 +5,16 @@ use std::{
}; };
use decodal::{ use decodal::{
Data, Diagnostic, DiagnosticKind, Engine, ImportLoader, LoadedImport, LoadedSource, SourceId, Data, Diagnostic, DiagnosticKind, Engine, ImportLoader, LoadedImport, SourceId, Span,
Span, format_diagnostic_with, format_diagnostic_with,
}; };
struct SourceInput {
key: String,
name: String,
source: String,
}
fn main() -> ExitCode { fn main() -> ExitCode {
match run() { match run() {
Ok(()) => ExitCode::SUCCESS, Ok(()) => ExitCode::SUCCESS,
@@ -69,7 +75,7 @@ fn materialize_path(path: &str) -> Result<Data, String> {
.map_err(|error| engine.format_diagnostic(&error)) .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 == "-" { if path == "-" {
use std::io::Read; use std::io::Read;
let mut source = String::new(); let mut source = String::new();
@@ -82,7 +88,7 @@ fn read_root_source(path: &str) -> Result<LoadedSource, Diagnostic> {
format!("failed to read stdin: {error}"), format!("failed to read stdin: {error}"),
) )
})?; })?;
Ok(LoadedSource { Ok(SourceInput {
key: String::from("<stdin>"), key: String::from("<stdin>"),
name: String::from("<stdin>"), name: String::from("<stdin>"),
source, source,
@@ -112,11 +118,11 @@ impl ImportLoader for FsLoader {
} else { } else {
PathBuf::from(path) 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| { let canonical = path.canonicalize().map_err(|error| {
Diagnostic::new( Diagnostic::new(
DiagnosticKind::Import, DiagnosticKind::Import,
@@ -132,7 +138,7 @@ fn load_path(path: &Path) -> Result<LoadedSource, Diagnostic> {
) )
})?; })?;
let key = canonical.to_string_lossy().into_owned(); let key = canonical.to_string_lossy().into_owned();
Ok(LoadedSource { Ok(SourceInput {
name: key.clone(), name: key.clone(),
key, key,
source, source,
+5 -5
View File
@@ -1,14 +1,14 @@
use decodal::{Data, EmptyLoader, Engine, HostValue}; use decodal::{Data, EmptyLoader, Engine, Value};
fn main() -> decodal::Result<()> { fn main() -> decodal::Result<()> {
let mut engine = Engine::new(EmptyLoader); let mut engine = Engine::new(EmptyLoader);
engine.bind_global( engine.bind_global(
"Service", "Service",
HostValue::object([ Value::object([
("name", HostValue::string_type()), ("name", Value::string_type()),
("port", HostValue::int_type().gt(443).default_int(8443)?), ("port", Value::int_type().gt(443).default_int(8443)?),
("enabled", HostValue::bool_type().default_bool(true)?), ("enabled", Value::bool_type().default_bool(true)?),
]), ]),
)?; )?;
@@ -1,6 +1,4 @@
use decodal::{ use decodal::{Data, Diagnostic, DiagnosticKind, Engine, ImportLoader, LoadedImport, Span, Value};
Data, Diagnostic, DiagnosticKind, Engine, HostValue, ImportLoader, LoadedImport, Span,
};
const POST: &str = r#"--- const POST: &str = r#"---
title: Hello 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 source = source.strip_prefix("---\n").ok_or_else(frontmatter_error)?;
let (frontmatter, body) = source.split_once("\n---\n").ok_or_else(frontmatter_error)?; let (frontmatter, body) = source.split_once("\n---\n").ok_or_else(frontmatter_error)?;
let mut fields = Vec::new(); let mut fields = Vec::new();
for line in frontmatter.lines() { for line in frontmatter.lines() {
let (name, value) = line.split_once(':').ok_or_else(frontmatter_error)?; let (name, value) = line.split_once(':').ok_or_else(frontmatter_error)?;
let value = match value.trim() { let value = match value.trim() {
"true" => HostValue::bool(true), "true" => Value::bool(true),
"false" => HostValue::bool(false), "false" => Value::bool(false),
value => HostValue::string(value), value => Value::string(value),
}; };
fields.push((name.trim(), value)); fields.push((name.trim(), value));
} }
Ok(HostValue::object([ Ok(Value::object([
("frontmatter", HostValue::object(fields)), ("frontmatter", Value::object(fields)),
("body", HostValue::string(body)), ("body", Value::string(body)),
])) ]))
} }
+10 -1
View File
@@ -48,7 +48,10 @@ pub struct SpannedExpr {
pub enum Expr { pub enum Expr {
Literal(Literal), Literal(Literal),
Ident(String), Ident(String),
Object(Vec<Field>), Object {
fields: Vec<Field>,
rest: Option<ObjectRest>,
},
Array(Vec<ExprId>), Array(Vec<ExprId>),
ArrayConstraint { ArrayConstraint {
item: ExprId, item: ExprId,
@@ -116,6 +119,12 @@ pub struct Field {
pub span: Span, pub span: Span,
} }
#[derive(Debug, Clone, PartialEq)]
pub struct ObjectRest {
pub value: ExprId,
pub span: Span,
}
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct Param { pub struct Param {
pub name: String, pub name: String,
+4
View File
@@ -19,6 +19,10 @@ pub fn normalize_constraints(
for entry in constraints { for entry in constraints {
match entry.constraint { match entry.constraint {
Constraint::Unknown => rest.push(ConstraintEntry {
constraint: Constraint::Unknown,
span: entry.span,
}),
Constraint::Type(next) => match primitive { Constraint::Type(next) => match primitive {
Some((current, current_span)) if current != next => { Some((current, current_span)) if current != next => {
return Err(Diagnostic::new( return Err(Diagnostic::new(
+483 -81
View File
@@ -5,14 +5,15 @@ use crate::{
ast::{Ast, BinaryOp, CompareOp, Expr, Field, Literal, UnaryOp}, ast::{Ast, BinaryOp, CompareOp, Expr, Field, Literal, UnaryOp},
constraints::normalize_constraints, constraints::normalize_constraints,
diagnostic::{Diagnostic, DiagnosticKind, Result}, diagnostic::{Diagnostic, DiagnosticKind, Result},
embedding::HostValue, module::{EmptyLoader, ImportLoader, LoadedImport, Module},
module::{EmptyLoader, ImportLoader, LoadedImport, LoadedSource, LoadedValue, Module},
parse_source_with_source_id, parse_source_with_source_id,
runtime::{ runtime::{
AbstractValue, Binding, ConcreteValue, Constraint, ConstraintEntry, Data, DataField, Env, AbstractValue, Binding, ConcreteValue, Constraint, ConstraintEntry, Data, DataField, Env,
EnvId, ExprRef, FunctionParam, FunctionValue, LiteralValue, ModuleId, ObjectField, 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> { 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> { pub fn bind_global(&mut self, name: impl Into<String>, value: Value) -> Result<ThunkId> {
let value = self.internalize_host_value(value)?; let value = self.internalize_value(value)?;
let thunk = self.add_value_thunk(value); let thunk = self.add_value_thunk(value);
self.bind(self.prelude_env, name.into(), thunk); self.bind(self.prelude_env, name.into(), thunk);
Ok(thunk) Ok(thunk)
@@ -280,9 +281,9 @@ impl<L: ImportLoader> Engine<L> {
let module = ModuleId(self.modules.len() as u32); let module = ModuleId(self.modules.len() as u32);
let root_env = self.new_env(Some(self.prelude_env)); let root_env = self.new_env(Some(self.prelude_env));
let root_thunk = if source_form == SourceForm::Fields { 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 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"); .expect("module object construction should not fail for parsed fields");
for field in &object.fields { for field in &object.fields {
self.bind(root_env, field.name.clone(), field.value); 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 current_key = self.modules[current.0 as usize].key.clone();
let loaded = self.loader.load(Some(&current_key), specifier)?; let loaded = self.loader.load(Some(&current_key), specifier)?;
match loaded { match loaded {
LoadedImport::Source(LoadedSource { key, name, source }) => { LoadedImport::Source { key, name, source } => {
if let Some(value) = self.find_imported_value(&key) { if let Some(value) = self.find_imported_value(&key) {
return self.force(value); return self.force(value);
} }
let module = self.add_source(key, name, &source)?; let module = self.add_source(key, name, &source)?;
self.eval_module(module) self.eval_module(module)
} }
LoadedImport::Value(LoadedValue { key, value }) => { LoadedImport::Value { key, value } => {
if let Some(module) = self.find_module(&key) { if let Some(module) = self.find_module(&key) {
return self.eval_module(module); return self.eval_module(module);
} }
@@ -333,7 +334,7 @@ impl<L: ImportLoader> Engine<L> {
return self.force(value); return self.force(value);
} }
let origin = ImportedValueOrigin::root(key.clone()); 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); let root = self.add_value_thunk_with_import_origin(value, origin);
self.imported_values.push(ImportedValue { key, root }); self.imported_values.push(ImportedValue { key, root });
self.force(root) self.force(root)
@@ -354,8 +355,8 @@ impl<L: ImportLoader> Engine<L> {
match expr { match expr {
Expr::Literal(literal) => Ok(RuntimeValue::Concrete(literal_to_concrete(literal))), Expr::Literal(literal) => Ok(RuntimeValue::Concrete(literal_to_concrete(literal))),
Expr::Ident(name) => self.eval_ident(&name, env, span), Expr::Ident(name) => self.eval_ident(&name, env, span),
Expr::Object(fields) => self Expr::Object { fields, rest } => self
.build_object(reference.module, &fields, env) .build_object(reference.module, &fields, rest.as_ref(), env)
.map(|object| RuntimeValue::Concrete(ConcreteValue::Object(object))), .map(|object| RuntimeValue::Concrete(ConcreteValue::Object(object))),
Expr::Array(items) => { Expr::Array(items) => {
let thunks = 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> { 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) { if let Some(primitive) = primitive_type(name) {
return Ok(RuntimeValue::Abstract(AbstractValue { return Ok(RuntimeValue::Abstract(AbstractValue {
constraints: vec![ConstraintEntry { constraints: vec![ConstraintEntry {
@@ -738,9 +748,23 @@ impl<L: ImportLoader> Engine<L> {
&mut self, &mut self,
module: ModuleId, module: ModuleId,
fields: &[Field], fields: &[Field],
rest: Option<&crate::ast::ObjectRest>,
env: EnvId, env: EnvId,
) -> Result<ObjectValue> { ) -> 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 { for field in fields {
self.insert_field( self.insert_field(
&mut object, &mut object,
@@ -805,7 +829,10 @@ impl<L: ImportLoader> Engine<L> {
return Ok(()); 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)?; self.insert_field(&mut nested, module, &path[1..], expr, env, span)?;
object.fields.push(ObjectField { object.fields.push(ObjectField {
name: name.clone(), name: name.clone(),
@@ -1032,6 +1059,7 @@ impl<L: ImportLoader> Engine<L> {
) -> Result<()> { ) -> Result<()> {
for wider_entry in &wider.constraints { for wider_entry in &wider.constraints {
let implied = match &wider_entry.constraint { let implied = match &wider_entry.constraint {
Constraint::Unknown => true,
Constraint::Type(wider_type) => narrower.constraints.iter().any(|entry| { Constraint::Type(wider_type) => narrower.constraints.iter().any(|entry| {
matches!(entry.constraint, Constraint::Type(narrower_type) if narrower_type == *wider_type) 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 { let ObjectValue {
if wider fields: narrower_fields,
.fields rest: narrower_rest,
} = narrower;
let ObjectValue {
fields: wider_fields,
rest: wider_rest,
} = wider;
for narrower_field in &narrower_fields {
if wider_fields
.iter() .iter()
.all(|wider_field| wider_field.name != narrower_field.name) .all(|wider_field| wider_field.name != narrower_field.name)
&& wider_rest.is_none()
{ {
let origin = self.thunk_import_origin(narrower_field.value).cloned(); let origin = self.thunk_import_origin(narrower_field.value).cloned();
let diagnostic_span = if origin.is_some() { let diagnostic_span = if origin.is_some() {
@@ -1172,10 +1209,33 @@ impl<L: ImportLoader> Engine<L> {
} }
} }
let mut refined = ObjectValue { fields: Vec::new() }; if let Some(narrower_rest) = &narrower_rest
for wider_field in wider.fields { && wider_rest.is_none()
let Some(narrower_field) = narrower {
.fields 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() .iter()
.find(|narrower_field| narrower_field.name == wider_field.name) .find(|narrower_field| narrower_field.name == wider_field.name)
else { else {
@@ -1227,6 +1287,82 @@ impl<L: ImportLoader> Engine<L> {
span: narrower_field.span, 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))) Ok(RuntimeValue::Concrete(ConcreteValue::Object(refined)))
} }
@@ -1321,7 +1457,46 @@ impl<L: ImportLoader> Engine<L> {
rhs: ObjectValue, rhs: ObjectValue,
span: Span, span: Span,
) -> Result<RuntimeValue> { ) -> 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 if let Some(index) = lhs
.fields .fields
.iter() .iter()
@@ -1381,9 +1556,65 @@ impl<L: ImportLoader> Engine<L> {
lhs.fields[index].span = rhs_field.span; lhs.fields[index].span = rhs_field.span;
} }
} else { } 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.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))) 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> { fn patch_objects(&mut self, mut lhs: ObjectValue, rhs: ObjectValue) -> Result<RuntimeValue> {
let rhs_rest = rhs.rest;
for rhs_field in rhs.fields { for rhs_field in rhs.fields {
if let Some(index) = lhs if let Some(index) = lhs
.fields .fields
@@ -1418,6 +1650,9 @@ impl<L: ImportLoader> Engine<L> {
lhs.fields.push(rhs_field); lhs.fields.push(rhs_field);
} }
} }
if rhs_rest.is_some() {
lhs.rest = rhs_rest;
}
Ok(RuntimeValue::Concrete(ConcreteValue::Object(lhs))) Ok(RuntimeValue::Concrete(ConcreteValue::Object(lhs)))
} }
@@ -1459,6 +1694,7 @@ impl<L: ImportLoader> Engine<L> {
constraint.span constraint.span
}; };
match constraint_value { match constraint_value {
Constraint::Unknown => Ok(value),
Constraint::Type(primitive) => { Constraint::Type(primitive) => {
if value_matches_primitive(&value, *primitive) { if value_matches_primitive(&value, *primitive) {
Ok(value) Ok(value)
@@ -1547,8 +1783,12 @@ impl<L: ImportLoader> Engine<L> {
)); ));
}; };
let value_schema = self.force(*value_constraint)?; let value_schema = self.force(*value_constraint)?;
let mut constrained_fields = Vec::with_capacity(object.fields.len()); let ObjectValue {
for field in object.fields { 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_span = self.thunk_span(field.value);
let field_origin = self.thunk_import_origin(field.value).cloned(); let field_origin = self.thunk_import_origin(field.value).cloned();
let field_value = self.force(field.value)?; let field_value = self.force(field.value)?;
@@ -1592,8 +1832,30 @@ impl<L: ImportLoader> Engine<L> {
span: field.span, 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 { Ok(RuntimeValue::Concrete(ConcreteValue::Object(ObjectValue {
fields: constrained_fields, fields: constrained_fields,
rest: constrained_rest,
}))) })))
} }
Constraint::Compare(op, expected) => compare_value(&value, *op, expected) 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> { fn internalize_value(&mut self, value: Value) -> Result<RuntimeValue> {
self.internalize_host_value_with_origin(value, None) self.internalize_value_with_origin(value, None)
} }
fn internalize_host_value_with_origin( fn internalize_value_with_origin(
&mut self, &mut self,
value: HostValue, value: Value,
origin: Option<&ImportedValueOrigin>, origin: Option<&ImportedValueOrigin>,
) -> Result<RuntimeValue> { ) -> Result<RuntimeValue> {
match value { match value {
HostValue::String(value) => Ok(RuntimeValue::Concrete(ConcreteValue::String(value))), Value::String(value) => Ok(RuntimeValue::Concrete(ConcreteValue::String(value))),
HostValue::Int(value) => Ok(RuntimeValue::Concrete(ConcreteValue::Int(value))), Value::Int(value) => Ok(RuntimeValue::Concrete(ConcreteValue::Int(value))),
HostValue::Float(value) => Ok(RuntimeValue::Concrete(ConcreteValue::Float(value))), Value::Float(value) => Ok(RuntimeValue::Concrete(ConcreteValue::Float(value))),
HostValue::Bool(value) => Ok(RuntimeValue::Concrete(ConcreteValue::Bool(value))), Value::Bool(value) => Ok(RuntimeValue::Concrete(ConcreteValue::Bool(value))),
HostValue::Array(items) => { Value::Array(items) => {
let mut thunks = Vec::new(); let mut thunks = Vec::new();
for (index, item) in items.into_iter().enumerate() { for (index, item) in items.into_iter().enumerate() {
let item_origin = origin.map(|origin| origin.index(index)); let item_origin = origin.map(|origin| origin.index(index));
let value = let value = self.internalize_value_with_origin(item, item_origin.as_ref())?;
self.internalize_host_value_with_origin(item, item_origin.as_ref())?;
thunks thunks
.push(self.add_value_thunk_with_optional_import_origin(value, item_origin)); .push(self.add_value_thunk_with_optional_import_origin(value, item_origin));
} }
Ok(RuntimeValue::Concrete(ConcreteValue::Array(thunks))) Ok(RuntimeValue::Concrete(ConcreteValue::Array(thunks)))
} }
HostValue::ArrayConstraint { Value::ArrayRange {
item, item,
mut constraints, mut constraints,
default, 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 item = self.add_value_thunk_with_optional_import_origin(item, origin.cloned());
let default = if let Some(default) = 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())) Some(self.add_value_thunk_with_optional_import_origin(value, origin.cloned()))
} else { } else {
None None
@@ -1723,16 +1984,16 @@ impl<L: ImportLoader> Engine<L> {
default, default,
})) }))
} }
HostValue::MapConstraint { Value::MapRange {
value, value,
mut constraints, mut constraints,
default, default,
} => { } => {
let value = self.internalize_host_value_with_origin(*value, origin)?; let value = self.internalize_value_with_origin(*value, origin)?;
let value = let value =
self.add_value_thunk_with_optional_import_origin(value, origin.cloned()); self.add_value_thunk_with_optional_import_origin(value, origin.cloned());
let default = if let Some(default) = 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())) Some(self.add_value_thunk_with_optional_import_origin(value, origin.cloned()))
} else { } else {
None None
@@ -1751,8 +2012,11 @@ impl<L: ImportLoader> Engine<L> {
default, default,
})) }))
} }
HostValue::Object(fields) => { Value::Object { fields, rest } => {
let mut object = ObjectValue { fields: Vec::new() }; let mut object = ObjectValue {
fields: Vec::new(),
rest: None,
};
for field in fields { for field in fields {
let field_origin = origin.map(|origin| origin.field(field.name.clone())); let field_origin = origin.map(|origin| origin.field(field.name.clone()));
if object if object
@@ -1763,7 +2027,7 @@ impl<L: ImportLoader> Engine<L> {
let diagnostic = Diagnostic::new( let diagnostic = Diagnostic::new(
DiagnosticKind::Conflict, DiagnosticKind::Conflict,
Span::default(), Span::default(),
format!("duplicate host object field `{}`", field.name), format!("duplicate object field `{}`", field.name),
); );
return Err(annotate_imported_value( return Err(annotate_imported_value(
diagnostic, diagnostic,
@@ -1771,8 +2035,8 @@ impl<L: ImportLoader> Engine<L> {
None, None,
)); ));
} }
let value = self let value =
.internalize_host_value_with_origin(field.value, field_origin.as_ref())?; self.internalize_value_with_origin(field.value, field_origin.as_ref())?;
let value = let value =
self.add_value_thunk_with_optional_import_origin(value, field_origin); self.add_value_thunk_with_optional_import_origin(value, field_origin);
object.fields.push(ObjectField { object.fields.push(ObjectField {
@@ -1781,14 +2045,22 @@ impl<L: ImportLoader> Engine<L> {
span: Span::default(), 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))) Ok(RuntimeValue::Concrete(ConcreteValue::Object(object)))
} }
HostValue::Abstract { Value::Range {
constraints, constraints,
default, default,
} => { } => {
let default = if let Some(default) = 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())) Some(self.add_value_thunk_with_optional_import_origin(value, origin.cloned()))
} else { } else {
None None
@@ -2252,6 +2524,7 @@ fn upper_bound_implies(
fn constraint_description(constraint: &Constraint) -> String { fn constraint_description(constraint: &Constraint) -> String {
match constraint { match constraint {
Constraint::Unknown => String::from("the unknown top range"),
Constraint::Type(primitive) => format!("type {primitive:?}"), Constraint::Type(primitive) => format!("type {primitive:?}"),
Constraint::ArrayItems(_) => String::from("an array element range"), Constraint::ArrayItems(_) => String::from("an array element range"),
Constraint::MapValues(_) => String::from("a map value range"), Constraint::MapValues(_) => String::from("a map value range"),
@@ -2590,7 +2863,7 @@ fn concrete_scalar_eq(lhs: &ConcreteValue, rhs: &ConcreteValue) -> bool {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::{LoadedSource, parse_source}; use crate::parse_source;
fn eval_data(source: &str) -> Data { fn eval_data(source: &str) -> Data {
let parsed = parse_source(source).unwrap(); let parsed = parse_source(source).unwrap();
@@ -2887,6 +3160,29 @@ mod tests {
assert!(value.default.is_none()); 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] #[test]
fn abstract_collection_ranges_can_be_proven_narrower() { fn abstract_collection_ranges_can_be_proven_narrower() {
let parsed = parse_source("[...(Int & > 10)] as [...Int]").unwrap(); let parsed = parse_source("[...(Int & > 10)] as [...Int]").unwrap();
@@ -2980,6 +3276,100 @@ mod tests {
assert_eq!(eval_data("{} as {...String}"), Data::Object(vec![])); 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] #[test]
fn removed_array_primitive_reports_migration() { fn removed_array_primitive_reports_migration() {
let parsed = parse_source("Array").unwrap(); let parsed = parse_source("Array").unwrap();
@@ -3100,11 +3490,11 @@ mod tests {
.ok_or_else(|| { .ok_or_else(|| {
Diagnostic::new(DiagnosticKind::Import, Span::default(), "missing source") Diagnostic::new(DiagnosticKind::Import, Span::default(), "missing source")
})?; })?;
Ok(LoadedImport::Source(LoadedSource { Ok(LoadedImport::Source {
key: specifier.into(), key: specifier.into(),
name: specifier.into(), name: specifier.into(),
source, 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(|| { let source = source.strip_prefix("---\n").ok_or_else(|| {
Diagnostic::new( Diagnostic::new(
DiagnosticKind::Import, DiagnosticKind::Import,
@@ -3172,15 +3562,15 @@ mod tests {
) )
})?; })?;
let value = match value.trim() { let value = match value.trim() {
"true" => HostValue::bool(true), "true" => Value::bool(true),
"false" => HostValue::bool(false), "false" => Value::bool(false),
value => HostValue::string(value.trim_matches('"')), value => Value::string(value.trim_matches('"')),
}; };
fields.push((name.trim(), value)); fields.push((name.trim(), value));
} }
Ok(HostValue::object([ Ok(Value::object([
("frontmatter", HostValue::object(fields)), ("frontmatter", Value::object(fields)),
("body", HostValue::string(body)), ("body", Value::string(body)),
])) ]))
} }
@@ -3275,9 +3665,9 @@ mod tests {
fn load(&mut self, _current_key: Option<&str>, _specifier: &str) -> Result<LoadedImport> { fn load(&mut self, _current_key: Option<&str>, _specifier: &str) -> Result<LoadedImport> {
Ok(LoadedImport::value( Ok(LoadedImport::value(
"content/navigation.json", "content/navigation.json",
HostValue::object([( Value::object([(
"items", "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> { fn load(&mut self, _current_key: Option<&str>, _specifier: &str) -> Result<LoadedImport> {
Ok(LoadedImport::value( Ok(LoadedImport::value(
"shared-value", "shared-value",
HostValue::int_type().default_int(1)?, Value::int_type().default_int(1)?,
)) ))
} }
} }
@@ -3351,16 +3741,10 @@ mod tests {
engine engine
.bind_global( .bind_global(
"Service", "Service",
HostValue::object([ Value::object([
("name", HostValue::string_type()), ("name", Value::string_type()),
( ("port", Value::int_type().gt(443).default_int(8443).unwrap()),
"port", ("enabled", Value::bool_type().default_bool(true).unwrap()),
HostValue::int_type().gt(443).default_int(8443).unwrap(),
),
(
"enabled",
HostValue::bool_type().default_bool(true).unwrap(),
),
]), ]),
) )
.unwrap(); .unwrap();
@@ -3385,12 +3769,9 @@ mod tests {
engine engine
.bind_global( .bind_global(
"Services", "Services",
HostValue::map_of(HostValue::object([ Value::map_of(Value::object([
("port", HostValue::int_type()), ("port", Value::int_type()),
( ("enabled", Value::bool_type().default_bool(true).unwrap()),
"enabled",
HostValue::bool_type().default_bool(true).unwrap(),
),
])), ])),
) )
.unwrap(); .unwrap();
@@ -3410,14 +3791,35 @@ mod tests {
} }
#[test] #[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); let mut engine = Engine::new(EmptyLoader);
engine engine
.bind_global( .bind_global(
"Service", "OpenService",
HostValue::object([("port", HostValue::int_type())]), Value::object_with_rest([("name", Value::string_type())], Value::unknown()),
) )
.unwrap(); .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 let module = engine
.add_root_source("main", "main", r#"{ port = "not-an-int"; } as Service"#) .add_root_source("main", "main", r#"{ port = "not-an-int"; } as Service"#)
.unwrap(); .unwrap();
@@ -3435,7 +3837,7 @@ mod tests {
#[test] #[test]
fn module_binding_shadows_host_prelude() { fn module_binding_shadows_host_prelude() {
let mut engine = Engine::new(EmptyLoader); 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 let module = engine
.add_root_source("main", "main", r#"Service = "local"; result = Service;"#) .add_root_source("main", "main", r#"Service = "local"; result = Service;"#)
.unwrap(); .unwrap();
+6 -8
View File
@@ -5,7 +5,6 @@ extern crate alloc;
pub mod ast; pub mod ast;
pub mod constraints; pub mod constraints;
pub mod diagnostic; pub mod diagnostic;
pub mod embedding;
pub mod environment; pub mod environment;
pub mod eval; pub mod eval;
mod lexer; mod lexer;
@@ -14,29 +13,28 @@ pub mod parser;
pub mod runtime; pub mod runtime;
pub mod span; pub mod span;
pub mod typed; 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; pub use constraints::normalize_constraints;
#[cfg(feature = "derive")] #[cfg(feature = "derive")]
pub use decodal_derive::Decodal; pub use decodal_derive::Decodal;
pub use diagnostic::{Diagnostic, DiagnosticKind, Result}; pub use diagnostic::{Diagnostic, DiagnosticKind, Result};
pub use embedding::{HostField, HostValue};
pub use environment::HostEnvironment; pub use environment::HostEnvironment;
pub use eval::{Engine, format_diagnostic_with}; pub use eval::{Engine, format_diagnostic_with};
pub use lexer::{ pub use lexer::{
Token as SyntaxToken, TokenKind as SyntaxTokenKind, tokenize_source, Token as SyntaxToken, TokenKind as SyntaxTokenKind, tokenize_source,
tokenize_source_with_source_id, tokenize_source_with_source_id,
}; };
pub use module::{ pub use module::{EmptyLoader, ImportCandidate, ImportLoader, LoadedImport, Module};
EmptyLoader, ImportCandidate, ImportLoader, LoadedImport, LoadedSource, LoadedValue, Module,
};
pub use parser::{ParseOutput, Parser, SourceForm, parse_source, parse_source_with_source_id}; 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 span::{SourceId, Span};
pub use typed::{ 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, decode_path, prefix_decode_error,
}; };
pub use value::{Value, ValueField};
pub fn version() -> &'static str { pub fn version() -> &'static str {
env!("CARGO_PKG_VERSION") env!("CARGO_PKG_VERSION")
+16 -21
View File
@@ -1,7 +1,7 @@
use alloc::{string::String, vec::Vec}; use alloc::{string::String, vec::Vec};
use crate::{ use crate::{
Ast, ExprId, HostValue, SourceForm, SourceId, Ast, ExprId, SourceForm, SourceId, Value,
runtime::{EnvId, ThunkId}, runtime::{EnvId, ThunkId},
}; };
@@ -17,23 +17,18 @@ pub struct Module {
pub root_thunk: ThunkId, pub root_thunk: ThunkId,
} }
#[derive(Debug, Clone)] /// Content resolved by an [`ImportLoader`].
pub struct LoadedSource {
pub key: String,
pub name: String,
pub source: String,
}
#[derive(Debug, Clone)]
pub struct LoadedValue {
pub key: String,
pub value: HostValue,
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum LoadedImport { pub enum LoadedImport {
Source(LoadedSource), Source {
Value(LoadedValue), key: String,
name: String,
source: String,
},
Value {
key: String,
value: Value,
},
} }
/// An import specifier offered by host-owned language tooling. /// An import specifier offered by host-owned language tooling.
@@ -63,18 +58,18 @@ impl LoadedImport {
name: impl Into<String>, name: impl Into<String>,
source: impl Into<String>, source: impl Into<String>,
) -> Self { ) -> Self {
Self::Source(LoadedSource { Self::Source {
key: key.into(), key: key.into(),
name: name.into(), name: name.into(),
source: source.into(), source: source.into(),
}) }
} }
pub fn value(key: impl Into<String>, value: HostValue) -> Self { pub fn value(key: impl Into<String>, value: Value) -> Self {
Self::Value(LoadedValue { Self::Value {
key: key.into(), key: key.into(),
value, value,
}) }
} }
} }
+41 -9
View File
@@ -2,7 +2,10 @@ use alloc::{string::String, vec::Vec};
use crate::{ use crate::{
SourceId, Span, 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}, diagnostic::{Diagnostic, Result},
lexer::{Lexer, Token, TokenKind}, lexer::{Lexer, Token, TokenKind},
}; };
@@ -59,7 +62,7 @@ impl Parser {
}) })
.unwrap_or_else(|| self.peek().span); .unwrap_or_else(|| self.peek().span);
( (
self.ast.push(Expr::Object(fields), span), self.ast.push(Expr::Object { fields, rest: None }, span),
SourceForm::Fields, SourceForm::Fields,
) )
} else { } else {
@@ -338,23 +341,38 @@ impl Parser {
let mut fields = Vec::new(); let mut fields = Vec::new();
if self.consume_kind(&TokenKind::RBrace).is_some() { if self.consume_kind(&TokenKind::RBrace).is_some() {
return Ok(self return Ok(self.ast.push(
.ast Expr::Object { fields, rest: None },
.push(Expr::Object(fields), start_span.join(self.previous_span()))); start_span.join(self.previous_span()),
));
} }
let mut rest = None;
loop { loop {
fields.push(self.parse_field()?); fields.push(self.parse_field()?);
if self.consume_kind(&TokenKind::Semicolon).is_some() { if self.consume_kind(&TokenKind::Semicolon).is_some() {
if self.consume_kind(&TokenKind::RBrace).is_some() { if self.consume_kind(&TokenKind::RBrace).is_some() {
break; 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; continue;
} }
self.expect_kind(&TokenKind::RBrace, "expected ';' or '}' after object field")?; self.expect_kind(&TokenKind::RBrace, "expected ';' or '}' after object field")?;
break; break;
} }
let span = start_span.join(self.previous_span()); 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> { fn parse_array_after_lbracket(&mut self, start_span: Span) -> Result<ExprId> {
@@ -697,13 +715,16 @@ mod tests {
#[test] #[test]
fn parses_top_level_fields_as_object() { fn parses_top_level_fields_as_object() {
let parsed = parse_source("port = Int & >= 1 default 8080;").unwrap(); 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] #[test]
fn parses_object_dot_field() { fn parses_object_dot_field() {
let parsed = parse_source("{ feature.enable = false; }").unwrap(); 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!() panic!()
}; };
assert_eq!(fields[0].path, ["feature", "enable"]); assert_eq!(fields[0].path, ["feature", "enable"]);
@@ -726,7 +747,7 @@ mod tests {
fn parser_accepts_the_public_lossless_token_stream() { fn parser_accepts_the_public_lossless_token_stream() {
let tokens = crate::tokenize_source("value = (# note\n 1);").unwrap(); let tokens = crate::tokenize_source("value = (# note\n 1);").unwrap();
let parsed = Parser::new(tokens).parse().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!() panic!()
}; };
assert!(matches!( assert!(matches!(
@@ -749,6 +770,17 @@ mod tests {
assert!(matches!(parsed.ast.get(value).expr, Expr::Ident(_))); 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] #[test]
fn parses_as_below_composition() { fn parses_as_below_composition() {
let parsed = parse_source("value & override as Schema").unwrap(); let parsed = parse_source("value & override as Schema").unwrap();
+12
View File
@@ -11,6 +11,9 @@ pub struct ExprRef {
pub expr: ExprId, 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)] #[derive(Debug, Clone, PartialEq)]
pub enum RuntimeValue { pub enum RuntimeValue {
Concrete(ConcreteValue), Concrete(ConcreteValue),
@@ -31,6 +34,13 @@ pub enum ConcreteValue {
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct ObjectValue { pub struct ObjectValue {
pub fields: Vec<ObjectField>, 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)] #[derive(Debug, Clone, PartialEq)]
@@ -67,6 +77,7 @@ pub struct ConstraintEntry {
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub enum Constraint { pub enum Constraint {
Unknown,
Type(PrimitiveType), Type(PrimitiveType),
ArrayItems(ThunkId), ArrayItems(ThunkId),
MapValues(ThunkId), MapValues(ThunkId),
@@ -91,6 +102,7 @@ pub enum LiteralValue {
Bool(bool), Bool(bool),
} }
/// Fully materialized Decodal data.
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub enum Data { pub enum Data {
String(String), String(String),
+176 -51
View File
@@ -1,17 +1,24 @@
use alloc::{collections::BTreeMap, format, string::String, vec::Vec}; use alloc::{collections::BTreeMap, format, string::String, vec::Vec};
use crate::{Data, HostValue}; use crate::{Data, Value, runtime::DataField};
pub trait DecodalSchema { pub trait DecodalSchema {
fn decodal_schema() -> HostValue; fn decodal_schema() -> Value;
} }
pub trait DecodalDecode: Sized { pub trait DecodalDecode: Sized {
fn decodal_decode(data: &Data) -> DecodeResult<Self>; fn decodal_decode(data: &Data) -> DecodeResult<Self>;
} }
pub trait IntoHostValue { pub trait IntoValue {
fn into_host_value(self) -> HostValue; 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>; pub type DecodeResult<T> = core::result::Result<T, DecodeError>;
@@ -80,9 +87,38 @@ fn prefix_error(path: &str, mut error: DecodeError) -> DecodeError {
error 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 { impl DecodalSchema for String {
fn decodal_schema() -> HostValue { fn decodal_schema() -> Value {
HostValue::string_type() Value::string_type()
} }
} }
@@ -95,15 +131,15 @@ impl DecodalDecode for String {
} }
} }
impl IntoHostValue for String { impl IntoValue for String {
fn into_host_value(self) -> HostValue { fn into_value(self) -> Value {
HostValue::string(self) Value::string(self)
} }
} }
impl IntoHostValue for &str { impl IntoValue for &str {
fn into_host_value(self) -> HostValue { fn into_value(self) -> Value {
HostValue::string(self) Value::string(self)
} }
} }
@@ -111,8 +147,8 @@ macro_rules! impl_int_decode {
($($ty:ty),* $(,)?) => { ($($ty:ty),* $(,)?) => {
$( $(
impl DecodalSchema for $ty { impl DecodalSchema for $ty {
fn decodal_schema() -> HostValue { fn decodal_schema() -> Value {
HostValue::int_type() Value::int_type()
} }
} }
@@ -126,9 +162,9 @@ macro_rules! impl_int_decode {
} }
} }
impl IntoHostValue for $ty { impl IntoValue for $ty {
fn into_host_value(self) -> HostValue { fn into_value(self) -> Value {
HostValue::int(self as i64) 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_int_decode!(i8, i16, i32, i64, u8, u16, u32);
impl DecodalSchema for f64 { impl DecodalSchema for f64 {
fn decodal_schema() -> HostValue { fn decodal_schema() -> Value {
HostValue::float_type() Value::float_type()
} }
} }
@@ -153,15 +189,15 @@ impl DecodalDecode for f64 {
} }
} }
impl IntoHostValue for f64 { impl IntoValue for f64 {
fn into_host_value(self) -> HostValue { fn into_value(self) -> Value {
HostValue::float(self) Value::float(self)
} }
} }
impl DecodalSchema for f32 { impl DecodalSchema for f32 {
fn decodal_schema() -> HostValue { fn decodal_schema() -> Value {
HostValue::float_type() Value::float_type()
} }
} }
@@ -171,15 +207,15 @@ impl DecodalDecode for f32 {
} }
} }
impl IntoHostValue for f32 { impl IntoValue for f32 {
fn into_host_value(self) -> HostValue { fn into_value(self) -> Value {
HostValue::float(f64::from(self)) Value::float(f64::from(self))
} }
} }
impl DecodalSchema for bool { impl DecodalSchema for bool {
fn decodal_schema() -> HostValue { fn decodal_schema() -> Value {
HostValue::bool_type() Value::bool_type()
} }
} }
@@ -192,15 +228,15 @@ impl DecodalDecode for bool {
} }
} }
impl IntoHostValue for bool { impl IntoValue for bool {
fn into_host_value(self) -> HostValue { fn into_value(self) -> Value {
HostValue::bool(self) Value::bool(self)
} }
} }
impl<T: DecodalSchema> DecodalSchema for Vec<T> { impl<T: DecodalSchema> DecodalSchema for Vec<T> {
fn decodal_schema() -> HostValue { fn decodal_schema() -> Value {
HostValue::array_of(T::decodal_schema()) Value::array_of(T::decodal_schema())
} }
} }
@@ -220,15 +256,15 @@ impl<T: DecodalDecode> DecodalDecode for Vec<T> {
} }
} }
impl<T: IntoHostValue> IntoHostValue for Vec<T> { impl<T: IntoValue> IntoValue for Vec<T> {
fn into_host_value(self) -> HostValue { fn into_value(self) -> Value {
HostValue::array(self.into_iter().map(IntoHostValue::into_host_value)) Value::array(self.into_iter().map(IntoValue::into_value))
} }
} }
impl<T: DecodalSchema> DecodalSchema for BTreeMap<String, T> { impl<T: DecodalSchema> DecodalSchema for BTreeMap<String, T> {
fn decodal_schema() -> HostValue { fn decodal_schema() -> Value {
HostValue::map_of(T::decodal_schema()) 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> { impl<T: IntoValue> IntoValue for BTreeMap<String, T> {
fn into_host_value(self) -> HostValue { fn into_value(self) -> Value {
HostValue::object( Value::object(
self.into_iter() 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")] #[cfg(feature = "std")]
impl<T: DecodalSchema> DecodalSchema for std::collections::HashMap<String, T> { impl<T: DecodalSchema> DecodalSchema for std::collections::HashMap<String, T> {
fn decodal_schema() -> HostValue { fn decodal_schema() -> Value {
HostValue::map_of(T::decodal_schema()) Value::map_of(T::decodal_schema())
} }
} }
@@ -282,17 +335,41 @@ impl<T: DecodalDecode> DecodalDecode for std::collections::HashMap<String, T> {
} }
#[cfg(feature = "std")] #[cfg(feature = "std")]
impl<T: IntoHostValue> IntoHostValue for std::collections::HashMap<String, T> { impl<T: IntoValue> IntoValue for std::collections::HashMap<String, T> {
fn into_host_value(self) -> HostValue { fn into_value(self) -> Value {
let mut fields: Vec<_> = self let mut fields: Vec<_> = self
.into_iter() .into_iter()
.map(|(name, value)| (name, value.into_host_value())) .map(|(name, value)| (name, value.into_value()))
.collect(); .collect();
fields.sort_by(|left, right| left.0.cmp(&right.0)); 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -302,10 +379,10 @@ mod tests {
fn string_maps_expose_map_schemas_and_decode_objects() { fn string_maps_expose_map_schemas_and_decode_objects() {
assert!(matches!( assert!(matches!(
BTreeMap::<String, i64>::decodal_schema(), BTreeMap::<String, i64>::decodal_schema(),
HostValue::MapConstraint { value, .. } Value::MapRange { value, .. }
if matches!( if matches!(
*value, *value,
HostValue::Abstract { ref constraints, .. } Value::Range { ref constraints, .. }
if constraints == &[Constraint::Type(PrimitiveType::Int)] if constraints == &[Constraint::Type(PrimitiveType::Int)]
) )
)); ));
@@ -317,4 +394,52 @@ mod tests {
let decoded = BTreeMap::<String, i64>::decodal_decode(&data).unwrap(); let decoded = BTreeMap::<String, i64>::decodal_decode(&data).unwrap();
assert_eq!(decoded.get("api"), Some(&8080)); 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::runtime::{Constraint, LiteralValue, PrimitiveType};
use crate::{CompareOp, Diagnostic, DiagnosticKind, Result, Span}; 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)] #[derive(Debug, Clone, PartialEq)]
pub enum HostValue { pub enum Value {
String(String), String(String),
Int(i64), Int(i64),
Float(f64), Float(f64),
Bool(bool), Bool(bool),
Array(Vec<HostValue>), Array(Vec<Value>),
ArrayConstraint { ArrayRange {
item: Box<HostValue>, item: Box<Value>,
constraints: Vec<Constraint>, constraints: Vec<Constraint>,
default: Option<Box<HostValue>>, default: Option<Box<Value>>,
}, },
MapConstraint { MapRange {
value: Box<HostValue>, value: Box<Value>,
constraints: Vec<Constraint>, constraints: Vec<Constraint>,
default: Option<Box<HostValue>>, default: Option<Box<Value>>,
}, },
Object(Vec<HostField>), Object {
Abstract { fields: Vec<ValueField>,
rest: Option<Box<Value>>,
},
Range {
constraints: Vec<Constraint>, constraints: Vec<Constraint>,
default: Option<Box<HostValue>>, default: Option<Box<Value>>,
}, },
} }
/// A named field in a [`Value::Object`].
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
pub struct HostField { pub struct ValueField {
pub name: String, pub name: String,
pub value: HostValue, pub value: Value,
} }
impl HostValue { impl Value {
pub fn string(value: impl Into<String>) -> Self { pub fn string(value: impl Into<String>) -> Self {
Self::String(value.into()) Self::String(value.into())
} }
@@ -52,65 +61,105 @@ impl HostValue {
pub fn array<I>(items: I) -> Self pub fn array<I>(items: I) -> Self
where where
I: IntoIterator<Item = HostValue>, I: IntoIterator<Item = Value>,
{ {
Self::Array(items.into_iter().collect()) Self::Array(items.into_iter().collect())
} }
pub fn object<I, N>(fields: I) -> Self pub fn object<I, N>(fields: I) -> Self
where where
I: IntoIterator<Item = (N, HostValue)>, I: IntoIterator<Item = (N, Value)>,
N: Into<String>, N: Into<String>,
{ {
Self::Object( Self::Object {
fields fields: fields
.into_iter() .into_iter()
.map(|(name, value)| HostField { .map(|(name, value)| ValueField {
name: name.into(), name: name.into(),
value, value,
}) })
.collect(), .collect(),
) rest: None,
}
} }
pub fn object_from_paths<I, N>(fields: I) -> Self pub fn object_from_paths<I, N>(fields: I) -> Self
where where
I: IntoIterator<Item = (N, HostValue)>, I: IntoIterator<Item = (N, Value)>,
N: Into<String>, N: Into<String>,
{ {
let mut root = Vec::new(); let mut root = Vec::new();
for (path, value) in fields { for (path, value) in fields {
insert_path(&mut root, &path.into(), value); 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 { 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 { 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 { 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 { 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 { pub fn array_of(item: Value) -> Self {
Self::ArrayConstraint { Self::ArrayRange {
item: Box::new(item), item: Box::new(item),
constraints: Vec::new(), constraints: Vec::new(),
default: None, default: None,
} }
} }
pub fn map_of(value: HostValue) -> Self { pub fn map_of(value: Value) -> Self {
Self::MapConstraint { Self::MapRange {
value: Box::new(value), value: Box::new(value),
constraints: Vec::new(), constraints: Vec::new(),
default: None, default: None,
@@ -118,11 +167,11 @@ impl HostValue {
} }
pub fn builtin_predicate(name: impl Into<String>) -> Self { 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 { pub fn range_with_constraint(constraint: Constraint) -> Self {
Self::Abstract { Self::Range {
constraints: alloc::vec![constraint], constraints: alloc::vec![constraint],
default: None, default: None,
} }
@@ -130,12 +179,12 @@ impl HostValue {
pub fn with_constraint(mut self, constraint: Constraint) -> Self { pub fn with_constraint(mut self, constraint: Constraint) -> Self {
match &mut self { match &mut self {
Self::Abstract { constraints, .. } => constraints.push(constraint), Self::Range { constraints, .. } => constraints.push(constraint),
Self::ArrayConstraint { constraints, .. } | Self::MapConstraint { constraints, .. } => { Self::ArrayRange { constraints, .. } | Self::MapRange { constraints, .. } => {
constraints.push(constraint) constraints.push(constraint)
} }
_ => { _ => {
self = Self::Abstract { self = Self::Range {
constraints: alloc::vec![constraint], constraints: alloc::vec![constraint],
default: Some(Box::new(self)), 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 { match self {
Self::ArrayConstraint { Self::ArrayRange {
item, item,
constraints, constraints,
default: None, default: None,
} => Ok(Self::ArrayConstraint { } => Ok(Self::ArrayRange {
item, item,
constraints, constraints,
default: Some(Box::new(value)), default: Some(Box::new(value)),
}), }),
Self::ArrayConstraint { Self::ArrayRange {
default: Some(_), .. default: Some(_), ..
} => Err(Diagnostic::new( } => Err(Diagnostic::new(
DiagnosticKind::DefaultConflict, DiagnosticKind::DefaultConflict,
Span::default(), Span::default(),
"host value already has a default", "value already has a default",
)), )),
Self::MapConstraint { Self::MapRange {
value: map_value, value: map_value,
constraints, constraints,
default: None, default: None,
} => Ok(Self::MapConstraint { } => Ok(Self::MapRange {
value: map_value, value: map_value,
constraints, constraints,
default: Some(Box::new(value)), default: Some(Box::new(value)),
}), }),
Self::MapConstraint { Self::MapRange {
default: Some(_), .. default: Some(_), ..
} => Err(Diagnostic::new( } => Err(Diagnostic::new(
DiagnosticKind::DefaultConflict, DiagnosticKind::DefaultConflict,
Span::default(), Span::default(),
"host value already has a default", "value already has a default",
)), )),
Self::Abstract { Self::Range {
constraints, constraints,
default: None, default: None,
} => Ok(Self::Abstract { } => Ok(Self::Range {
constraints, constraints,
default: Some(Box::new(value)), default: Some(Box::new(value)),
}), }),
Self::Abstract { .. } => Err(Diagnostic::new( Self::Range { .. } => Err(Diagnostic::new(
DiagnosticKind::DefaultConflict, DiagnosticKind::DefaultConflict,
Span::default(), Span::default(),
"host value already has a default", "value already has a default",
)), )),
concrete => Ok(Self::Abstract { concrete => Ok(Self::Range {
constraints: Vec::new(), constraints: Vec::new(),
default: Some(Box::new(concrete)), default: Some(Box::new(concrete)),
}), }),
@@ -236,8 +285,8 @@ impl HostValue {
} }
} }
impl HostField { impl ValueField {
pub fn new(name: impl Into<String>, value: HostValue) -> Self { pub fn new(name: impl Into<String>, value: Value) -> Self {
Self { Self {
name: name.into(), name: name.into(),
value, 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 mut parts = path.splitn(2, '.');
let Some(head) = parts.next().filter(|part| !part.is_empty()) else { let Some(head) = parts.next().filter(|part| !part.is_empty()) else {
return; return;
}; };
if let Some(tail) = parts.next() { if let Some(tail) = parts.next() {
if let Some(field) = fields.iter_mut().find(|field| field.name == head) { 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); insert_path(children, tail, value);
} else { } else {
let mut children = Vec::new(); let mut children = Vec::new();
insert_path(&mut children, tail, value); insert_path(&mut children, tail, value);
field.value = HostValue::Object(children); field.value = Value::Object {
fields: children,
rest: None,
};
} }
} else { } else {
let mut children = Vec::new(); let mut children = Vec::new();
insert_path(&mut children, tail, value); 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) { } else if let Some(field) = fields.iter_mut().find(|field| field.name == head) {
field.value = value; field.value = value;
} else { } else {
fields.push(HostField::new(head, value)); fields.push(ValueField::new(head, value));
} }
} }
+106 -5
View File
@@ -35,12 +35,38 @@ fn expand_decodal(input: DeriveInput) -> Result<TokenStream2> {
let mut schema_fields = Vec::new(); let mut schema_fields = Vec::new();
let mut decode_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 { for field in fields {
let ident = field.ident.expect("named field"); let ident = field.ident.expect("named field");
let ty = field.ty; let ty = field.ty;
let attrs = FieldAttrs::from_attrs(&field.attrs, &ident.to_string())?; 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(); 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 schema_value = schema_expr(&ty, &attrs)?;
let decode_value = decode_expr(&ty, &path, &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(); let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl();
Ok(quote! { Ok(quote! {
impl #impl_generics ::decodal::DecodalSchema for #name #ty_generics #where_clause { impl #impl_generics ::decodal::DecodalSchema for #name #ty_generics #where_clause {
fn decodal_schema() -> ::decodal::HostValue { fn decodal_schema() -> ::decodal::Value {
::decodal::HostValue::object_from_paths([ #build_schema
#(#schema_fields),*
])
} }
} }
@@ -76,6 +128,8 @@ fn expand_decodal(input: DeriveInput) -> Result<TokenStream2> {
#[derive(Default)] #[derive(Default)]
struct FieldAttrs { struct FieldAttrs {
rename: String, rename: String,
rename_explicit: bool,
rest: bool,
default: Option<DefaultAttr>, default: Option<DefaultAttr>,
constraints: Vec<ConstraintAttr>, constraints: Vec<ConstraintAttr>,
} }
@@ -95,6 +149,14 @@ impl FieldAttrs {
if meta.path.is_ident("rename") { if meta.path.is_ident("rename") {
let value: LitStr = meta.value()?.parse()?; let value: LitStr = meta.value()?.parse()?;
output.rename = value.value(); 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(()); return Ok(());
} }
if meta.path.is_ident("default") { if meta.path.is_ident("default") {
@@ -182,7 +244,7 @@ fn schema_expr(ty: &Type, attrs: &FieldAttrs) -> Result<TokenStream2> {
}; };
tokens.extend(quote! { tokens.extend(quote! {
schema = schema schema = schema
.default(::decodal::IntoHostValue::into_host_value(#default_expr)) .default(::decodal::IntoValue::into_value(#default_expr))
.expect("Decodal derive generated a valid default"); .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")
);
}
}
+119 -1
View File
@@ -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; use decodal_derive::Decodal;
#[derive(Debug, PartialEq, Decodal)] #[derive(Debug, PartialEq, Decodal)]
@@ -24,6 +26,28 @@ struct Fleet {
workers: Vec<Worker>, 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] #[test]
fn derives_schema_and_decode() { fn derives_schema_and_decode() {
let mut engine = Engine::new(EmptyLoader); 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 std::collections::{BTreeMap, HashMap};
use decodal::{ use decodal::{
Engine, HostEnvironment, HostValue, ImportLoader, LoadedImport, Result, RuntimeValue, Engine, HostEnvironment, ImportLoader, LoadedImport, Result, Value, runtime::RuntimeValue,
}; };
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -116,12 +116,12 @@ pub(crate) fn complete<E: HostEnvironment>(
let mut loader = environment.create_loader(); let mut loader = environment.create_loader();
let loaded = loader.load(Some(key), specifier)?; let loaded = loader.load(Some(key), specifier)?;
detail = match &loaded { detail = match &loaded {
LoadedImport::Source(source) => source.name.clone(), LoadedImport::Source { name, .. } => name.clone(),
LoadedImport::Value(value) => value.key.clone(), LoadedImport::Value { key, .. } => key.clone(),
}; };
fields = match loaded { fields = match loaded {
LoadedImport::Source(source) => collect_fields(&tokenize(&source.source)), LoadedImport::Source { source, .. } => collect_fields(&tokenize(&source)),
LoadedImport::Value(value) => fields_from_host_value(&value.value), LoadedImport::Value { value, .. } => fields_from_value(&value),
}; };
} else if let Some(local) = local_fields.get(root) { } else if let Some(local) = local_fields.get(root) {
fields = local.clone(); fields = local.clone();
@@ -229,6 +229,7 @@ fn builtin_items() -> Vec<CompletionItem> {
("Int", CompletionKind::Type, "integer constraint", 5), ("Int", CompletionKind::Type, "integer constraint", 5),
("Float", CompletionKind::Type, "float constraint", 5), ("Float", CompletionKind::Type, "float constraint", 5),
("Bool", CompletionKind::Type, "boolean constraint", 5), ("Bool", CompletionKind::Type, "boolean constraint", 5),
("Unknown", CompletionKind::Type, "unknown top range", 5),
] ]
.into_iter() .into_iter()
.map(|(label, kind, detail, priority)| CompletionItem { .map(|(label, kind, detail, priority)| CompletionItem {
@@ -267,24 +268,24 @@ fn fields_from_runtime<L: ImportLoader>(
Ok(tree) Ok(tree)
} }
fn fields_from_host_value(value: &HostValue) -> FieldTree { fn fields_from_value(value: &Value) -> FieldTree {
match value { match value {
HostValue::Object(fields) => fields Value::Object { fields, .. } => fields
.iter() .iter()
.map(|field| (field.name.clone(), fields_from_host_value(&field.value))) .map(|field| (field.name.clone(), fields_from_value(&field.value)))
.collect(), .collect(),
HostValue::ArrayConstraint { Value::ArrayRange {
default: Some(value), default: Some(value),
.. ..
} }
| HostValue::MapConstraint { | Value::MapRange {
default: Some(value), default: Some(value),
.. ..
} }
| HostValue::Abstract { | Value::Range {
default: Some(value), default: Some(value),
.. ..
} => fields_from_host_value(value), } => fields_from_value(value),
_ => FieldTree::new(), _ => FieldTree::new(),
} }
} }
@@ -688,7 +689,7 @@ mod tests {
use std::collections::BTreeMap; use std::collections::BTreeMap;
use decodal::{ use decodal::{
Diagnostic, DiagnosticKind, EmptyLoader, HostValue, ImportCandidate, LoadedImport, Span, Diagnostic, DiagnosticKind, EmptyLoader, ImportCandidate, LoadedImport, Span, Value,
}; };
use super::*; use super::*;
@@ -708,15 +709,15 @@ mod tests {
if specifier == "./post.md" { if specifier == "./post.md" {
return Ok(LoadedImport::value( return Ok(LoadedImport::value(
"post.md", "post.md",
HostValue::object([ Value::object([
( (
"frontmatter", "frontmatter",
HostValue::object([ Value::object([
("title", HostValue::string("Hello")), ("title", Value::string("Hello")),
("draft", HostValue::bool(false)), ("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<()> { fn configure_engine(&self, engine: &mut Engine<Self::Loader>) -> Result<()> {
engine.bind_global( engine.bind_global(
"App", "App",
HostValue::object([( Value::object([(
"config", "config",
HostValue::object([ Value::object([("enabled", Value::bool_type()), ("port", Value::int_type())]),
("enabled", HostValue::bool_type()),
("port", HostValue::int_type()),
]),
)]), )]),
)?; )?;
Ok(()) Ok(())
@@ -854,6 +852,7 @@ mod tests {
.unwrap(); .unwrap();
let labels = labels(result); let labels = labels(result);
assert!(labels.contains(&"String".into())); assert!(labels.contains(&"String".into()));
assert!(labels.contains(&"Unknown".into()));
assert!(labels.contains(&"service".into())); assert!(labels.contains(&"service".into()));
assert!(labels.contains(&"value".into())); assert!(labels.contains(&"value".into()));
assert!(labels.contains(&"App".into())); assert!(labels.contains(&"App".into()));
@@ -881,6 +880,10 @@ mod tests {
.unwrap() .unwrap()
.unwrap(); .unwrap();
assert!(labels(result).contains(&"String".into())); 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; struct EmptyEnvironment;
+14 -14
View File
@@ -92,7 +92,7 @@ impl SemanticAnalysis {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use decodal::{ use decodal::{
Data, Diagnostic, DiagnosticKind, Engine, HostValue, ImportLoader, LoadedImport, Span, Data, Diagnostic, DiagnosticKind, Engine, ImportLoader, LoadedImport, Span, Value,
}; };
use super::*; use super::*;
@@ -100,11 +100,11 @@ mod tests {
const ROOT: &str = r#"Post & import "./post.md""#; const ROOT: &str = r#"Post & import "./post.md""#;
struct ContentEnvironment { struct ContentEnvironment {
draft: HostValue, draft: Value,
} }
struct ContentLoader { struct ContentLoader {
draft: HostValue, draft: Value,
} }
impl ImportLoader for ContentLoader { impl ImportLoader for ContentLoader {
@@ -122,15 +122,15 @@ mod tests {
} }
Ok(LoadedImport::value( Ok(LoadedImport::value(
"content/post.md", "content/post.md",
HostValue::object([ Value::object([
( (
"frontmatter", "frontmatter",
HostValue::object([ Value::object([
("title", HostValue::string("Hello")), ("title", Value::string("Hello")),
("draft", self.draft.clone()), ("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<()> { fn configure_engine(&self, engine: &mut Engine<Self::Loader>) -> decodal::Result<()> {
engine.bind_global( engine.bind_global(
"Post", "Post",
HostValue::object([ Value::object([
( (
"frontmatter", "frontmatter",
HostValue::object([ Value::object([
("title", HostValue::string_type()), ("title", Value::string_type()),
("draft", HostValue::bool_type()), ("draft", Value::bool_type()),
]), ]),
), ),
("body", HostValue::string_type()), ("body", Value::string_type()),
]), ]),
)?; )?;
Ok(()) Ok(())
@@ -173,7 +173,7 @@ mod tests {
#[test] #[test]
fn injected_environment_matches_direct_evaluation() { fn injected_environment_matches_direct_evaluation() {
let environment = ContentEnvironment { let environment = ContentEnvironment {
draft: HostValue::bool(false), draft: Value::bool(false),
}; };
let direct = evaluate_direct(&environment).unwrap(); let direct = evaluate_direct(&environment).unwrap();
let service = LanguageService::new(&environment); let service = LanguageService::new(&environment);
@@ -186,7 +186,7 @@ mod tests {
#[test] #[test]
fn injected_environment_preserves_import_diagnostics() { fn injected_environment_preserves_import_diagnostics() {
let service = LanguageService::new(ContentEnvironment { let service = LanguageService::new(ContentEnvironment {
draft: HostValue::string("maybe"), draft: Value::string("maybe"),
}); });
let analysis = service.analyze("main.dcdl", "main.dcdl", ROOT); let analysis = service.analyze("main.dcdl", "main.dcdl", ROOT);
+57 -9
View File
@@ -1,8 +1,8 @@
use std::{error::Error, fmt}; use std::{error::Error, fmt};
use decodal::{ use decodal::{
Ast, BinaryOp, CompareOp, Expr, ExprId, Field, Param, SourceForm, Span, SyntaxToken, Ast, BinaryOp, CompareOp, Expr, ExprId, Field, ObjectRest, Param, SourceForm, Span,
SyntaxTokenKind, SyntaxToken, SyntaxTokenKind,
ast::{MatchArm, UnaryOp}, ast::{MatchArm, UnaryOp},
parse_source, tokenize_source, parse_source, tokenize_source,
}; };
@@ -81,7 +81,7 @@ impl<'a> Formatter<'a> {
fn format(&self, root: ExprId, source_form: SourceForm) -> String { fn format(&self, root: ExprId, source_form: SourceForm) -> String {
let mut out = String::new(); let mut out = String::new();
if source_form == SourceForm::Fields { 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") unreachable!("field-form sources parse to an object")
}; };
self.write_field_list(&mut out, fields, 0, self.source.len(), 0); 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) if self.has_comment(node.span)
&& !matches!( && !matches!(
node.expr, 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)); out.push_str(self.raw(node.span));
@@ -162,7 +162,9 @@ impl<'a> Formatter<'a> {
Expr::Literal(_) | Expr::Ident(_) | Expr::RegexConstraint(_) | Expr::Wildcard => { Expr::Literal(_) | Expr::Ident(_) | Expr::RegexConstraint(_) | Expr::Wildcard => {
out.push_str(self.raw(node.span)); 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::Array(items) => self.write_array(out, node.span, items, indent),
Expr::ArrayConstraint { item } => { Expr::ArrayConstraint { item } => {
out.push_str("[..."); 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) = let (start, end) =
self.delimited_range(span, SyntaxTokenKind::LBrace, SyntaxTokenKind::RBrace); 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("{}"); out.push_str("{}");
return; return;
} }
out.push_str("{\n"); 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); write_indent(out, indent);
out.push('}'); out.push('}');
} }
@@ -524,7 +561,7 @@ impl<'a> Formatter<'a> {
Expr::As { narrower, wider } => { Expr::As { narrower, wider } => {
self.is_inline_expr(*narrower) && self.is_inline_expr(*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 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] #[test]
fn preserves_regex_and_escaped_strings() { fn preserves_regex_and_escaped_strings() {
let source = r#"value={pattern=/^api\/.+$/;text="a\n\"b";};"#; let source = r#"value={pattern=/^api\/.+$/;text="a\n\"b";};"#;
+8 -11
View File
@@ -7,7 +7,7 @@ use std::{
use decodal::{ use decodal::{
Diagnostic as DecodalDiagnostic, DiagnosticKind, HostEnvironment, ImportCandidate, Diagnostic as DecodalDiagnostic, DiagnosticKind, HostEnvironment, ImportCandidate,
ImportLoader, LoadedImport, LoadedSource, SourceId, Span, ImportLoader, LoadedImport, SourceId, Span,
}; };
use decodal_language_service::{ use decodal_language_service::{
CompletionKind as ServiceCompletionKind, CompletionResult, LanguageService, SemanticAnalysis, CompletionKind as ServiceCompletionKind, CompletionResult, LanguageService, SemanticAnalysis,
@@ -667,11 +667,11 @@ impl ImportLoader for FileSystemLoader {
) )
})? })?
}; };
Ok(LoadedImport::Source(LoadedSource { Ok(LoadedImport::Source {
key: key.clone(), key: key.clone(),
name: key, name: key,
source, source,
})) })
} }
fn complete_import( fn complete_import(
@@ -743,7 +743,7 @@ mod tests {
time::Duration, time::Duration,
}; };
use decodal::{Engine, HostValue}; use decodal::{Engine, Value};
use lsp_server::RequestId; use lsp_server::RequestId;
use serde_json::json; use serde_json::json;
@@ -772,12 +772,12 @@ mod tests {
.is_some_and(|source| source == "draft: false"); .is_some_and(|source| source == "draft: false");
Ok(LoadedImport::value( Ok(LoadedImport::value(
"content/post.md", "content/post.md",
HostValue::object([( Value::object([(
"draft", "draft",
if draft { if draft {
HostValue::bool(false) Value::bool(false)
} else { } else {
HostValue::string("maybe") Value::string("maybe")
}, },
)]), )]),
)) ))
@@ -794,10 +794,7 @@ mod tests {
} }
fn configure_engine(&self, engine: &mut Engine<Self::Loader>) -> decodal::Result<()> { fn configure_engine(&self, engine: &mut Engine<Self::Loader>) -> decodal::Result<()> {
engine.bind_global( engine.bind_global("Post", Value::object([("draft", Value::bool_type())]))?;
"Post",
HostValue::object([("draft", HostValue::bool_type())]),
)?;
Ok(()) Ok(())
} }
} }
+1 -1
View File
@@ -6,7 +6,7 @@ use decodal_language_service::{CompletionKind, LanguageService};
use wasm_bindgen::prelude::*; use wasm_bindgen::prelude::*;
#[cfg(any(target_arch = "wasm32", test))] #[cfg(any(target_arch = "wasm32", test))]
mod host_value; mod value;
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
mod web_environment; mod web_environment;
@@ -1,41 +1,41 @@
use decodal::{CompareOp, Constraint, HostValue, LiteralValue, PrimitiveType}; use decodal::{CompareOp, Constraint, LiteralValue, PrimitiveType, Value};
use serde_json::{Map, 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 { match value {
Value::Null => Err(String::from("null is not a Decodal host value")), JsonValue::Null => Err(String::from("null is not a Decodal value")),
Value::Bool(value) => Ok(HostValue::bool(*value)), JsonValue::Bool(value) => Ok(Value::bool(*value)),
Value::Number(value) => number(value), JsonValue::Number(value) => number(value),
Value::String(value) => Ok(HostValue::string(value)), JsonValue::String(value) => Ok(Value::string(value)),
Value::Array(items) => items JsonValue::Array(items) => items
.iter() .iter()
.enumerate() .enumerate()
.map(|(index, value)| { .map(|(index, value)| {
from_json(value).map_err(|error| format!("array item {index}: {error}")) from_json(value).map_err(|error| format!("array item {index}: {error}"))
}) })
.collect::<Result<Vec<_>, _>>() .collect::<Result<Vec<_>, _>>()
.map(HostValue::array), .map(Value::array),
Value::Object(fields) => object(fields), 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() { if let Some(value) = value.as_i64() {
return Ok(HostValue::int(value)); return Ok(Value::int(value));
} }
if let Some(value) = value.as_u64() { if let Some(value) = value.as_u64() {
return i64::try_from(value) return i64::try_from(value)
.map(HostValue::int) .map(Value::int)
.map_err(|_| String::from("integer host value is outside the signed 64-bit range")); .map_err(|_| String::from("integer value is outside the signed 64-bit range"));
} }
value value
.as_f64() .as_f64()
.filter(|value| value.is_finite()) .filter(|value| value.is_finite())
.map(HostValue::float) .map(Value::float)
.ok_or_else(|| String::from("invalid numeric host value")) .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 { let Some(descriptor) = fields.get("$decodal") else {
return fields return fields
.iter() .iter()
@@ -45,7 +45,7 @@ fn object(fields: &Map<String, Value>) -> Result<HostValue, String> {
.map_err(|error| format!("field `{name}`: {error}")) .map_err(|error| format!("field `{name}`: {error}"))
}) })
.collect::<Result<Vec<_>, _>>() .collect::<Result<Vec<_>, _>>()
.map(HostValue::object); .map(Value::object);
}; };
let descriptor = descriptor let descriptor = descriptor
@@ -59,6 +59,14 @@ fn object(fields: &Map<String, Value>) -> Result<HostValue, String> {
.map(Box::new); .map(Box::new);
match descriptor { 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" => { "String" | "Int" | "Float" | "Bool" => {
let primitive = match descriptor { let primitive = match descriptor {
"String" => PrimitiveType::String, "String" => PrimitiveType::String,
@@ -69,7 +77,7 @@ fn object(fields: &Map<String, Value>) -> Result<HostValue, String> {
}; };
let mut all_constraints = vec![Constraint::Type(primitive)]; let mut all_constraints = vec![Constraint::Type(primitive)];
all_constraints.extend(constraints); all_constraints.extend(constraints);
Ok(HostValue::Abstract { Ok(Value::Range {
constraints: all_constraints, constraints: all_constraints,
default, default,
}) })
@@ -78,7 +86,7 @@ fn object(fields: &Map<String, Value>) -> Result<HostValue, String> {
let item = fields let item = fields
.get("item") .get("item")
.ok_or_else(|| String::from("Array descriptor requires `item`"))?; .ok_or_else(|| String::from("Array descriptor requires `item`"))?;
Ok(HostValue::ArrayConstraint { Ok(Value::ArrayRange {
item: Box::new(from_json(item)?), item: Box::new(from_json(item)?),
constraints, constraints,
default, default,
@@ -88,21 +96,49 @@ fn object(fields: &Map<String, Value>) -> Result<HostValue, String> {
let value = fields let value = fields
.get("value") .get("value")
.ok_or_else(|| String::from("Map descriptor requires `value`"))?; .ok_or_else(|| String::from("Map descriptor requires `value`"))?;
Ok(HostValue::MapConstraint { Ok(Value::MapRange {
value: Box::new(from_json(value)?), value: Box::new(from_json(value)?),
constraints, constraints,
default, 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, constraints,
default, 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 { let Some(value) = value else {
return Ok(Vec::new()); return Ok(Vec::new());
}; };
@@ -118,12 +154,13 @@ fn parse_constraints(value: Option<&Value>) -> Result<Vec<Constraint>, String> {
.collect() .collect()
} }
fn parse_constraint(value: &Value) -> Result<Constraint, String> { fn parse_constraint(value: &JsonValue) -> Result<Constraint, String> {
let fields = value let fields = value
.as_object() .as_object()
.ok_or_else(|| String::from("constraint must be an object"))?; .ok_or_else(|| String::from("constraint must be an object"))?;
let kind = required_string(fields, "kind")?; let kind = required_string(fields, "kind")?;
match kind { match kind {
"unknown" => Ok(Constraint::Unknown),
"type" => match required_string(fields, "value")? { "type" => match required_string(fields, "value")? {
"String" => Ok(Constraint::Type(PrimitiveType::String)), "String" => Ok(Constraint::Type(PrimitiveType::String)),
"Int" => Ok(Constraint::Type(PrimitiveType::Int)), "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 { match value {
Value::String(value) => Ok(LiteralValue::String(value.clone())), JsonValue::String(value) => Ok(LiteralValue::String(value.clone())),
Value::Bool(value) => Ok(LiteralValue::Bool(*value)), JsonValue::Bool(value) => Ok(LiteralValue::Bool(*value)),
Value::Number(value) => match number(value)? { JsonValue::Number(value) => match number(value)? {
HostValue::Int(value) => Ok(LiteralValue::Int(value)), Value::Int(value) => Ok(LiteralValue::Int(value)),
HostValue::Float(value) => Ok(LiteralValue::Float(value)), Value::Float(value) => Ok(LiteralValue::Float(value)),
_ => unreachable!(), _ => unreachable!(),
}, },
_ => Err(String::from("comparison value must be a primitive literal")), _ => 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 fields
.get(name) .get(name)
.and_then(Value::as_str) .and_then(JsonValue::as_str)
.ok_or_else(|| format!("constraint requires string `{name}`")) .ok_or_else(|| format!("constraint requires string `{name}`"))
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use decodal::{Constraint, HostValue, LiteralValue, PrimitiveType}; use decodal::{Constraint, LiteralValue, PrimitiveType, Value};
use super::from_json; use super::from_json;
@@ -188,11 +225,11 @@ mod tests {
}); });
assert_eq!( assert_eq!(
from_json(&value).unwrap(), from_json(&value).unwrap(),
HostValue::object([ Value::object([
("body", HostValue::string("# Hello")), ("body", Value::string("# Hello")),
( (
"frontmatter", "frontmatter",
HostValue::object([("draft", HostValue::bool(false))]), Value::object([("draft", Value::bool(false))]),
), ),
]) ])
); );
@@ -210,8 +247,8 @@ mod tests {
}); });
assert_eq!( assert_eq!(
from_json(&value).unwrap(), from_json(&value).unwrap(),
HostValue::ArrayConstraint { Value::ArrayRange {
item: Box::new(HostValue::Abstract { item: Box::new(Value::Range {
constraints: vec![ constraints: vec![
Constraint::Type(PrimitiveType::Int), Constraint::Type(PrimitiveType::Int),
Constraint::Compare(decodal::CompareOp::Gt, LiteralValue::Int(0)), Constraint::Compare(decodal::CompareOp::Gt, LiteralValue::Int(0)),
@@ -219,10 +256,7 @@ mod tests {
default: None, default: None,
}), }),
constraints: Vec::new(), constraints: Vec::new(),
default: Some(Box::new(HostValue::array([ default: Some(Box::new(Value::array([Value::int(1), Value::int(2),]))),
HostValue::int(1),
HostValue::int(2),
]))),
} }
); );
} }
@@ -236,10 +270,41 @@ mod tests {
}); });
assert_eq!( assert_eq!(
from_json(&value).unwrap(), from_json(&value).unwrap(),
HostValue::MapConstraint { Value::MapRange {
value: Box::new(HostValue::bool_type()), value: Box::new(Value::bool_type()),
constraints: Vec::new(), 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))),
} }
); );
} }
+6 -6
View File
@@ -1,17 +1,17 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use decodal::{ use decodal::{
Diagnostic, DiagnosticKind, Engine, HostEnvironment, HostValue, ImportCandidate, ImportLoader, Diagnostic, DiagnosticKind, Engine, HostEnvironment, ImportCandidate, ImportLoader,
LoadedImport, Span, LoadedImport, Span, Value as DecodalValue,
}; };
use js_sys::{Function, Reflect}; use js_sys::{Function, Reflect};
use serde_json::Value; use serde_json::Value;
use wasm_bindgen::{JsCast, JsValue}; use wasm_bindgen::{JsCast, JsValue};
use crate::host_value; use crate::value;
pub(crate) struct JsEnvironment { pub(crate) struct JsEnvironment {
globals: BTreeMap<String, HostValue>, globals: BTreeMap<String, DecodalValue>,
load_import: Option<Function>, load_import: Option<Function>,
complete_import: Option<Function>, complete_import: Option<Function>,
} }
@@ -37,7 +37,7 @@ impl JsEnvironment {
globals globals
.iter() .iter()
.map(|(name, value)| { .map(|(name, value)| {
host_value::from_json(value) value::from_json(value)
.map(|value| (name.clone(), value)) .map(|value| (name.clone(), value))
.map_err(|error| { .map_err(|error| {
JsValue::from_str(&format!("invalid global `{name}`: {error}")) JsValue::from_str(&format!("invalid global `{name}`: {error}"))
@@ -133,7 +133,7 @@ fn loaded_import(value: &Value) -> Result<LoadedImport, String> {
let value = fields let value = fields
.get("value") .get("value")
.ok_or_else(|| String::from("value import requires `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}"))?; .map_err(|error| format!("invalid imported value: {error}"))?;
Ok(LoadedImport::value(key, value)) Ok(LoadedImport::value(key, value))
} }
@@ -75,5 +75,5 @@ fallback は有限で明示的な仕組みに限定する。
- `default`: 未指定値の fallback。 - `default`: 未指定値の fallback。
- `match`: 有限 pattern に基づく分岐。 - `match`: 有限 pattern に基づく分岐。
Decodal は `unknown` / `any` を持たないため、field 不在や未解決 identifier を fallback 可能な通常値として扱わない。 `Unknown` は明示的に書く最上位のabstract rangeであり、評価エラーを包む値ではない。
それらは diagnostic として報告する。 field 不在や未解決 identifier は `Unknown` へ変換せず、diagnostic として報告する。
+63 -34
View File
@@ -17,23 +17,23 @@ let / function env
``` ```
Module top-level bindings shadow prelude bindings. 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 ## Global bindings
The host can bind values before adding or evaluating user sources. The host can bind values before adding or evaluating user sources.
```rust ```rust
use decodal::{EmptyLoader, Engine, HostValue}; use decodal::{EmptyLoader, Engine, Value};
let mut engine = Engine::new(EmptyLoader); let mut engine = Engine::new(EmptyLoader);
engine.bind_global( engine.bind_global(
"Service", "Service",
HostValue::object([ Value::object([
("name", HostValue::string_type()), ("name", Value::string_type()),
("port", HostValue::int_type().gt(443).default_int(8443)?), ("port", Value::int_type().gt(443).default_int(8443)?),
("enabled", HostValue::bool_type().default_bool(true)?), ("enabled", Value::bool_type().default_bool(true)?),
]), ]),
)?; )?;
``` ```
@@ -47,40 +47,49 @@ A user source can then refer to `Service` without importing it.
} as Service } 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. It keeps host code from constructing internal `ThunkId` or `ObjectValue` values directly.
```text ```text
HostValue = Value =
String String
Int Int
Float Float
Bool Bool
Array(Vec<HostValue>) Array(Vec<Value>)
ArrayConstraint { item, constraints, default } ArrayRange { item, constraints, default }
MapConstraint { value, constraints, default } MapRange { value, constraints, default }
Object(Vec<HostField>) Object { fields, rest: Option<Value> }
Abstract { constraints, default } 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. `Value::array_of(item)` builds an array constraint with a required element schema.
There is no host API for an unconstrained abstract array. 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`. `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 `IntoHostValue` using this representation. `BTreeMap<String, T>` and, with `std`, `HashMap<String, T>` implement `DecodalSchema`, `DecodalDecode`, and `IntoValue` using this representation.
## Abstract host objects `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.
A host-provided schema object is represented as a concrete object structure whose fields may contain abstract values.
```rust ```rust
HostValue::object([ Value::object_with_rest(
("name", HostValue::string_type()), [("enabled", Value::bool_type().default_bool(true)?)],
("port", HostValue::int_type().gt(443).default_int(8443)?), 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. 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: 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. - `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 ```rust
use decodal::{Decodal, DecodalDecode, DecodalSchema, EmptyLoader, Engine}; use decodal::{Decodal, DecodalDecode, DecodalSchema, EmptyLoader, Engine};
@@ -202,20 +230,21 @@ run_stdio(|initialize| {
`LspEnvironment` adds document lifecycle hooks on top of `HostEnvironment`. `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 `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. 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 ## Structured imports
`ImportLoader::load` returns either `LoadedImport::Source` or `LoadedImport::Value`. `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 ```text
import "./post.md" import "./post.md"
-> host parses content -> host parses content
-> LoadedImport::Value( -> LoadedImport::Value {
{ frontmatter: {...}, body: "..." } key: "content/post.md",
) value: { frontmatter: {...}, body: "..." }
-> Engine internalizes HostValue }
-> Engine internalizes Value
-> normal composition and materialization -> 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 loader owns path resolution, media or extension dispatch, parsing rules, and parse diagnostics.
The stable loader key is also used to cache structured imports. 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`. 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`.
`HostValue` does not need source spans: syntax diagnostics for the external format remain the loader's responsibility, while cross-value validation reports semantic provenance. `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. `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 先 module は、この段階で全て読み込む必要はない。
import expression が評価されたとき、処理系は `ImportLoader::load` に現在の module key と import specifier を渡す。 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 して登録する。 DCDL source の場合、module registry は key が未登録なら対象 module を parse / desugar して登録する。
登録された source module は module root thunk を持つ。 登録された 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 を使う。 同じ key が複数回 import された場合は、同じ source module または structured root thunk を使う。
つまり source import は module を即時評価しない。 つまり source import は module を即時評価しない。
+2 -2
View File
@@ -10,7 +10,7 @@ Core に入れる機能は、基本的に deterministic な value transformation
- arithmetic / logical / comparison operators - arithmetic / logical / comparison operators
- array concat - array concat
- object / constraint composition - object / constraint composition
- asymmetric range refinement and homogeneous map constraints - asymmetric range refinement, `Unknown`, map constraints, and object rest constraints
- default materialization - default materialization
- pure function evaluation - pure function evaluation
- host supplied import evaluation - host supplied import evaluation
@@ -24,7 +24,7 @@ Core に入れないものは以下である。
- arbitrary host function calls - arbitrary host function calls
- symbolic constraint solving beyond simple normalization - symbolic constraint solving beyond simple normalization
未解決 identifier や missing field は `unknown` として流れず、diagnostic になる。 未解決 identifier や missing field は明示的な `Unknown` range とは異なり、diagnostic になる。
この方針により、存在チェックや optional chaining のような dynamic object inspection は core language の対象外とする。 この方針により、存在チェックや optional chaining のような dynamic object inspection は core language の対象外とする。
## Constraint reasoning ## Constraint reasoning
+11 -3
View File
@@ -14,9 +14,9 @@ RuntimeValue =
`Concrete` は明示的な値である。 `Concrete` は明示的な値である。
`Abstract` は、まだ具体値に確定していない制約付きの値である。 `Abstract` は、まだ具体値に確定していない制約付きの値である。
Decodal は `unknown``any``null` のような「存在するが意味が未確定な値」を runtime value として持たない Decodal は `Unknown` を、すべてのDecodal値を包含する最上位の abstract range として持
識別子や field が解決できない場合は、その場で diagnostic になる。 これは存在する具体値の型情報を消す `Any` ではない。具体値を `Unknown` に対して検証した場合は具体値を保持し、`Unknown` のままmaterializeしようとした場合は diagnostic になる。
未解決値を後続の演算へ流して推論することはしない 識別子や field が解決できない状態とは区別し、それらは従来どおりその場で diagnostic になる
## ConcreteValue ## ConcreteValue
@@ -37,10 +37,15 @@ object は concrete structure として扱う。
```text ```text
ObjectValue: ObjectValue:
fields: Map<Symbol, ObjectField> fields: Map<Symbol, ObjectField>
rest: Option<ObjectRest>
ObjectField: ObjectField:
value: ThunkId value: ThunkId
span: Span span: Span
ObjectRest:
value: ThunkId
span: Span
``` ```
例えば以下の schema object は、object 自体は concrete だが、field の値は abstract value になる。 例えば以下の schema object は、object 自体は concrete だが、field の値は abstract value になる。
@@ -122,6 +127,7 @@ constraint は concrete value とは別の型として扱う。
```text ```text
Constraint = Constraint =
Unknown
Type(PrimitiveType) Type(PrimitiveType)
ArrayItems(ThunkId) ArrayItems(ThunkId)
MapValues(ThunkId) MapValues(ThunkId)
@@ -137,6 +143,8 @@ Constraint =
`MapValues` は object の key 集合を制限せず、すべての field value へ適用する schema thunk を表す。 `MapValues` は object の key 集合を制限せず、すべての field value へ適用する schema thunk を表す。
連想配列は materialize 後も `Data::Object` になり、別の data variant は持たない。 連想配列は materialize 後も `Data::Object` になり、別の data variant は持たない。
`ObjectValue.rest` は名前付きfieldを持つobjectの残余field rangeを表す。rest自体はfieldを生成せず、materializeは実在するfieldだけを出力する。
初期実装では、object の形は主に `Concrete(Object)` の field に `Abstract` を置くことで表現する。 初期実装では、object の形は主に `Concrete(Object)` の field に `Abstract` を置くことで表現する。
object 全体にかかる constraint は必要になった時点で追加する。 object 全体にかかる constraint は必要になった時点で追加する。
@@ -82,12 +82,27 @@ Int & >= 10 & <= 10 # OK
最小の組み込み制約は以下である。 最小の組み込み制約は以下である。
```dcdl ```dcdl
Unknown
String String
Int Int
Float Float
Bool Bool
``` ```
`Unknown` はすべてのDecodal値を含む最上位rangeである。検査を無効化する `Any` ではなく、具体的な値またはより狭いrangeがまだ決まっていないことを表す。
```dcdl
Int & Unknown # Int
42 as Unknown # 42
Unknown as Int # エラー
```
`Unknown` 自体は具体値を持たないためmaterializeできない。defaultを与えるか、具体値で絞り込む必要がある。
```dcdl
Unknown default {} # {}
```
追加の述語制約はライブラリまたは組み込みとして提供できる。 追加の述語制約はライブラリまたは組み込みとして提供できる。
```dcdl ```dcdl
@@ -150,7 +165,16 @@ Services = {...{
``` ```
key は schema で列挙せず、空 object も許容する。 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 ## default
+23 -1
View File
@@ -63,4 +63,26 @@ services = {
右辺にしかない field は default の有無にかかわらず abstract のまま残る。 右辺にしかない field は default の有無にかかわらず abstract のまま残る。
host から渡した object は識別子構文に収まらない文字列 key も保持できるが、DCDL source の object field name は通常の識別子に限られる。 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 は必要になるまで評価されない。
存在しない field への参照は diagnostic になる。 存在しない field への参照は diagnostic になる。
Decodal は unknown / Any のような値を伝播せず、field の有無を曖昧にしない。 明示的な `Unknown` range は存在しないfieldを表さないため、fieldの有無を曖昧にしない。
+3 -1
View File
@@ -71,7 +71,9 @@ literal = string | integer | float | "true" | "false" | regex ;
comparison_operator = "==" | "!=" | "<" | "<=" | ">" | ">=" ; comparison_operator = "==" | "!=" | "<" | "<=" | ">" | ">=" ;
comparison_constraint = ( "<" | "<=" | ">" | ">=" ) , expression ; comparison_constraint = ( "<" | "<=" | ">" | ">=" ) , expression ;
object = "{" , [ field_definition , { ";" , field_definition } , [ ";" ] ] , "}" ; object = "{" , [ field_definition , { ";" , field_definition }
, [ ";" , object_rest ] , [ ";" ] ] , "}" ;
object_rest = "..." , expression ;
map_constraint = "{" , "..." , expression , "}" ; map_constraint = "{" , "..." , expression , "}" ;
field_definition = field_path , "=" , expression ; field_definition = field_path , "=" , expression ;
field_path = identifier , { "." , identifier } ; field_path = identifier , { "." , identifier } ;
@@ -43,11 +43,11 @@ CLI では canonical path を key とする。
### 構造化 import ### 構造化 import
`ImportLoader::load` は DCDL source または host が構築した `HostValue` を import 結果として返す。 `ImportLoader::load` は DCDL source または host が構築した `Value` を import 結果として返す。
Markdown、JSON、TOML などの解釈規則は core に固定せず、loader がファイル種別を判定して構造化する。 Markdown、JSON、TOML などの解釈規則は core に固定せず、loader がファイル種別を判定して構造化する。
```rust ```rust
use decodal::{HostValue, ImportLoader, LoadedImport}; use decodal::{Value, ImportLoader, LoadedImport};
struct ContentLoader; struct ContentLoader;
@@ -62,9 +62,9 @@ impl ImportLoader for ContentLoader {
let parsed = parse_frontmatter(&markdown)?; let parsed = parse_frontmatter(&markdown)?;
return Ok(LoadedImport::value( return Ok(LoadedImport::value(
parsed.key, parsed.key,
HostValue::object([ Value::object([
("frontmatter", parsed.frontmatter), ("frontmatter", parsed.frontmatter),
("body", HostValue::string(parsed.body)), ("body", Value::string(parsed.body)),
]), ]),
)); ));
} }
+1
View File
@@ -13,6 +13,7 @@ tags = [...String];
``` ```
現在の primitive type は `String``Int``Float``Bool` である。 現在の primitive type は `String``Int``Float``Bool` である。
`Unknown` はprimitive typeではなく、すべてのDecodal値を含む最上位の抽象rangeである。具体値またはdefaultがない `Unknown` はmaterializeできない。
配列は primitive type ではなく、必須の要素制約を持つ `[...T]` で表現する。 配列は primitive type ではなく、必須の要素制約を持つ `[...T]` で表現する。
各型の個別仕様へのリンクは [Manual Index](../../index.md) に集約する。 各型の個別仕様へのリンクは [Manual Index](../../index.md) に集約する。
+6 -1
View File
@@ -90,7 +90,12 @@ Literal { String | Integer | Float | True | False | Regex }
CompareOperator { EqualEqual | BangEqual | Lt | Lte | Gt | Gte } CompareOperator { EqualEqual | BangEqual | Lt | Lte | Gt | Gte }
ComparisonConstraint { (Lt | Lte | Gt | Gte) Expression } 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 } MapConstraint { LBrace Ellipsis Expression RBrace }
FieldDefinition { FieldPath Equal Expression } FieldDefinition { FieldPath Equal Expression }
FieldPath { Identifier !fieldPath (Dot Identifier)* } FieldPath { Identifier !fieldPath (Dot Identifier)* }
@@ -210,3 +210,29 @@ Map constraint and ascription
(field_definition (field_definition
path: (field_path (identifier)) path: (field_path (identifier))
value: (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))))))
+11 -1
View File
@@ -99,10 +99,20 @@ module.exports = grammar({
object: $ => seq( 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( map_constraint: $ => seq(
'{', '{',
'...', '...',
@@ -54,7 +54,7 @@
] @punctuation.delimiter ] @punctuation.delimiter
((identifier) @type.builtin ((identifier) @type.builtin
(#match? @type.builtin "^(String|Int|Float|Bool)$")) (#match? @type.builtin "^(String|Int|Float|Bool|Unknown)$"))
(field_definition (field_definition
path: (field_path (identifier) @property)) path: (field_path (identifier) @property))
+38
View File
@@ -302,6 +302,27 @@
] ]
} }
}, },
{
"type": "CHOICE",
"members": [
{
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": ";"
},
{
"type": "SYMBOL",
"name": "object_rest"
}
]
},
{
"type": "BLANK"
}
]
},
{ {
"type": "CHOICE", "type": "CHOICE",
"members": [ "members": [
@@ -327,6 +348,23 @@
} }
] ]
}, },
"object_rest": {
"type": "SEQ",
"members": [
{
"type": "STRING",
"value": "..."
},
{
"type": "FIELD",
"name": "value",
"content": {
"type": "SYMBOL",
"name": "_expression"
}
}
]
},
"map_constraint": { "map_constraint": {
"type": "SEQ", "type": "SEQ",
"members": [ "members": [
+92
View File
@@ -1758,10 +1758,102 @@
{ {
"type": "field_definition", "type": "field_definition",
"named": true "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", "type": "parameter",
"named": true, "named": true,
+7222 -6054
View File
File diff suppressed because it is too large Load Diff
+11
View File
@@ -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. // This file was generated by lezer-generator. You probably shouldn't edit it.
import {LRParser} from "npm:@lezer/lr@^1.4.10" 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({ export const parser = LRParser.deserialize({
version: 14, 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`", 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: "!.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~", 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: "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", 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 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", 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: 91, maxTerm: 92,
skippedNodes: [0,1], skippedNodes: [0,1],
repeatNodeCount: 7, 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], tokenizers: [0, 1],
topRules: {"Source":[0,2]}, topRules: {"Source":[0,2]},
specialized: [{term: 6, get: (value) => spec_Identifier[value] || -1}], 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. // This file was generated by lezer-generator. You probably shouldn't edit it.
import {LRParser} from "@lezer/lr" import {LRParser} from "@lezer/lr"
const spec_Identifier = {__proto__:null,as:182} const spec_Identifier = {__proto__:null,as:184}
export const parser = LRParser.deserialize({ export const parser = LRParser.deserialize({
version: 14, 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`", 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: "!.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~", 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: "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", 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 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", 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: 91, maxTerm: 92,
skippedNodes: [0,1], skippedNodes: [0,1],
repeatNodeCount: 7, 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], tokenizers: [0, 1],
topRules: {"Source":[0,2]}, topRules: {"Source":[0,2]},
specialized: [{term: 6, get: (value) => spec_Identifier[value] || -1}], specialized: [{term: 6, get: (value) => spec_Identifier[value] || -1}],
tokenPrec: 2708 tokenPrec: 2740
}) })
@@ -40,43 +40,44 @@ export const
RBrace = 38, RBrace = 38,
Object = 39, Object = 39,
Semicolon = 40, Semicolon = 40,
ArrayConstraint = 41, ObjectRest = 41,
LBracket = 42, ArrayConstraint = 42,
Comma = 43, LBracket = 43,
RBracket = 44, Comma = 44,
Array = 45, RBracket = 45,
LetExpression = 46, Array = 46,
Let = 47, LetExpression = 47,
FieldDefinitionList = 48, Let = 48,
In = 49, FieldDefinitionList = 49,
FunctionExpression = 50, In = 50,
LParen = 51, FunctionExpression = 51,
ParameterList = 52, LParen = 52,
Parameter = 53, ParameterList = 53,
Colon = 54, Parameter = 54,
RParen = 55, Colon = 55,
Arrow = 56, RParen = 56,
MatchExpression = 57, Arrow = 57,
Match = 58, MatchExpression = 58,
MatchArm = 59, Match = 59,
Pattern = 60, MatchArm = 60,
Underscore = 61, Pattern = 61,
ImportExpression = 62, Underscore = 62,
Import = 63, ImportExpression = 63,
CallSuffix = 64, Import = 64,
ArgumentList = 65, CallSuffix = 65,
Bang = 66, ArgumentList = 66,
Minus = 67, Bang = 67,
Star = 68, Minus = 68,
Slash = 69, Star = 69,
Plus = 70, Slash = 70,
PlusPlus = 71, Plus = 71,
CompareOperator = 72, PlusPlus = 72,
EqualEqual = 73, CompareOperator = 73,
BangEqual = 74, EqualEqual = 74,
AmpAmp = 75, BangEqual = 75,
PipePipe = 76, AmpAmp = 76,
Amp = 77, PipePipe = 77,
SlashSlash = 78, Amp = 78,
Default = 79, SlashSlash = 79,
As = 80 Default = 80,
As = 81
@@ -52,6 +52,16 @@ test('parses map constraints and range refinement', () => {
assert.deepEqual(parseErrors(source), []); 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', () => { test('only treats the exact as keyword as an operator', () => {
assert.deepEqual(parseErrors('asset = 1; value = asset as Int;'), []); assert.deepEqual(parseErrors('asset = 1; value = asset as Int;'), []);
}); });
+5 -1
View File
@@ -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. `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. Methods return JSON strings so callers can handle diagnostics and successful results without depending on Rust data structures.
+30 -15
View File
@@ -1,21 +1,24 @@
/* tslint:disable */ /* tslint:disable */
/* eslint-disable */ /* eslint-disable */
/** A concrete JavaScript value or Decodal schema descriptor supplied by the host. */ /** A concrete JavaScript value or Decodal range descriptor. */
export type DecodalHostValue = export type DecodalValue =
| string | string
| number | number
| boolean | boolean
| DecodalHostValue[] | DecodalValue[]
| { [field: string]: DecodalHostValue } | { [field: string]: DecodalValue }
| DecodalPrimitiveDescriptor | DecodalPrimitiveDescriptor
| DecodalUnknownDescriptor
| DecodalArrayDescriptor | DecodalArrayDescriptor
| DecodalMapDescriptor | DecodalMapDescriptor
| DecodalAbstractDescriptor; | DecodalObjectDescriptor
| DecodalRangeDescriptor;
export type DecodalPrimitiveType = 'String' | 'Int' | 'Float' | 'Bool'; export type DecodalPrimitiveType = 'String' | 'Int' | 'Float' | 'Bool';
export type DecodalConstraint = export type DecodalConstraint =
| { kind: 'unknown' }
| { kind: 'type'; value: DecodalPrimitiveType } | { kind: 'type'; value: DecodalPrimitiveType }
| { kind: 'compare'; op: '>' | '>=' | '<' | '<='; value: string | number | boolean } | { kind: 'compare'; op: '>' | '>=' | '<' | '<='; value: string | number | boolean }
| { kind: 'regex'; value: string } | { kind: 'regex'; value: string }
@@ -24,32 +27,44 @@ export type DecodalConstraint =
export interface DecodalPrimitiveDescriptor { export interface DecodalPrimitiveDescriptor {
$decodal: DecodalPrimitiveType; $decodal: DecodalPrimitiveType;
constraints?: DecodalConstraint[]; constraints?: DecodalConstraint[];
default?: DecodalHostValue; default?: DecodalValue;
}
export interface DecodalUnknownDescriptor {
$decodal: 'Unknown';
constraints?: DecodalConstraint[];
default?: DecodalValue;
} }
export interface DecodalArrayDescriptor { export interface DecodalArrayDescriptor {
$decodal: 'Array'; $decodal: 'Array';
item: DecodalHostValue; item: DecodalValue;
constraints?: DecodalConstraint[]; constraints?: DecodalConstraint[];
default?: DecodalHostValue; default?: DecodalValue;
} }
export interface DecodalMapDescriptor { export interface DecodalMapDescriptor {
$decodal: 'Map'; $decodal: 'Map';
value: DecodalHostValue; value: DecodalValue;
constraints?: DecodalConstraint[]; constraints?: DecodalConstraint[];
default?: DecodalHostValue; default?: DecodalValue;
} }
export interface DecodalAbstractDescriptor { export interface DecodalObjectDescriptor {
$decodal: 'Abstract'; $decodal: 'Object';
fields: Record<string, DecodalValue>;
rest: DecodalValue;
}
export interface DecodalRangeDescriptor {
$decodal: 'Range';
constraints?: DecodalConstraint[]; constraints?: DecodalConstraint[];
default?: DecodalHostValue; default?: DecodalValue;
} }
export type DecodalLoadedImport = export type DecodalLoadedImport =
| { kind: 'source'; key: string; name?: string; source: string } | { kind: 'source'; key: string; name?: string; source: string }
| { kind: 'value'; key: string; value: DecodalHostValue }; | { kind: 'value'; key: string; value: DecodalValue };
export type DecodalImportCandidate = export type DecodalImportCandidate =
| string | string
@@ -57,7 +72,7 @@ export type DecodalImportCandidate =
/** Host-owned environment shared by evaluation and language tooling. */ /** Host-owned environment shared by evaluation and language tooling. */
export interface DecodalEnvironment { export interface DecodalEnvironment {
globals?: Record<string, DecodalHostValue>; globals?: Record<string, DecodalValue>;
/** Synchronous import callback. Preload or cache asynchronous resources first. */ /** Synchronous import callback. Preload or cache asynchronous resources first. */
loadImport?: (currentKey: string | null, specifier: string) => DecodalLoadedImport; loadImport?: (currentKey: string | null, specifier: string) => DecodalLoadedImport;
/** Synchronous import completion callback. */ /** Synchronous import completion callback. */
Binary file not shown.
+5 -3
View File
@@ -11,16 +11,18 @@ export {
} from './decodal_wasm.js'; } from './decodal_wasm.js';
export type { export type {
DecodalAbstractDescriptor,
DecodalArrayDescriptor, DecodalArrayDescriptor,
DecodalMapDescriptor,
DecodalConstraint, DecodalConstraint,
DecodalEnvironment, DecodalEnvironment,
DecodalHostValue,
DecodalImportCandidate, DecodalImportCandidate,
DecodalLoadedImport, DecodalLoadedImport,
DecodalMapDescriptor,
DecodalObjectDescriptor,
DecodalPrimitiveDescriptor, DecodalPrimitiveDescriptor,
DecodalPrimitiveType, DecodalPrimitiveType,
DecodalRangeDescriptor,
DecodalUnknownDescriptor,
DecodalValue,
InitInput, InitInput,
InitOutput, InitOutput,
SyncInitInput, SyncInitInput,
@@ -8,22 +8,25 @@ copyFileSync(resolve(packageDir, '../../LICENSE-APACHE'), resolve(packageDir, 'L
const declarationPath = resolve(packageDir, 'decodal_wasm.d.ts'); const declarationPath = resolve(packageDir, 'decodal_wasm.d.ts');
let declarations = readFileSync(declarationPath, 'utf8'); let declarations = readFileSync(declarationPath, 'utf8');
const hostTypes = ` const valueTypes = `
/** A concrete JavaScript value or Decodal schema descriptor supplied by the host. */ /** A concrete JavaScript value or Decodal range descriptor. */
export type DecodalHostValue = export type DecodalValue =
| string | string
| number | number
| boolean | boolean
| DecodalHostValue[] | DecodalValue[]
| { [field: string]: DecodalHostValue } | { [field: string]: DecodalValue }
| DecodalPrimitiveDescriptor | DecodalPrimitiveDescriptor
| DecodalUnknownDescriptor
| DecodalArrayDescriptor | DecodalArrayDescriptor
| DecodalMapDescriptor | DecodalMapDescriptor
| DecodalAbstractDescriptor; | DecodalObjectDescriptor
| DecodalRangeDescriptor;
export type DecodalPrimitiveType = 'String' | 'Int' | 'Float' | 'Bool'; export type DecodalPrimitiveType = 'String' | 'Int' | 'Float' | 'Bool';
export type DecodalConstraint = export type DecodalConstraint =
| { kind: 'unknown' }
| { kind: 'type'; value: DecodalPrimitiveType } | { kind: 'type'; value: DecodalPrimitiveType }
| { kind: 'compare'; op: '>' | '>=' | '<' | '<='; value: string | number | boolean } | { kind: 'compare'; op: '>' | '>=' | '<' | '<='; value: string | number | boolean }
| { kind: 'regex'; value: string } | { kind: 'regex'; value: string }
@@ -32,32 +35,44 @@ export type DecodalConstraint =
export interface DecodalPrimitiveDescriptor { export interface DecodalPrimitiveDescriptor {
$decodal: DecodalPrimitiveType; $decodal: DecodalPrimitiveType;
constraints?: DecodalConstraint[]; constraints?: DecodalConstraint[];
default?: DecodalHostValue; default?: DecodalValue;
}
export interface DecodalUnknownDescriptor {
$decodal: 'Unknown';
constraints?: DecodalConstraint[];
default?: DecodalValue;
} }
export interface DecodalArrayDescriptor { export interface DecodalArrayDescriptor {
$decodal: 'Array'; $decodal: 'Array';
item: DecodalHostValue; item: DecodalValue;
constraints?: DecodalConstraint[]; constraints?: DecodalConstraint[];
default?: DecodalHostValue; default?: DecodalValue;
} }
export interface DecodalMapDescriptor { export interface DecodalMapDescriptor {
$decodal: 'Map'; $decodal: 'Map';
value: DecodalHostValue; value: DecodalValue;
constraints?: DecodalConstraint[]; constraints?: DecodalConstraint[];
default?: DecodalHostValue; default?: DecodalValue;
} }
export interface DecodalAbstractDescriptor { export interface DecodalObjectDescriptor {
$decodal: 'Abstract'; $decodal: 'Object';
fields: Record<string, DecodalValue>;
rest: DecodalValue;
}
export interface DecodalRangeDescriptor {
$decodal: 'Range';
constraints?: DecodalConstraint[]; constraints?: DecodalConstraint[];
default?: DecodalHostValue; default?: DecodalValue;
} }
export type DecodalLoadedImport = export type DecodalLoadedImport =
| { kind: 'source'; key: string; name?: string; source: string } | { kind: 'source'; key: string; name?: string; source: string }
| { kind: 'value'; key: string; value: DecodalHostValue }; | { kind: 'value'; key: string; value: DecodalValue };
export type DecodalImportCandidate = export type DecodalImportCandidate =
| string | string
@@ -65,7 +80,7 @@ export type DecodalImportCandidate =
/** Host-owned environment shared by evaluation and language tooling. */ /** Host-owned environment shared by evaluation and language tooling. */
export interface DecodalEnvironment { export interface DecodalEnvironment {
globals?: Record<string, DecodalHostValue>; globals?: Record<string, DecodalValue>;
/** Synchronous import callback. Preload or cache asynchronous resources first. */ /** Synchronous import callback. Preload or cache asynchronous resources first. */
loadImport?: (currentKey: string | null, specifier: string) => DecodalLoadedImport; loadImport?: (currentKey: string | null, specifier: string) => DecodalLoadedImport;
/** Synchronous import completion callback. */ /** Synchronous import completion callback. */
@@ -73,7 +88,7 @@ export interface DecodalEnvironment {
} }
`; `;
if (!declarations.includes('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( declarations = declarations.replace(
'constructor(options: any);', '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. \`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. Methods return JSON strings so callers can handle diagnostics and successful results without depending on Rust data structures.
`, `,
+2 -2
View File
@@ -1,5 +1,5 @@
const DECODAL_KEYWORDS = new Set(['let', 'in', 'fn', 'match', 'import', 'default']); 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 DECODAL_LITERALS = new Set(['true', 'false']);
const HTML_ESCAPE = { const HTML_ESCAPE = {
@@ -41,7 +41,7 @@ export function highlightDecodalTokens(source, tokens) {
function tokenClass(kind, text) { function tokenClass(kind, text) {
if (['let', 'in', 'fn', 'match', 'import', 'default'].includes(kind)) return 'tok-keyword'; 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 (kind === 'ident') return '';
if (['true', 'false'].includes(kind)) return 'tok-literal'; if (['true', 'false'].includes(kind)) return 'tok-literal';
if (['int', 'float'].includes(kind)) return 'tok-number'; 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 }, active: { $decodal: 'Bool', default: true },
}, },
}, },
OpenConfig: {
$decodal: 'Object',
fields: {
enabled: { $decodal: 'Bool', default: true },
},
rest: { $decodal: 'Unknown' },
},
}, },
}, },
loadImport(_currentKey, specifier) { loadImport(_currentKey, specifier) {
@@ -46,11 +53,12 @@ test('injects one JavaScript host environment into evaluation and completion', a
const evaluated = JSON.parse(service.evaluate( const evaluated = JSON.parse(service.evaluate(
'main.dcdl', 'main.dcdl',
'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.equal(evaluated.ok, true, evaluated.error);
assert.match(evaluated.output, /"port": 8080/); assert.match(evaluated.output, /"port": 8080/);
assert.match(evaluated.output, /"active": true/); assert.match(evaluated.output, /"active": true/);
assert.match(evaluated.output, /"plugin": \{/);
assert.match(evaluated.output, /"body": "# Hello"/); assert.match(evaluated.output, /"body": "# Hello"/);
const member = JSON.parse(service.complete('main.dcdl', 'App.en', 6, false)); 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.equal(imported.ok, true, imported.error);
assert.ok(imported.completion.options.some((item) => item.label === './schema.dcdl')); 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(); service.free();
}); });