Add associative map constraints and range refinement
This commit is contained in:
@@ -53,6 +53,9 @@ pub enum Expr {
|
||||
ArrayConstraint {
|
||||
item: ExprId,
|
||||
},
|
||||
MapConstraint {
|
||||
value: ExprId,
|
||||
},
|
||||
Let {
|
||||
bindings: Vec<Field>,
|
||||
body: ExprId,
|
||||
@@ -94,6 +97,10 @@ pub enum Expr {
|
||||
base: ExprId,
|
||||
fallback: ExprId,
|
||||
},
|
||||
As {
|
||||
narrower: ExprId,
|
||||
wider: ExprId,
|
||||
},
|
||||
CompareConstraint {
|
||||
op: CompareOp,
|
||||
value: ExprId,
|
||||
|
||||
@@ -12,6 +12,7 @@ pub fn normalize_constraints(
|
||||
) -> crate::Result<Vec<ConstraintEntry>> {
|
||||
let mut primitive: Option<(PrimitiveType, Span)> = None;
|
||||
let mut array_span: Option<Span> = None;
|
||||
let mut map_span: Option<Span> = None;
|
||||
let mut lower: Option<(Bound, Span)> = None;
|
||||
let mut upper: Option<(Bound, Span)> = None;
|
||||
let mut rest = Vec::new();
|
||||
@@ -69,6 +70,13 @@ pub fn normalize_constraints(
|
||||
span: entry.span,
|
||||
});
|
||||
}
|
||||
Constraint::MapValues(value) => {
|
||||
map_span.get_or_insert(entry.span);
|
||||
rest.push(ConstraintEntry {
|
||||
constraint: Constraint::MapValues(value),
|
||||
span: entry.span,
|
||||
});
|
||||
}
|
||||
Constraint::BuiltinPredicate(name) => rest.push(ConstraintEntry {
|
||||
constraint: Constraint::BuiltinPredicate(name),
|
||||
span: entry.span,
|
||||
@@ -86,6 +94,26 @@ pub fn normalize_constraints(
|
||||
.with_label(array_span, "array element constraint"));
|
||||
}
|
||||
|
||||
if let (Some((_, primitive_span)), Some(map_span)) = (primitive, map_span) {
|
||||
return Err(Diagnostic::new(
|
||||
DiagnosticKind::Conflict,
|
||||
span,
|
||||
"map value constraints conflict with primitive type constraints",
|
||||
)
|
||||
.with_label(primitive_span, "primitive constraint")
|
||||
.with_label(map_span, "map value constraint"));
|
||||
}
|
||||
|
||||
if let (Some(array_span), Some(map_span)) = (array_span, map_span) {
|
||||
return Err(Diagnostic::new(
|
||||
DiagnosticKind::Conflict,
|
||||
span,
|
||||
"array and map constraints conflict",
|
||||
)
|
||||
.with_label(array_span, "array element constraint")
|
||||
.with_label(map_span, "map value constraint"));
|
||||
}
|
||||
|
||||
if let Some(array_span) = array_span
|
||||
&& (lower.is_some() || upper.is_some())
|
||||
{
|
||||
@@ -104,6 +132,24 @@ pub fn normalize_constraints(
|
||||
return Err(diagnostic);
|
||||
}
|
||||
|
||||
if let Some(map_span) = map_span
|
||||
&& (lower.is_some() || upper.is_some())
|
||||
{
|
||||
let mut diagnostic = Diagnostic::new(
|
||||
DiagnosticKind::Conflict,
|
||||
span,
|
||||
"numeric comparison constraints conflict with map value constraints",
|
||||
)
|
||||
.with_label(map_span, "map value constraint");
|
||||
if let Some((_, lower_span)) = lower {
|
||||
diagnostic = diagnostic.with_label(lower_span, "numeric comparison constraint");
|
||||
}
|
||||
if let Some((_, upper_span)) = upper {
|
||||
diagnostic = diagnostic.with_label(upper_span, "numeric comparison constraint");
|
||||
}
|
||||
return Err(diagnostic);
|
||||
}
|
||||
|
||||
if matches!(
|
||||
primitive,
|
||||
Some((PrimitiveType::String | PrimitiveType::Bool, _))
|
||||
|
||||
@@ -15,6 +15,11 @@ pub enum HostValue {
|
||||
constraints: Vec<Constraint>,
|
||||
default: Option<Box<HostValue>>,
|
||||
},
|
||||
MapConstraint {
|
||||
value: Box<HostValue>,
|
||||
constraints: Vec<Constraint>,
|
||||
default: Option<Box<HostValue>>,
|
||||
},
|
||||
Object(Vec<HostField>),
|
||||
Abstract {
|
||||
constraints: Vec<Constraint>,
|
||||
@@ -104,6 +109,14 @@ impl HostValue {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn map_of(value: HostValue) -> Self {
|
||||
Self::MapConstraint {
|
||||
value: Box::new(value),
|
||||
constraints: Vec::new(),
|
||||
default: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn builtin_predicate(name: impl Into<String>) -> Self {
|
||||
Self::abstract_with_constraint(Constraint::BuiltinPredicate(name.into()))
|
||||
}
|
||||
@@ -118,7 +131,9 @@ impl HostValue {
|
||||
pub fn with_constraint(mut self, constraint: Constraint) -> Self {
|
||||
match &mut self {
|
||||
Self::Abstract { constraints, .. } => constraints.push(constraint),
|
||||
Self::ArrayConstraint { constraints, .. } => constraints.push(constraint),
|
||||
Self::ArrayConstraint { constraints, .. } | Self::MapConstraint { constraints, .. } => {
|
||||
constraints.push(constraint)
|
||||
}
|
||||
_ => {
|
||||
self = Self::Abstract {
|
||||
constraints: alloc::vec![constraint],
|
||||
@@ -169,6 +184,22 @@ impl HostValue {
|
||||
Span::default(),
|
||||
"host value already has a default",
|
||||
)),
|
||||
Self::MapConstraint {
|
||||
value: map_value,
|
||||
constraints,
|
||||
default: None,
|
||||
} => Ok(Self::MapConstraint {
|
||||
value: map_value,
|
||||
constraints,
|
||||
default: Some(Box::new(value)),
|
||||
}),
|
||||
Self::MapConstraint {
|
||||
default: Some(_), ..
|
||||
} => Err(Diagnostic::new(
|
||||
DiagnosticKind::DefaultConflict,
|
||||
Span::default(),
|
||||
"host value already has a default",
|
||||
)),
|
||||
Self::Abstract {
|
||||
constraints,
|
||||
default: None,
|
||||
|
||||
+909
-32
File diff suppressed because it is too large
Load Diff
@@ -22,6 +22,7 @@ pub enum TokenKind {
|
||||
Match,
|
||||
Import,
|
||||
Default,
|
||||
As,
|
||||
Underscore,
|
||||
LBrace,
|
||||
RBrace,
|
||||
@@ -406,6 +407,7 @@ impl<'a> Lexer<'a> {
|
||||
"match" => TokenKind::Match,
|
||||
"import" => TokenKind::Import,
|
||||
"default" => TokenKind::Default,
|
||||
"as" => TokenKind::As,
|
||||
_ => TokenKind::Ident(String::from(text)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,6 +245,13 @@ impl Parser {
|
||||
},
|
||||
span,
|
||||
),
|
||||
InfixKind::As => self.ast.push(
|
||||
Expr::As {
|
||||
narrower: lhs,
|
||||
wider: rhs,
|
||||
},
|
||||
span,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -319,6 +326,16 @@ impl Parser {
|
||||
}
|
||||
|
||||
fn parse_object_after_lbrace(&mut self, start_span: Span) -> Result<ExprId> {
|
||||
if self.consume_kind(&TokenKind::Ellipsis).is_some() {
|
||||
let value = self.parse_expr(0)?;
|
||||
self.expect_kind(
|
||||
&TokenKind::RBrace,
|
||||
"expected '}' after map value constraint",
|
||||
)?;
|
||||
let span = start_span.join(self.previous_span());
|
||||
return Ok(self.ast.push(Expr::MapConstraint { value }, span));
|
||||
}
|
||||
|
||||
let mut fields = Vec::new();
|
||||
if self.consume_kind(&TokenKind::RBrace).is_some() {
|
||||
return Ok(self
|
||||
@@ -544,6 +561,7 @@ impl Parser {
|
||||
|
||||
fn peek_infix(&self) -> Option<(InfixKind, u8, u8)> {
|
||||
match self.peek_kind() {
|
||||
TokenKind::As => Some((InfixKind::As, 0, 1)),
|
||||
TokenKind::Default => Some((InfixKind::Default, 1, 2)),
|
||||
TokenKind::SlashSlash => Some((InfixKind::Patch, 3, 4)),
|
||||
TokenKind::Amp => Some((InfixKind::And, 5, 6)),
|
||||
@@ -652,6 +670,7 @@ impl Parser {
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum InfixKind {
|
||||
As,
|
||||
Add,
|
||||
Sub,
|
||||
Mul,
|
||||
@@ -720,4 +739,23 @@ mod tests {
|
||||
fn rejects_array_constraint_without_element_constraint() {
|
||||
assert!(parse_source("[...]").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_map_constraint() {
|
||||
let parsed = parse_source("{...String}").unwrap();
|
||||
let Expr::MapConstraint { value } = parsed.ast.get(parsed.root).expr else {
|
||||
panic!()
|
||||
};
|
||||
assert!(matches!(parsed.ast.get(value).expr, Expr::Ident(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_as_below_composition() {
|
||||
let parsed = parse_source("value & override as Schema").unwrap();
|
||||
let Expr::As { narrower, wider } = parsed.ast.get(parsed.root).expr else {
|
||||
panic!()
|
||||
};
|
||||
assert!(matches!(parsed.ast.get(narrower).expr, Expr::Binary { .. }));
|
||||
assert!(matches!(parsed.ast.get(wider).expr, Expr::Ident(_)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@ pub struct ConstraintEntry {
|
||||
pub enum Constraint {
|
||||
Type(PrimitiveType),
|
||||
ArrayItems(ThunkId),
|
||||
MapValues(ThunkId),
|
||||
Compare(CompareOp, LiteralValue),
|
||||
Regex(String),
|
||||
BuiltinPredicate(String),
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use alloc::{format, string::String, vec::Vec};
|
||||
use alloc::{collections::BTreeMap, format, string::String, vec::Vec};
|
||||
|
||||
use crate::{Data, HostValue};
|
||||
|
||||
@@ -225,3 +225,96 @@ impl<T: IntoHostValue> IntoHostValue for Vec<T> {
|
||||
HostValue::array(self.into_iter().map(IntoHostValue::into_host_value))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: DecodalSchema> DecodalSchema for BTreeMap<String, T> {
|
||||
fn decodal_schema() -> HostValue {
|
||||
HostValue::map_of(T::decodal_schema())
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: DecodalDecode> DecodalDecode for BTreeMap<String, T> {
|
||||
fn decodal_decode(data: &Data) -> DecodeResult<Self> {
|
||||
match data {
|
||||
Data::Object(fields) => fields
|
||||
.iter()
|
||||
.map(|field| {
|
||||
T::decodal_decode(&field.value)
|
||||
.map(|value| (field.name.clone(), value))
|
||||
.map_err(|error| prefix_error(&field.name, error))
|
||||
})
|
||||
.collect(),
|
||||
_ => Err(DecodeError::at_type("", "Object")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: IntoHostValue> IntoHostValue for BTreeMap<String, T> {
|
||||
fn into_host_value(self) -> HostValue {
|
||||
HostValue::object(
|
||||
self.into_iter()
|
||||
.map(|(name, value)| (name, value.into_host_value())),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl<T: DecodalSchema> DecodalSchema for std::collections::HashMap<String, T> {
|
||||
fn decodal_schema() -> HostValue {
|
||||
HostValue::map_of(T::decodal_schema())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl<T: DecodalDecode> DecodalDecode for std::collections::HashMap<String, T> {
|
||||
fn decodal_decode(data: &Data) -> DecodeResult<Self> {
|
||||
match data {
|
||||
Data::Object(fields) => fields
|
||||
.iter()
|
||||
.map(|field| {
|
||||
T::decodal_decode(&field.value)
|
||||
.map(|value| (field.name.clone(), value))
|
||||
.map_err(|error| prefix_error(&field.name, error))
|
||||
})
|
||||
.collect(),
|
||||
_ => Err(DecodeError::at_type("", "Object")),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl<T: IntoHostValue> IntoHostValue for std::collections::HashMap<String, T> {
|
||||
fn into_host_value(self) -> HostValue {
|
||||
let mut fields: Vec<_> = self
|
||||
.into_iter()
|
||||
.map(|(name, value)| (name, value.into_host_value()))
|
||||
.collect();
|
||||
fields.sort_by(|left, right| left.0.cmp(&right.0));
|
||||
HostValue::object(fields)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::runtime::{Constraint, DataField, PrimitiveType};
|
||||
|
||||
#[test]
|
||||
fn string_maps_expose_map_schemas_and_decode_objects() {
|
||||
assert!(matches!(
|
||||
BTreeMap::<String, i64>::decodal_schema(),
|
||||
HostValue::MapConstraint { value, .. }
|
||||
if matches!(
|
||||
*value,
|
||||
HostValue::Abstract { ref constraints, .. }
|
||||
if constraints == &[Constraint::Type(PrimitiveType::Int)]
|
||||
)
|
||||
));
|
||||
|
||||
let data = Data::Object(alloc::vec![DataField {
|
||||
name: String::from("api"),
|
||||
value: Data::Int(8080),
|
||||
}]);
|
||||
let decoded = BTreeMap::<String, i64>::decodal_decode(&data).unwrap();
|
||||
assert_eq!(decoded.get("api"), Some(&8080));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,6 +222,7 @@ fn builtin_items() -> Vec<CompletionItem> {
|
||||
("match", CompletionKind::Keyword, "pattern matching", 5),
|
||||
("import", CompletionKind::Keyword, "load a module", 5),
|
||||
("default", CompletionKind::Keyword, "fallback value", 0),
|
||||
("as", CompletionKind::Keyword, "refine narrower as wider", 0),
|
||||
("true", CompletionKind::Constant, "Bool", 0),
|
||||
("false", CompletionKind::Constant, "Bool", 0),
|
||||
("String", CompletionKind::Type, "string constraint", 5),
|
||||
@@ -276,6 +277,10 @@ fn fields_from_host_value(value: &HostValue) -> FieldTree {
|
||||
default: Some(value),
|
||||
..
|
||||
}
|
||||
| HostValue::MapConstraint {
|
||||
default: Some(value),
|
||||
..
|
||||
}
|
||||
| HostValue::Abstract {
|
||||
default: Some(value),
|
||||
..
|
||||
|
||||
@@ -169,6 +169,11 @@ impl<'a> Formatter<'a> {
|
||||
self.write_expr(out, *item, indent, 0);
|
||||
out.push(']');
|
||||
}
|
||||
Expr::MapConstraint { value } => {
|
||||
out.push_str("{...");
|
||||
self.write_expr(out, *value, indent, 0);
|
||||
out.push('}');
|
||||
}
|
||||
Expr::Let { bindings, body } => self.write_let(out, node.span, bindings, *body, indent),
|
||||
Expr::Import(_) => {
|
||||
out.push_str("import ");
|
||||
@@ -219,6 +224,11 @@ impl<'a> Formatter<'a> {
|
||||
out.push_str(" default ");
|
||||
self.write_expr(out, *fallback, indent, precedence + 1);
|
||||
}
|
||||
Expr::As { narrower, wider } => {
|
||||
self.write_expr(out, *narrower, indent, precedence);
|
||||
out.push_str(" as ");
|
||||
self.write_expr(out, *wider, indent, precedence + 1);
|
||||
}
|
||||
Expr::CompareConstraint { op, value } => {
|
||||
out.push_str(compare_operator(*op));
|
||||
out.push(' ');
|
||||
@@ -461,6 +471,7 @@ impl<'a> Formatter<'a> {
|
||||
|
||||
fn precedence(&self, id: ExprId) -> u8 {
|
||||
match &self.ast.get(id).expr {
|
||||
Expr::As { .. } => 0,
|
||||
Expr::Default { .. } => 1,
|
||||
Expr::Binary { op, .. } => match op {
|
||||
BinaryOp::Patch => 3,
|
||||
@@ -508,7 +519,11 @@ impl<'a> Formatter<'a> {
|
||||
}
|
||||
Expr::Array(items) => items.iter().all(|item| self.is_inline_expr(*item)),
|
||||
Expr::ArrayConstraint { item } => self.is_inline_expr(*item),
|
||||
Expr::MapConstraint { value } => self.is_inline_expr(*value),
|
||||
Expr::Parenthesized { expr } => self.is_inline_expr(*expr),
|
||||
Expr::As { narrower, wider } => {
|
||||
self.is_inline_expr(*narrower) && self.is_inline_expr(*wider)
|
||||
}
|
||||
Expr::Object(_) | Expr::Let { .. } | Expr::Function { .. } | Expr::Match { .. } => {
|
||||
false
|
||||
}
|
||||
@@ -670,6 +685,18 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formats_map_constraints_and_range_ascription() {
|
||||
let source =
|
||||
"config={service={port=8080;};}as{service={port=Int;};};labels={a=1;b=2;}as{...Int};";
|
||||
let formatted = format_source(source).unwrap();
|
||||
assert_eq!(
|
||||
formatted,
|
||||
"config = {\n service = {\n port = 8080;\n };\n} as {\n service = {\n port = Int;\n };\n};\nlabels = {\n a = 1;\n b = 2;\n} as {...Int};\n"
|
||||
);
|
||||
assert_eq!(format_source(&formatted).unwrap(), formatted);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formats_let_functions_and_precedence_from_the_canonical_ast() {
|
||||
let source = "value=let x=1+2*3;in(a:Int)=>{result=(x+a)*2;};";
|
||||
@@ -743,6 +770,10 @@ mod tests {
|
||||
include_str!("../../../examples/import/schema.dcdl"),
|
||||
),
|
||||
("logical", include_str!("../../../examples/logical.dcdl")),
|
||||
(
|
||||
"map-ascription",
|
||||
include_str!("../../../examples/map-ascription.dcdl"),
|
||||
),
|
||||
(
|
||||
"regex/main",
|
||||
include_str!("../../../examples/regex/main.dcdl"),
|
||||
|
||||
@@ -84,6 +84,16 @@ fn object(fields: &Map<String, Value>) -> Result<HostValue, String> {
|
||||
default,
|
||||
})
|
||||
}
|
||||
"Map" => {
|
||||
let value = fields
|
||||
.get("value")
|
||||
.ok_or_else(|| String::from("Map descriptor requires `value`"))?;
|
||||
Ok(HostValue::MapConstraint {
|
||||
value: Box::new(from_json(value)?),
|
||||
constraints,
|
||||
default,
|
||||
})
|
||||
}
|
||||
"Abstract" => Ok(HostValue::Abstract {
|
||||
constraints,
|
||||
default,
|
||||
@@ -217,6 +227,23 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converts_map_schema_descriptors() {
|
||||
let value = serde_json::json!({
|
||||
"$decodal": "Map",
|
||||
"value": { "$decodal": "Bool" },
|
||||
"default": {},
|
||||
});
|
||||
assert_eq!(
|
||||
from_json(&value).unwrap(),
|
||||
HostValue::MapConstraint {
|
||||
value: Box::new(HostValue::bool_type()),
|
||||
constraints: Vec::new(),
|
||||
default: Some(Box::new(HostValue::object([] as [(&str, HostValue); 0]))),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_null_values() {
|
||||
assert!(
|
||||
|
||||
Reference in New Issue
Block a user