use serde::de::{self, MapAccess, Visitor};
use serde::ser::SerializeMap;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::{ExprId, PropId, VarId};
#[derive(Debug, Clone, PartialEq)]
pub enum IrLiteral {
Null,
Bool(bool),
Int(i64),
Float(f64),
Str(String),
Uuid([u8; 16]),
Duration {
months: i64,
days: i64,
seconds: i64,
nanos: i64,
},
DateTime(i64),
Date(i64),
LocalDateTime {
days: i64,
nanos: i64,
},
Time(i64),
ZonedTime {
nanos: i64,
offset: i32,
},
ZonedDateTime {
days: i64,
nanos: i64,
offset: i32,
zone: Option<String>,
},
List(Vec<IrLiteral>),
Map(Vec<(String, IrLiteral)>),
}
impl Serialize for IrLiteral {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
match self {
Self::Null => IrLiteralSer::Null.serialize(s),
Self::Bool(b) => IrLiteralSer::Bool(*b).serialize(s),
Self::Int(i) => IrLiteralSer::Int(*i).serialize(s),
Self::Str(v) => IrLiteralSer::Str(v).serialize(s),
Self::Uuid(v) => IrLiteralSer::Uuid(v).serialize(s),
Self::Duration {
months,
days,
seconds,
nanos,
} => IrLiteralSer::Duration(*months, *days, *seconds, *nanos).serialize(s),
Self::DateTime(dt) => IrLiteralSer::DateTime(*dt).serialize(s),
Self::Date(d) => IrLiteralSer::Date(*d).serialize(s),
Self::LocalDateTime { days, nanos } => {
IrLiteralSer::LocalDateTime(*days, *nanos).serialize(s)
}
Self::Time(n) => IrLiteralSer::Time(*n).serialize(s),
Self::ZonedTime { nanos, offset } => {
IrLiteralSer::ZonedTime(*nanos, *offset).serialize(s)
}
Self::ZonedDateTime {
days,
nanos,
offset,
zone,
} => IrLiteralSer::ZonedDateTime(*days, *nanos, *offset, zone.clone()).serialize(s),
Self::List(items) => IrLiteralSer::List(items).serialize(s),
Self::Map(entries) => IrLiteralSer::Map(entries).serialize(s),
Self::Float(f) => {
if f.is_finite() {
IrLiteralSer::Float(*f).serialize(s)
} else {
let tag = if f.is_nan() {
"NaN"
} else if *f > 0.0 {
"+Infinity"
} else {
"-Infinity"
};
let mut map = s.serialize_map(Some(1))?;
map.serialize_entry("$float", tag)?;
map.end()
}
}
}
}
}
#[derive(Serialize)]
#[serde(tag = "type", content = "value")]
enum IrLiteralSer<'a> {
Null,
Bool(bool),
Int(i64),
Float(f64),
Str(&'a str),
Uuid(&'a [u8; 16]),
Duration(i64, i64, i64, i64),
DateTime(i64),
Date(i64),
LocalDateTime(i64, i64),
Time(i64),
ZonedTime(i64, i32),
ZonedDateTime(i64, i64, i32, Option<String>),
List(&'a [IrLiteral]),
Map(&'a [(String, IrLiteral)]),
}
impl<'de> Deserialize<'de> for IrLiteral {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
d.deserialize_any(IrLiteralVisitor)
}
}
struct IrLiteralVisitor;
impl<'de> Visitor<'de> for IrLiteralVisitor {
type Value = IrLiteral;
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"an IrLiteral (tagged object with \"type\"/\"value\" fields, \
or {{\"$float\": \"NaN\"/\"+Infinity\"/\"-Infinity\"}})"
)
}
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
let first_key: String = map
.next_key()?
.ok_or_else(|| de::Error::custom("expected at least one map key"))?;
if first_key == "$float" {
let tag: String = map.next_value()?;
let f = match tag.as_str() {
"NaN" => f64::NAN,
"+Infinity" => f64::INFINITY,
"-Infinity" => f64::NEG_INFINITY,
other => {
return Err(de::Error::unknown_variant(
other,
&["NaN", "+Infinity", "-Infinity"],
));
}
};
return Ok(IrLiteral::Float(f));
}
if first_key != "type" {
return Err(de::Error::unknown_field(&first_key, &["type", "$float"]));
}
let variant: String = map.next_value()?;
match variant.as_str() {
"Null" => Ok(IrLiteral::Null),
"Bool" => Ok(IrLiteral::Bool(read_value_field(&mut map)?)),
"Int" => Ok(IrLiteral::Int(read_value_field(&mut map)?)),
"Float" => Ok(IrLiteral::Float(read_value_field(&mut map)?)),
"Str" => Ok(IrLiteral::Str(read_value_field(&mut map)?)),
"Uuid" => Ok(IrLiteral::Uuid(read_value_field(&mut map)?)),
"Duration" => {
let (months, days, seconds, nanos) = read_value_field(&mut map)?;
Ok(IrLiteral::Duration {
months,
days,
seconds,
nanos,
})
}
"DateTime" => Ok(IrLiteral::DateTime(read_value_field(&mut map)?)),
"Date" => Ok(IrLiteral::Date(read_value_field(&mut map)?)),
"LocalDateTime" => {
let (days, nanos) = read_value_field(&mut map)?;
Ok(IrLiteral::LocalDateTime { days, nanos })
}
"Time" => Ok(IrLiteral::Time(read_value_field(&mut map)?)),
"ZonedTime" => {
let (nanos, offset) = read_value_field(&mut map)?;
Ok(IrLiteral::ZonedTime { nanos, offset })
}
"ZonedDateTime" => {
let (days, nanos, offset, zone) = read_value_field(&mut map)?;
Ok(IrLiteral::ZonedDateTime {
days,
nanos,
offset,
zone,
})
}
"List" => Ok(IrLiteral::List(read_value_field(&mut map)?)),
"Map" => Ok(IrLiteral::Map(read_value_field(&mut map)?)),
other => Err(de::Error::unknown_variant(
other,
&[
"Null",
"Bool",
"Int",
"Float",
"Str",
"Uuid",
"Duration",
"DateTime",
"Date",
"LocalDateTime",
"Time",
"ZonedTime",
"ZonedDateTime",
"List",
"Map",
],
)),
}
}
}
fn read_value_field<'de, T, A>(map: &mut A) -> Result<T, A::Error>
where
T: Deserialize<'de>,
A: MapAccess<'de>,
{
while let Some(key) = map.next_key::<String>()? {
if key == "value" {
return map.next_value();
}
let _: de::IgnoredAny = map.next_value()?;
}
Err(de::Error::missing_field("value"))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum BinaryOpKind {
Eq,
Neq,
Lt,
Lte,
Gt,
Gte,
And,
Or,
Xor,
Add,
Sub,
Mul,
Div,
Mod,
Pow,
In,
StartsWith,
EndsWith,
Contains,
RegexMatch,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum UnaryOpKind {
Not,
Neg,
IsNull,
IsNotNull,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CaseArm {
pub when: ExprId,
pub then: ExprId,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum IrExpr {
Literal(IrLiteral),
VarRef(VarId),
PropertyAccess {
base: ExprId,
prop: PropId,
},
BinaryOp {
op: BinaryOpKind,
left: ExprId,
right: ExprId,
},
UnaryOp {
op: UnaryOpKind,
expr: ExprId,
},
FunctionCall {
name: String,
args: Vec<ExprId>,
},
Parameter(String),
Case {
operand: Option<ExprId>,
arms: Vec<CaseArm>,
else_expr: Option<ExprId>,
},
ListLiteral(Vec<ExprId>),
MapLiteral(Vec<(String, ExprId)>),
Quantifier {
kind: graphforge_ast::QuantifierKind,
loop_var: VarId,
list: ExprId,
predicate: ExprId,
},
ListComprehension {
loop_var: VarId,
list: ExprId,
filter: Option<ExprId>,
projection: Option<ExprId>,
},
}
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct ExprArena {
nodes: Vec<IrExpr>,
}
impl ExprArena {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, expr: IrExpr) -> ExprId {
let id =
ExprId(u32::try_from(self.nodes.len()).expect("ExprArena exceeded u32::MAX capacity"));
self.nodes.push(expr);
id
}
#[must_use]
pub fn get(&self, id: ExprId) -> &IrExpr {
&self.nodes[id.0 as usize]
}
pub fn substitute_parameters(&mut self, params: &std::collections::HashMap<String, IrLiteral>) {
for node in &mut self.nodes {
if let IrExpr::Parameter(name) = node
&& let Some(value) = params.get(name)
{
*node = IrExpr::Literal(value.clone());
}
}
}
#[must_use]
pub fn len(&self) -> usize {
self.nodes.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.nodes.is_empty()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{PropId, VarId};
#[test]
fn non_trivial_expression_tree_roundtrip() {
let mut arena = ExprArena::new();
let a = arena.push(IrExpr::VarRef(VarId(0)));
let a_name = arena.push(IrExpr::PropertyAccess {
base: a,
prop: PropId(1),
});
let alice = arena.push(IrExpr::Literal(IrLiteral::Str("Alice".into())));
let eq = arena.push(IrExpr::BinaryOp {
op: BinaryOpKind::Eq,
left: a_name,
right: alice,
});
let a_age = arena.push(IrExpr::PropertyAccess {
base: a,
prop: PropId(2),
});
let thirty = arena.push(IrExpr::Literal(IrLiteral::Int(30)));
let gt = arena.push(IrExpr::BinaryOp {
op: BinaryOpKind::Gt,
left: a_age,
right: thirty,
});
let and = arena.push(IrExpr::BinaryOp {
op: BinaryOpKind::And,
left: eq,
right: gt,
});
let json = serde_json::to_string(&arena).unwrap();
let restored: ExprArena = serde_json::from_str(&json).unwrap();
assert_eq!(arena, restored);
assert_eq!(
restored.get(and),
&IrExpr::BinaryOp {
op: BinaryOpKind::And,
left: eq,
right: gt,
}
);
}
#[test]
fn push_returns_sequential_ids() {
let mut arena = ExprArena::new();
let id0 = arena.push(IrExpr::Literal(IrLiteral::Null));
let id1 = arena.push(IrExpr::Literal(IrLiteral::Bool(true)));
let id2 = arena.push(IrExpr::Literal(IrLiteral::Int(42)));
assert_eq!(id0, ExprId(0));
assert_eq!(id1, ExprId(1));
assert_eq!(id2, ExprId(2));
}
#[test]
fn get_retrieves_correct_expression() {
let mut arena = ExprArena::new();
let id = arena.push(IrExpr::Literal(IrLiteral::Str("hello".into())));
assert_eq!(
arena.get(id),
&IrExpr::Literal(IrLiteral::Str("hello".into()))
);
}
#[test]
fn empty_arena_len_and_is_empty() {
let arena = ExprArena::new();
assert_eq!(arena.len(), 0);
assert!(arena.is_empty());
}
#[test]
fn arena_all_ir_expr_variants() {
let mut arena = ExprArena::new();
let lit_null = arena.push(IrExpr::Literal(IrLiteral::Null));
let lit_bool = arena.push(IrExpr::Literal(IrLiteral::Bool(false)));
let lit_int = arena.push(IrExpr::Literal(IrLiteral::Int(-1)));
let lit_float = arena.push(IrExpr::Literal(IrLiteral::Float(2.71)));
let lit_str = arena.push(IrExpr::Literal(IrLiteral::Str("x".into())));
let lit_dur = arena.push(IrExpr::Literal(IrLiteral::Duration {
months: 0,
days: 0,
seconds: 0,
nanos: 1_000_000,
}));
let lit_dt = arena.push(IrExpr::Literal(IrLiteral::DateTime(0)));
let var = arena.push(IrExpr::VarRef(VarId(7)));
let prop = arena.push(IrExpr::PropertyAccess {
base: var,
prop: PropId(3),
});
let binop = arena.push(IrExpr::BinaryOp {
op: BinaryOpKind::Add,
left: lit_int,
right: lit_float,
});
let unop = arena.push(IrExpr::UnaryOp {
op: UnaryOpKind::IsNull,
expr: prop,
});
let call = arena.push(IrExpr::FunctionCall {
name: "toUpper".into(),
args: vec![lit_str],
});
let param = arena.push(IrExpr::Parameter("name".into()));
let arm = CaseArm {
when: lit_bool,
then: lit_int,
};
let case = arena.push(IrExpr::Case {
operand: Some(var),
arms: vec![arm],
else_expr: Some(lit_null),
});
let list = arena.push(IrExpr::ListLiteral(vec![lit_int, lit_float]));
let map = arena.push(IrExpr::MapLiteral(vec![("key".into(), lit_str)]));
let json = serde_json::to_string(&arena).unwrap();
let restored: ExprArena = serde_json::from_str(&json).unwrap();
assert_eq!(arena, restored);
assert_eq!(restored.get(binop), arena.get(binop));
assert_eq!(restored.get(unop), arena.get(unop));
assert_eq!(restored.get(call), arena.get(call));
assert_eq!(restored.get(case), arena.get(case));
assert_eq!(restored.get(list), arena.get(list));
assert_eq!(restored.get(map), arena.get(map));
assert_eq!(restored.get(param), arena.get(param));
assert_eq!(restored.get(lit_dur), arena.get(lit_dur));
assert_eq!(restored.get(lit_dt), arena.get(lit_dt));
}
#[test]
fn case_arm_roundtrip() {
let arm = CaseArm {
when: ExprId(0),
then: ExprId(1),
};
let json = serde_json::to_string(&arm).unwrap();
let back: CaseArm = serde_json::from_str(&json).unwrap();
assert_eq!(arm, back);
}
#[test]
fn unary_is_null_not_top_level_variant() {
let mut arena = ExprArena::new();
let var = arena.push(IrExpr::VarRef(VarId(0)));
let is_null = arena.push(IrExpr::UnaryOp {
op: UnaryOpKind::IsNull,
expr: var,
});
assert!(matches!(
arena.get(is_null),
IrExpr::UnaryOp {
op: UnaryOpKind::IsNull,
..
}
));
}
#[test]
fn binary_op_kind_serde_roundtrip() {
for op in [
BinaryOpKind::Eq,
BinaryOpKind::Neq,
BinaryOpKind::Lt,
BinaryOpKind::Lte,
BinaryOpKind::Gt,
BinaryOpKind::Gte,
BinaryOpKind::And,
BinaryOpKind::Or,
BinaryOpKind::Xor,
BinaryOpKind::Add,
BinaryOpKind::Sub,
BinaryOpKind::Mul,
BinaryOpKind::Div,
BinaryOpKind::Mod,
BinaryOpKind::Pow,
BinaryOpKind::In,
BinaryOpKind::StartsWith,
BinaryOpKind::EndsWith,
BinaryOpKind::Contains,
BinaryOpKind::RegexMatch,
] {
let json = serde_json::to_string(&op).unwrap();
let back: BinaryOpKind = serde_json::from_str(&json).unwrap();
assert_eq!(op, back);
}
}
#[test]
fn unary_op_kind_serde_roundtrip() {
for op in [
UnaryOpKind::Not,
UnaryOpKind::Neg,
UnaryOpKind::IsNull,
UnaryOpKind::IsNotNull,
] {
let json = serde_json::to_string(&op).unwrap();
let back: UnaryOpKind = serde_json::from_str(&json).unwrap();
assert_eq!(op, back);
}
}
#[test]
fn float_finite_roundtrip() {
let lit = IrLiteral::Float(2.71_f64);
let json = serde_json::to_string(&lit).unwrap();
let back: IrLiteral = serde_json::from_str(&json).unwrap();
assert_eq!(lit, back);
}
#[test]
fn uuid_literal_serde_roundtrip() {
let lit = IrLiteral::Uuid([0x5a; 16]);
let json = serde_json::to_string(&lit).unwrap();
assert!(json.contains("Uuid"));
assert_eq!(serde_json::from_str::<IrLiteral>(&json).unwrap(), lit);
}
#[test]
fn localdatetime_literal_serde_roundtrip() {
let lit = IrLiteral::LocalDateTime {
days: 5_393,
nanos: 45_074_645_876_123,
};
let json = serde_json::to_string(&lit).unwrap();
assert!(json.contains("LocalDateTime"), "tagged variant: {json}");
let back: IrLiteral = serde_json::from_str(&json).unwrap();
assert_eq!(lit, back);
}
#[test]
fn temporal_storage_variants_serde_roundtrip() {
for lit in [
IrLiteral::Time(45_074_645_876_123),
IrLiteral::ZonedTime {
nanos: 45_074_645_876_123,
offset: 3_600,
},
IrLiteral::ZonedDateTime {
days: 5_393,
nanos: 45_074_645_876_123,
offset: 3_600,
zone: Some("Europe/Stockholm".to_owned()),
},
IrLiteral::ZonedDateTime {
days: 5_393,
nanos: 0,
offset: -3_600,
zone: None,
},
] {
let json = serde_json::to_string(&lit).unwrap();
let back: IrLiteral = serde_json::from_str(&json).unwrap();
assert_eq!(lit, back, "round-trip {json}");
}
}
#[test]
fn float_nan_roundtrip() {
let lit = IrLiteral::Float(f64::NAN);
let json = serde_json::to_string(&lit).unwrap();
assert!(json.contains("NaN"), "NaN should be tagged: {json}");
let back: IrLiteral = serde_json::from_str(&json).unwrap();
assert!(
matches!(back, IrLiteral::Float(f) if f.is_nan()),
"should deserialise back to NaN"
);
}
#[test]
fn float_positive_infinity_roundtrip() {
let lit = IrLiteral::Float(f64::INFINITY);
let json = serde_json::to_string(&lit).unwrap();
assert!(json.contains("+Infinity"), "should be tagged: {json}");
let back: IrLiteral = serde_json::from_str(&json).unwrap();
assert_eq!(back, IrLiteral::Float(f64::INFINITY));
}
#[test]
fn float_negative_infinity_roundtrip() {
let lit = IrLiteral::Float(f64::NEG_INFINITY);
let json = serde_json::to_string(&lit).unwrap();
assert!(json.contains("-Infinity"), "should be tagged: {json}");
let back: IrLiteral = serde_json::from_str(&json).unwrap();
assert_eq!(back, IrLiteral::Float(f64::NEG_INFINITY));
}
#[test]
fn ir_literal_deser_extra_field_before_value() {
let json = r#"{"type":"Bool","extra":99,"value":true}"#;
let lit: IrLiteral = serde_json::from_str(json).unwrap();
assert_eq!(lit, IrLiteral::Bool(true));
}
#[test]
fn ir_literal_deser_value_before_type_not_supported() {
let json = r#"{"value":42,"type":"Int"}"#;
let result: Result<IrLiteral, _> = serde_json::from_str(json);
let _ = result;
}
}