Add associative map constraints and range refinement

This commit is contained in:
2026-08-14 06:26:15 +09:00
parent 8198b615a8
commit cc6ab40807
52 changed files with 8951 additions and 6918 deletions
+7
View File
@@ -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,
+46
View File
@@ -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, _))
+32 -1
View File
@@ -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,
File diff suppressed because it is too large Load Diff
+2
View File
@@ -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)),
}
}
+38
View File
@@ -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(_)));
}
}
+1
View File
@@ -69,6 +69,7 @@ pub struct ConstraintEntry {
pub enum Constraint {
Type(PrimitiveType),
ArrayItems(ThunkId),
MapValues(ThunkId),
Compare(CompareOp, LiteralValue),
Regex(String),
BuiltinPredicate(String),
+94 -1
View File
@@ -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));
}
}