Add typed Decodal derive support

This commit is contained in:
2026-06-26 00:39:33 +09:00
parent 87fef44c68
commit b8404df047
21 changed files with 729 additions and 11 deletions
+4 -1
View File
@@ -70,7 +70,10 @@ pub fn normalize_constraints(
if matches!(
primitive,
Some((PrimitiveType::String | PrimitiveType::Bool, _))
Some((
PrimitiveType::String | PrimitiveType::Bool | PrimitiveType::Array,
_
))
) && (lower.is_some() || upper.is_some())
{
let mut diagnostic = Diagnostic::new(
+51
View File
@@ -63,6 +63,18 @@ impl HostValue {
)
}
pub fn object_from_paths<I, N>(fields: I) -> Self
where
I: IntoIterator<Item = (N, HostValue)>,
N: Into<String>,
{
let mut root = Vec::new();
for (path, value) in fields {
insert_path(&mut root, &path.into(), value);
}
Self::Object(root)
}
pub fn string_type() -> Self {
Self::abstract_with_constraint(Constraint::Type(PrimitiveType::String))
}
@@ -79,6 +91,10 @@ impl HostValue {
Self::abstract_with_constraint(Constraint::Type(PrimitiveType::Bool))
}
pub fn array_type() -> Self {
Self::abstract_with_constraint(Constraint::Type(PrimitiveType::Array))
}
pub fn builtin_predicate(name: impl Into<String>) -> Self {
Self::abstract_with_constraint(Constraint::BuiltinPredicate(name.into()))
}
@@ -162,3 +178,38 @@ impl HostValue {
self.default(Self::bool(value))
}
}
impl HostField {
pub fn new(name: impl Into<String>, value: HostValue) -> Self {
Self {
name: name.into(),
value,
}
}
}
fn insert_path(fields: &mut Vec<HostField>, path: &str, value: HostValue) {
let mut parts = path.splitn(2, '.');
let Some(head) = parts.next().filter(|part| !part.is_empty()) else {
return;
};
if let Some(tail) = parts.next() {
if let Some(field) = fields.iter_mut().find(|field| field.name == head) {
if let HostValue::Object(children) = &mut field.value {
insert_path(children, tail, value);
} else {
let mut children = Vec::new();
insert_path(&mut children, tail, value);
field.value = HostValue::Object(children);
}
} else {
let mut children = Vec::new();
insert_path(&mut children, tail, value);
fields.push(HostField::new(head, HostValue::Object(children)));
}
} else if let Some(field) = fields.iter_mut().find(|field| field.name == head) {
field.value = value;
} else {
fields.push(HostField::new(head, value));
}
}
+4
View File
@@ -1170,6 +1170,7 @@ fn primitive_type(name: &str) -> Option<PrimitiveType> {
"Int" => Some(PrimitiveType::Int),
"Float" => Some(PrimitiveType::Float),
"Bool" => Some(PrimitiveType::Bool),
"Array" => Some(PrimitiveType::Array),
_ => None,
}
}
@@ -1250,6 +1251,9 @@ fn value_matches_primitive(value: &RuntimeValue, primitive: PrimitiveType) -> bo
) | (
RuntimeValue::Concrete(ConcreteValue::Bool(_)),
PrimitiveType::Bool
) | (
RuntimeValue::Concrete(ConcreteValue::Array(_)),
PrimitiveType::Array
)
)
}
+5
View File
@@ -12,6 +12,7 @@ pub mod module;
pub mod parser;
pub mod runtime;
pub mod span;
pub mod typed;
pub use ast::{Ast, BinaryOp, CompareOp, Expr, ExprId, Field, Literal, Param};
pub use constraints::normalize_constraints;
@@ -23,6 +24,10 @@ pub use module::{EmptyLoader, LoadedSource, Module, SourceLoader};
pub use parser::{ParseOutput, Parser, SourceForm, parse_source, parse_source_with_source_id};
pub use runtime::{Constraint, Data, ExprRef, LiteralValue, ModuleId, PrimitiveType, RuntimeValue};
pub use span::{SourceId, Span};
pub use typed::{
DecodalDecode, DecodalSchema, DecodeError, DecodeResult, IntoHostValue, data_at_path,
decode_path, prefix_decode_error,
};
pub fn version() -> &'static str {
env!("CARGO_PKG_VERSION")
+1
View File
@@ -79,6 +79,7 @@ pub enum PrimitiveType {
Int,
Float,
Bool,
Array,
}
#[derive(Debug, Clone, PartialEq)]
+227
View File
@@ -0,0 +1,227 @@
use alloc::{format, string::String, vec::Vec};
use crate::{Data, HostValue};
pub trait DecodalSchema {
fn decodal_schema() -> HostValue;
}
pub trait DecodalDecode: Sized {
fn decodal_decode(data: &Data) -> DecodeResult<Self>;
}
pub trait IntoHostValue {
fn into_host_value(self) -> HostValue;
}
pub type DecodeResult<T> = core::result::Result<T, DecodeError>;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DecodeError {
pub path: String,
pub message: String,
}
impl DecodeError {
pub fn new(path: impl Into<String>, message: impl Into<String>) -> Self {
Self {
path: path.into(),
message: message.into(),
}
}
pub fn at_type(path: &str, expected: &'static str) -> Self {
Self::new(path, format!("expected {expected}"))
}
}
impl core::fmt::Display for DecodeError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
if self.path.is_empty() {
write!(f, "{}", self.message)
} else {
write!(f, "{}: {}", self.path, self.message)
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for DecodeError {}
pub fn decode_path<T: DecodalDecode>(data: &Data, path: &str) -> DecodeResult<T> {
let value = data_at_path(data, path).ok_or_else(|| DecodeError::new(path, "missing field"))?;
T::decodal_decode(value).map_err(|error| prefix_error(path, error))
}
pub fn data_at_path<'a>(data: &'a Data, path: &str) -> Option<&'a Data> {
let mut current = data;
for part in path.split('.') {
if part.is_empty() {
return None;
}
let Data::Object(fields) = current else {
return None;
};
current = &fields.iter().find(|field| field.name == part)?.value;
}
Some(current)
}
pub fn prefix_decode_error(path: &str, error: DecodeError) -> DecodeError {
prefix_error(path, error)
}
fn prefix_error(path: &str, mut error: DecodeError) -> DecodeError {
if error.path.is_empty() {
error.path = String::from(path);
} else if !path.is_empty() {
error.path = format!("{path}.{}", error.path);
}
error
}
impl DecodalSchema for String {
fn decodal_schema() -> HostValue {
HostValue::string_type()
}
}
impl DecodalDecode for String {
fn decodal_decode(data: &Data) -> DecodeResult<Self> {
match data {
Data::String(value) => Ok(value.clone()),
_ => Err(DecodeError::at_type("", "String")),
}
}
}
impl IntoHostValue for String {
fn into_host_value(self) -> HostValue {
HostValue::string(self)
}
}
impl IntoHostValue for &str {
fn into_host_value(self) -> HostValue {
HostValue::string(self)
}
}
macro_rules! impl_int_decode {
($($ty:ty),* $(,)?) => {
$(
impl DecodalSchema for $ty {
fn decodal_schema() -> HostValue {
HostValue::int_type()
}
}
impl DecodalDecode for $ty {
fn decodal_decode(data: &Data) -> DecodeResult<Self> {
match data {
Data::Int(value) => <$ty>::try_from(*value)
.map_err(|_| DecodeError::new("", "integer value is out of range")),
_ => Err(DecodeError::at_type("", "Int")),
}
}
}
impl IntoHostValue for $ty {
fn into_host_value(self) -> HostValue {
HostValue::int(self as i64)
}
}
)*
};
}
impl_int_decode!(i8, i16, i32, i64, u8, u16, u32);
impl DecodalSchema for f64 {
fn decodal_schema() -> HostValue {
HostValue::float_type()
}
}
impl DecodalDecode for f64 {
fn decodal_decode(data: &Data) -> DecodeResult<Self> {
match data {
Data::Float(value) => Ok(*value),
Data::Int(value) => Ok(*value as f64),
_ => Err(DecodeError::at_type("", "Float")),
}
}
}
impl IntoHostValue for f64 {
fn into_host_value(self) -> HostValue {
HostValue::float(self)
}
}
impl DecodalSchema for f32 {
fn decodal_schema() -> HostValue {
HostValue::float_type()
}
}
impl DecodalDecode for f32 {
fn decodal_decode(data: &Data) -> DecodeResult<Self> {
f64::decodal_decode(data).map(|value| value as f32)
}
}
impl IntoHostValue for f32 {
fn into_host_value(self) -> HostValue {
HostValue::float(f64::from(self))
}
}
impl DecodalSchema for bool {
fn decodal_schema() -> HostValue {
HostValue::bool_type()
}
}
impl DecodalDecode for bool {
fn decodal_decode(data: &Data) -> DecodeResult<Self> {
match data {
Data::Bool(value) => Ok(*value),
_ => Err(DecodeError::at_type("", "Bool")),
}
}
}
impl IntoHostValue for bool {
fn into_host_value(self) -> HostValue {
HostValue::bool(self)
}
}
impl<T> DecodalSchema for Vec<T> {
fn decodal_schema() -> HostValue {
HostValue::array_type()
}
}
impl<T: DecodalDecode> DecodalDecode for Vec<T> {
fn decodal_decode(data: &Data) -> DecodeResult<Self> {
match data {
Data::Array(items) => items
.iter()
.enumerate()
.map(|(index, item)| {
T::decodal_decode(item)
.map_err(|error| prefix_error(&format!("[{index}]"), error))
})
.collect(),
_ => Err(DecodeError::at_type("", "Array")),
}
}
}
impl<T: IntoHostValue> IntoHostValue for Vec<T> {
fn into_host_value(self) -> HostValue {
HostValue::array(self.into_iter().map(IntoHostValue::into_host_value))
}
}