1use std::fmt;
4
5use xlog_core::{symbol, ScalarType};
6
7use crate::Term;
8
9#[derive(Clone, Debug, PartialEq)]
11#[non_exhaustive]
12pub enum GroundTermEncodingError {
13 IntegerOutOfRange {
15 expected: ScalarType,
17 value: i64,
19 },
20 InvalidBooleanInteger {
22 value: i64,
24 },
25 InvalidBooleanSymbol {
27 symbol: String,
29 },
30 Variable {
32 name: String,
34 },
35 Anonymous,
37 Aggregate,
39 TypeMismatch {
41 expected: ScalarType,
43 actual: Term,
45 },
46}
47
48impl fmt::Display for GroundTermEncodingError {
49 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
50 match self {
51 Self::IntegerOutOfRange { expected, value } => {
52 let scalar_name = match expected {
53 ScalarType::U32 => "u32",
54 ScalarType::U64 => "u64",
55 ScalarType::I32 => "i32",
56 ScalarType::I64 => "i64",
57 ScalarType::F32 => "f32",
58 ScalarType::F64 => "f64",
59 ScalarType::Bool => "bool",
60 ScalarType::Symbol => "symbol",
61 };
62 write!(formatter, "{scalar_name} out of range: {value}")
63 }
64 Self::InvalidBooleanInteger { value } => {
65 write!(formatter, "bool expects 0/1, got {value}")
66 }
67 Self::InvalidBooleanSymbol { symbol } => write!(
68 formatter,
69 "Expected boolean symbol 'true' or 'false', got '{symbol}'"
70 ),
71 Self::Variable { name } => write!(formatter, "Fact cannot contain variable {name}"),
72 Self::Anonymous => formatter.write_str("Fact cannot contain anonymous wildcard '_'"),
73 Self::Aggregate => formatter.write_str("Fact cannot contain aggregate"),
74 Self::TypeMismatch { expected, actual } => write!(
75 formatter,
76 "Type mismatch in fact: expected {expected:?}, got {actual:?}"
77 ),
78 }
79 }
80}
81
82impl std::error::Error for GroundTermEncodingError {}
83
84pub fn append_ground_term_bytes(
90 output: &mut Vec<u8>,
91 term: &Term,
92 scalar_type: ScalarType,
93) -> Result<(), GroundTermEncodingError> {
94 match (scalar_type, term) {
95 (ScalarType::U32, Term::Integer(value)) => {
96 let encoded =
97 u32::try_from(*value).map_err(|_| GroundTermEncodingError::IntegerOutOfRange {
98 expected: ScalarType::U32,
99 value: *value,
100 })?;
101 output.extend_from_slice(&encoded.to_le_bytes());
102 }
103 (ScalarType::U64, Term::Integer(value)) => {
104 let encoded =
105 u64::try_from(*value).map_err(|_| GroundTermEncodingError::IntegerOutOfRange {
106 expected: ScalarType::U64,
107 value: *value,
108 })?;
109 output.extend_from_slice(&encoded.to_le_bytes());
110 }
111 (ScalarType::I32, Term::Integer(value)) => {
112 let encoded =
113 i32::try_from(*value).map_err(|_| GroundTermEncodingError::IntegerOutOfRange {
114 expected: ScalarType::I32,
115 value: *value,
116 })?;
117 output.extend_from_slice(&encoded.to_le_bytes());
118 }
119 (ScalarType::I64, Term::Integer(value)) => {
120 output.extend_from_slice(&value.to_le_bytes());
121 }
122 (ScalarType::F32, Term::Float(value)) => {
123 output.extend_from_slice(&(*value as f32).to_le_bytes());
124 }
125 (ScalarType::F64, Term::Float(value)) => {
126 output.extend_from_slice(&value.to_le_bytes());
127 }
128 (ScalarType::F32, Term::Integer(value)) => {
129 output.extend_from_slice(&(*value as f32).to_le_bytes());
130 }
131 (ScalarType::F64, Term::Integer(value)) => {
132 output.extend_from_slice(&(*value as f64).to_le_bytes());
133 }
134 (ScalarType::Bool, Term::Integer(value)) => match *value {
135 0 => output.push(0),
136 1 => output.push(1),
137 value => {
138 return Err(GroundTermEncodingError::InvalidBooleanInteger { value });
139 }
140 },
141 (ScalarType::Bool, Term::Symbol(id)) => {
142 let value = symbol::resolve(*id);
143 match value.as_str() {
144 "false" => output.push(0),
145 "true" => output.push(1),
146 _ => {
147 return Err(GroundTermEncodingError::InvalidBooleanSymbol { symbol: value });
148 }
149 }
150 }
151 (ScalarType::Symbol, Term::String(value)) => {
152 output.extend_from_slice(&symbol::intern(value).to_le_bytes());
153 }
154 (ScalarType::Symbol, Term::Symbol(id)) => {
155 output.extend_from_slice(&id.to_le_bytes());
156 }
157 (_, Term::Variable(name)) => {
158 return Err(GroundTermEncodingError::Variable { name: name.clone() });
159 }
160 (_, Term::Anonymous) => return Err(GroundTermEncodingError::Anonymous),
161 (_, Term::Aggregate(_)) => return Err(GroundTermEncodingError::Aggregate),
162 (expected, actual) => {
163 return Err(GroundTermEncodingError::TypeMismatch {
164 expected,
165 actual: actual.clone(),
166 });
167 }
168 }
169
170 Ok(())
171}
172
173#[cfg(test)]
174mod tests {
175 use super::{append_ground_term_bytes, GroundTermEncodingError};
176 use crate::ast::{AggExpr, AggOp};
177 use crate::Term;
178 use xlog_core::{symbol, ScalarType};
179
180 #[test]
181 fn encodes_supported_scalar_terms() {
182 let true_symbol = symbol::intern("true");
183 let false_symbol = symbol::intern("false");
184 let existing_symbol = symbol::intern("existing");
185 let string_symbol = symbol::intern("from-string");
186 let cases = vec![
187 (
188 "u32 integer",
189 ScalarType::U32,
190 Term::Integer(42),
191 42_u32.to_le_bytes().to_vec(),
192 ),
193 (
194 "u64 integer",
195 ScalarType::U64,
196 Term::Integer(43),
197 43_u64.to_le_bytes().to_vec(),
198 ),
199 (
200 "i32 integer",
201 ScalarType::I32,
202 Term::Integer(-44),
203 (-44_i32).to_le_bytes().to_vec(),
204 ),
205 (
206 "i64 integer",
207 ScalarType::I64,
208 Term::Integer(-45),
209 (-45_i64).to_le_bytes().to_vec(),
210 ),
211 (
212 "f32 float",
213 ScalarType::F32,
214 Term::Float(1.5),
215 1.5_f32.to_le_bytes().to_vec(),
216 ),
217 (
218 "f64 float",
219 ScalarType::F64,
220 Term::Float(2.25),
221 2.25_f64.to_le_bytes().to_vec(),
222 ),
223 (
224 "f32 integer",
225 ScalarType::F32,
226 Term::Integer(46),
227 46_f32.to_le_bytes().to_vec(),
228 ),
229 (
230 "f64 integer",
231 ScalarType::F64,
232 Term::Integer(47),
233 47_f64.to_le_bytes().to_vec(),
234 ),
235 ("false integer", ScalarType::Bool, Term::Integer(0), vec![0]),
236 ("true integer", ScalarType::Bool, Term::Integer(1), vec![1]),
237 (
238 "true symbol",
239 ScalarType::Bool,
240 Term::Symbol(true_symbol),
241 vec![1],
242 ),
243 (
244 "false symbol",
245 ScalarType::Bool,
246 Term::Symbol(false_symbol),
247 vec![0],
248 ),
249 (
250 "string symbol",
251 ScalarType::Symbol,
252 Term::String("from-string".to_string()),
253 string_symbol.to_le_bytes().to_vec(),
254 ),
255 (
256 "interned symbol",
257 ScalarType::Symbol,
258 Term::Symbol(existing_symbol),
259 existing_symbol.to_le_bytes().to_vec(),
260 ),
261 ];
262
263 for (label, scalar_type, term, expected) in cases {
264 let mut actual = vec![0xA5];
265 append_ground_term_bytes(&mut actual, &term, scalar_type)
266 .unwrap_or_else(|error| panic!("{label} should encode successfully, got {error}"));
267 let mut expected_with_prefix = vec![0xA5];
268 expected_with_prefix.extend_from_slice(&expected);
269 assert_eq!(actual, expected_with_prefix, "{label}");
270 }
271 }
272
273 #[test]
274 fn rejects_out_of_range_and_non_ground_terms() {
275 let invalid_bool = symbol::intern("not-a-boolean");
276 let cases = vec![
277 (
278 "negative u32",
279 ScalarType::U32,
280 Term::Integer(-1),
281 GroundTermEncodingError::IntegerOutOfRange {
282 expected: ScalarType::U32,
283 value: -1,
284 },
285 "u32 out of range: -1",
286 ),
287 (
288 "large u32",
289 ScalarType::U32,
290 Term::Integer(i64::from(u32::MAX) + 1),
291 GroundTermEncodingError::IntegerOutOfRange {
292 expected: ScalarType::U32,
293 value: i64::from(u32::MAX) + 1,
294 },
295 "u32 out of range: 4294967296",
296 ),
297 (
298 "negative u64",
299 ScalarType::U64,
300 Term::Integer(-1),
301 GroundTermEncodingError::IntegerOutOfRange {
302 expected: ScalarType::U64,
303 value: -1,
304 },
305 "u64 out of range: -1",
306 ),
307 (
308 "small i32",
309 ScalarType::I32,
310 Term::Integer(i64::from(i32::MIN) - 1),
311 GroundTermEncodingError::IntegerOutOfRange {
312 expected: ScalarType::I32,
313 value: i64::from(i32::MIN) - 1,
314 },
315 "i32 out of range: -2147483649",
316 ),
317 (
318 "large i32",
319 ScalarType::I32,
320 Term::Integer(i64::from(i32::MAX) + 1),
321 GroundTermEncodingError::IntegerOutOfRange {
322 expected: ScalarType::I32,
323 value: i64::from(i32::MAX) + 1,
324 },
325 "i32 out of range: 2147483648",
326 ),
327 (
328 "invalid integer boolean",
329 ScalarType::Bool,
330 Term::Integer(2),
331 GroundTermEncodingError::InvalidBooleanInteger { value: 2 },
332 "bool expects 0/1, got 2",
333 ),
334 (
335 "invalid boolean symbol",
336 ScalarType::Bool,
337 Term::Symbol(invalid_bool),
338 GroundTermEncodingError::InvalidBooleanSymbol {
339 symbol: "not-a-boolean".to_string(),
340 },
341 "Expected boolean symbol 'true' or 'false', got 'not-a-boolean'",
342 ),
343 (
344 "variable",
345 ScalarType::U32,
346 Term::Variable("X".to_string()),
347 GroundTermEncodingError::Variable {
348 name: "X".to_string(),
349 },
350 "Fact cannot contain variable X",
351 ),
352 (
353 "anonymous",
354 ScalarType::U32,
355 Term::Anonymous,
356 GroundTermEncodingError::Anonymous,
357 "Fact cannot contain anonymous wildcard '_'",
358 ),
359 (
360 "aggregate",
361 ScalarType::U64,
362 Term::Aggregate(AggExpr {
363 op: AggOp::Count,
364 variable: "X".to_string(),
365 }),
366 GroundTermEncodingError::Aggregate,
367 "Fact cannot contain aggregate",
368 ),
369 (
370 "float mismatch",
371 ScalarType::U32,
372 Term::Float(1.5),
373 GroundTermEncodingError::TypeMismatch {
374 expected: ScalarType::U32,
375 actual: Term::Float(1.5),
376 },
377 "Type mismatch in fact: expected U32, got Float(1.5)",
378 ),
379 (
380 "string mismatch",
381 ScalarType::U64,
382 Term::String("text".to_string()),
383 GroundTermEncodingError::TypeMismatch {
384 expected: ScalarType::U64,
385 actual: Term::String("text".to_string()),
386 },
387 "Type mismatch in fact: expected U64, got String(\"text\")",
388 ),
389 (
390 "list mismatch",
391 ScalarType::U64,
392 Term::List(vec![]),
393 GroundTermEncodingError::TypeMismatch {
394 expected: ScalarType::U64,
395 actual: Term::List(vec![]),
396 },
397 "Type mismatch in fact: expected U64, got List([])",
398 ),
399 (
400 "cons mismatch",
401 ScalarType::U64,
402 Term::Cons {
403 head: Box::new(Term::Integer(1)),
404 tail: Box::new(Term::List(vec![])),
405 },
406 GroundTermEncodingError::TypeMismatch {
407 expected: ScalarType::U64,
408 actual: Term::Cons {
409 head: Box::new(Term::Integer(1)),
410 tail: Box::new(Term::List(vec![])),
411 },
412 },
413 "Type mismatch in fact: expected U64, got Cons { head: Integer(1), tail: List([]) }",
414 ),
415 (
416 "compound mismatch",
417 ScalarType::U64,
418 Term::Compound {
419 functor: "pair".to_string(),
420 args: vec![Term::Integer(1), Term::Integer(2)],
421 },
422 GroundTermEncodingError::TypeMismatch {
423 expected: ScalarType::U64,
424 actual: Term::Compound {
425 functor: "pair".to_string(),
426 args: vec![Term::Integer(1), Term::Integer(2)],
427 },
428 },
429 "Type mismatch in fact: expected U64, got Compound { functor: \"pair\", args: [Integer(1), Integer(2)] }",
430 ),
431 (
432 "predicate reference mismatch",
433 ScalarType::U64,
434 Term::PredRef("target".to_string()),
435 GroundTermEncodingError::TypeMismatch {
436 expected: ScalarType::U64,
437 actual: Term::PredRef("target".to_string()),
438 },
439 "Type mismatch in fact: expected U64, got PredRef(\"target\")",
440 ),
441 ];
442
443 for (label, scalar_type, term, expected_error, expected_message) in cases {
444 let mut output = vec![0x5A];
445 let error = match append_ground_term_bytes(&mut output, &term, scalar_type) {
446 Ok(()) => panic!("{label} unexpectedly encoded"),
447 Err(error) => error,
448 };
449 assert_eq!(error, expected_error, "{label}");
450 assert_eq!(error.to_string(), expected_message, "{label}");
451 assert_eq!(output, vec![0x5A], "{label} mutated output on failure");
452 }
453 }
454}