1use crate::ir::{Constant, Predicate, Rule, Term, TermRef};
10use bytes::Bytes;
11use ipfrs_core::{Block, Cid, Error};
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14
15const DAG_CBOR_CODEC: u64 = 0x71;
17
18#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
25#[serde(tag = "type", rename_all = "snake_case")]
26pub enum TermIpld {
27 Atom { value: String },
29 Variable { name: String },
31 Number { value: f64 },
33 Compound {
35 functor: String,
36 args: Vec<TermIpld>,
37 },
38 List { items: Vec<TermIpld> },
40 Tensor {
43 dtype: String,
44 shape: Vec<u64>,
45 cid: Option<String>,
46 },
47 Ref { cid: String, hint: Option<String> },
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct RuleIpld {
60 pub head: TermIpld,
62 pub body: Vec<TermIpld>,
64 pub metadata: HashMap<String, String>,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct FactIpld {
75 pub predicate: String,
77 pub args: Vec<TermIpld>,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct KnowledgeBaseIpld {
89 pub rules: Vec<String>,
91 pub facts: Vec<FactIpld>,
93 pub version: String,
95}
96
97fn encode_to_block<T: Serialize>(value: &T) -> Result<Block, Error> {
106 let json_bytes = serde_json::to_vec(value)
107 .map_err(|e| Error::Serialization(format!("IPLD codec serialization: {}", e)))?;
108
109 let cid = build_dag_cbor_cid(&json_bytes)?;
112 let block = Block::from_parts(cid, Bytes::from(json_bytes));
113 Ok(block)
114}
115
116fn decode_from_block<T: for<'de> Deserialize<'de>>(block: &Block) -> Result<T, Error> {
118 serde_json::from_slice(block.data())
119 .map_err(|e| Error::Deserialization(format!("IPLD codec deserialization: {}", e)))
120}
121
122fn build_dag_cbor_cid(data: &[u8]) -> Result<Cid, Error> {
124 use ipfrs_core::CidBuilder;
125 CidBuilder::new()
126 .codec(DAG_CBOR_CODEC)
127 .build(data)
128 .map_err(|e| Error::Cid(format!("Failed to compute DAG-CBOR CID: {}", e)))
129}
130
131pub fn rule_to_block(rule: &RuleIpld) -> Result<Block, Error> {
138 encode_to_block(rule)
139}
140
141pub fn block_to_rule(block: &Block) -> Result<RuleIpld, Error> {
143 decode_from_block(block)
144}
145
146pub fn fact_to_block(fact: &FactIpld) -> Result<Block, Error> {
148 encode_to_block(fact)
149}
150
151pub fn block_to_fact(block: &Block) -> Result<FactIpld, Error> {
153 decode_from_block(block)
154}
155
156pub fn kb_to_block(kb: &KnowledgeBaseIpld) -> Result<Block, Error> {
158 encode_to_block(kb)
159}
160
161pub fn block_to_kb(block: &Block) -> Result<KnowledgeBaseIpld, Error> {
163 decode_from_block(block)
164}
165
166pub fn rule_cid(rule: &RuleIpld) -> Result<Cid, Error> {
172 let json_bytes = serde_json::to_vec(rule)
173 .map_err(|e| Error::Serialization(format!("CID computation serialization: {}", e)))?;
174 build_dag_cbor_cid(&json_bytes)
175}
176
177pub fn fact_cid(fact: &FactIpld) -> Result<Cid, Error> {
179 let json_bytes = serde_json::to_vec(fact)
180 .map_err(|e| Error::Serialization(format!("CID computation serialization: {}", e)))?;
181 build_dag_cbor_cid(&json_bytes)
182}
183
184impl TryFrom<&Term> for TermIpld {
187 type Error = Error;
188
189 fn try_from(term: &Term) -> Result<Self, Error> {
190 match term {
191 Term::Var(name) => Ok(TermIpld::Variable { name: name.clone() }),
192
193 Term::Const(Constant::String(s)) => Ok(TermIpld::Atom { value: s.clone() }),
194 Term::Const(Constant::Int(i)) => Ok(TermIpld::Number { value: *i as f64 }),
195 Term::Const(Constant::Bool(b)) => Ok(TermIpld::Number {
196 value: if *b { 1.0 } else { 0.0 },
197 }),
198 Term::Const(Constant::Float(s)) => {
199 let value = s.parse::<f64>().map_err(|_| {
200 Error::InvalidData(format!("Cannot parse float constant: {}", s))
201 })?;
202 Ok(TermIpld::Number { value })
203 }
204
205 Term::Fun(functor, args) => {
206 let ipld_args = args
207 .iter()
208 .map(TermIpld::try_from)
209 .collect::<Result<Vec<_>, _>>()?;
210 Ok(TermIpld::Compound {
211 functor: functor.clone(),
212 args: ipld_args,
213 })
214 }
215
216 Term::Ref(TermRef { cid, hint }) => Ok(TermIpld::Ref {
217 cid: cid.to_string(),
218 hint: hint.clone(),
219 }),
220 }
221 }
222}
223
224impl TryFrom<&TermIpld> for Term {
225 type Error = Error;
226
227 fn try_from(ipld: &TermIpld) -> Result<Self, Error> {
228 match ipld {
229 TermIpld::Atom { value } => Ok(Term::Const(Constant::String(value.clone()))),
230
231 TermIpld::Variable { name } => Ok(Term::Var(name.clone())),
232
233 TermIpld::Number { value } => {
234 if value.fract() == 0.0 && value.abs() < i64::MAX as f64 {
236 Ok(Term::Const(Constant::Int(*value as i64)))
237 } else {
238 Ok(Term::Const(Constant::Float(value.to_string())))
239 }
240 }
241
242 TermIpld::Compound { functor, args } => {
243 let ir_args = args
244 .iter()
245 .map(Term::try_from)
246 .collect::<Result<Vec<_>, _>>()?;
247 Ok(Term::Fun(functor.clone(), ir_args))
248 }
249
250 TermIpld::List { items } => {
251 let ir_items = items
253 .iter()
254 .map(Term::try_from)
255 .collect::<Result<Vec<_>, _>>()?;
256 Ok(Term::Fun("list".to_string(), ir_items))
257 }
258
259 TermIpld::Tensor { dtype, shape, cid } => {
260 let dtype_term = Term::Const(Constant::String(dtype.clone()));
263 let shape_terms: Vec<Term> = shape
264 .iter()
265 .map(|d| Term::Const(Constant::Int(*d as i64)))
266 .collect();
267 let shape_fun = Term::Fun("shape".to_string(), shape_terms);
268 let cid_term = cid
269 .as_deref()
270 .map(|s| Term::Const(Constant::String(s.to_string())))
271 .unwrap_or(Term::Const(Constant::String("none".to_string())));
272 Ok(Term::Fun(
273 "tensor".to_string(),
274 vec![dtype_term, shape_fun, cid_term],
275 ))
276 }
277
278 TermIpld::Ref { cid, hint } => {
279 let parsed_cid: Cid = cid.parse().map_err(|e| {
280 Error::InvalidData(format!("Invalid CID in TermIpld::Ref: {}", e))
281 })?;
282 Ok(Term::Ref(TermRef {
283 cid: parsed_cid,
284 hint: hint.clone(),
285 }))
286 }
287 }
288 }
289}
290
291pub fn predicate_to_term_ipld(pred: &Predicate) -> Result<TermIpld, Error> {
296 let args = pred
297 .args
298 .iter()
299 .map(TermIpld::try_from)
300 .collect::<Result<Vec<_>, _>>()?;
301 Ok(TermIpld::Compound {
302 functor: pred.name.clone(),
303 args,
304 })
305}
306
307pub fn term_ipld_to_predicate(ipld: &TermIpld) -> Result<Predicate, Error> {
309 match ipld {
310 TermIpld::Compound { functor, args } => {
311 let ir_args = args
312 .iter()
313 .map(Term::try_from)
314 .collect::<Result<Vec<_>, _>>()?;
315 Ok(Predicate::new(functor.clone(), ir_args))
316 }
317 other => Err(Error::InvalidData(format!(
318 "Expected Compound TermIpld for Predicate conversion, got: {:?}",
319 other
320 ))),
321 }
322}
323
324pub fn rule_to_rule_ipld(rule: &Rule) -> Result<RuleIpld, Error> {
326 let head = predicate_to_term_ipld(&rule.head)?;
327 let body = rule
328 .body
329 .iter()
330 .map(predicate_to_term_ipld)
331 .collect::<Result<Vec<_>, _>>()?;
332 Ok(RuleIpld {
333 head,
334 body,
335 metadata: HashMap::new(),
336 })
337}
338
339pub fn rule_ipld_to_rule(ipld: &RuleIpld) -> Result<Rule, Error> {
341 let head = term_ipld_to_predicate(&ipld.head)?;
342 let body = ipld
343 .body
344 .iter()
345 .map(term_ipld_to_predicate)
346 .collect::<Result<Vec<_>, _>>()?;
347 Ok(Rule::new(head, body))
348}
349
350pub fn predicate_to_fact_ipld(pred: &Predicate) -> Result<FactIpld, Error> {
352 let args = pred
353 .args
354 .iter()
355 .map(TermIpld::try_from)
356 .collect::<Result<Vec<_>, _>>()?;
357 Ok(FactIpld {
358 predicate: pred.name.clone(),
359 args,
360 })
361}
362
363pub fn fact_ipld_to_predicate(ipld: &FactIpld) -> Result<Predicate, Error> {
365 let args = ipld
366 .args
367 .iter()
368 .map(Term::try_from)
369 .collect::<Result<Vec<_>, _>>()?;
370 Ok(Predicate::new(ipld.predicate.clone(), args))
371}
372
373#[cfg(test)]
376mod tests {
377 use super::*;
378 use crate::ir::Constant;
379
380 #[test]
383 fn test_atom_roundtrip() {
384 let original = TermIpld::Atom {
385 value: "hello".to_string(),
386 };
387 let block = encode_to_block(&original).expect("encode");
388 let decoded: TermIpld = decode_from_block(&block).expect("decode");
389 assert_eq!(original, decoded);
390 }
391
392 #[test]
393 fn test_variable_roundtrip() {
394 let original = TermIpld::Variable {
395 name: "X".to_string(),
396 };
397 let block = encode_to_block(&original).expect("encode");
398 let decoded: TermIpld = decode_from_block(&block).expect("decode");
399 assert_eq!(original, decoded);
400 }
401
402 #[test]
403 fn test_number_roundtrip() {
404 let original = TermIpld::Number { value: 42.0 };
405 let block = encode_to_block(&original).expect("encode");
406 let decoded: TermIpld = decode_from_block(&block).expect("decode");
407 assert_eq!(original, decoded);
408 }
409
410 #[test]
411 fn test_compound_term_roundtrip() {
412 let original = TermIpld::Compound {
413 functor: "parent".to_string(),
414 args: vec![
415 TermIpld::Atom {
416 value: "alice".to_string(),
417 },
418 TermIpld::Atom {
419 value: "bob".to_string(),
420 },
421 ],
422 };
423 let block = encode_to_block(&original).expect("encode");
424 let decoded: TermIpld = decode_from_block(&block).expect("decode");
425 assert_eq!(original, decoded);
426 }
427
428 #[test]
429 fn test_list_roundtrip() {
430 let original = TermIpld::List {
431 items: vec![
432 TermIpld::Number { value: 1.0 },
433 TermIpld::Number { value: 2.0 },
434 TermIpld::Number { value: 3.0 },
435 ],
436 };
437 let block = encode_to_block(&original).expect("encode");
438 let decoded: TermIpld = decode_from_block(&block).expect("decode");
439 assert_eq!(original, decoded);
440 }
441
442 #[test]
443 fn test_tensor_roundtrip() {
444 let original = TermIpld::Tensor {
445 dtype: "float32".to_string(),
446 shape: vec![128, 64],
447 cid: Some("bafybeihdwdcefgh".to_string()),
448 };
449 let block = encode_to_block(&original).expect("encode");
450 let decoded: TermIpld = decode_from_block(&block).expect("decode");
451 assert_eq!(original, decoded);
452 }
453
454 #[test]
457 fn test_rule_to_block_and_back() {
458 let rule = RuleIpld {
459 head: TermIpld::Compound {
460 functor: "grandparent".to_string(),
461 args: vec![
462 TermIpld::Variable {
463 name: "X".to_string(),
464 },
465 TermIpld::Variable {
466 name: "Z".to_string(),
467 },
468 ],
469 },
470 body: vec![
471 TermIpld::Compound {
472 functor: "parent".to_string(),
473 args: vec![
474 TermIpld::Variable {
475 name: "X".to_string(),
476 },
477 TermIpld::Variable {
478 name: "Y".to_string(),
479 },
480 ],
481 },
482 TermIpld::Compound {
483 functor: "parent".to_string(),
484 args: vec![
485 TermIpld::Variable {
486 name: "Y".to_string(),
487 },
488 TermIpld::Variable {
489 name: "Z".to_string(),
490 },
491 ],
492 },
493 ],
494 metadata: HashMap::new(),
495 };
496
497 let block = rule_to_block(&rule).expect("encode rule");
498 let decoded = block_to_rule(&block).expect("decode rule");
499
500 match (&decoded.head, &rule.head) {
502 (
503 TermIpld::Compound {
504 functor: f1,
505 args: a1,
506 },
507 TermIpld::Compound {
508 functor: f2,
509 args: a2,
510 },
511 ) => {
512 assert_eq!(f1, f2);
513 assert_eq!(a1.len(), a2.len());
514 }
515 _ => panic!("Head should be Compound"),
516 }
517 assert_eq!(decoded.body.len(), rule.body.len());
518 }
519
520 #[test]
521 fn test_identical_rules_same_cid() {
522 let make_rule = || RuleIpld {
523 head: TermIpld::Compound {
524 functor: "likes".to_string(),
525 args: vec![
526 TermIpld::Variable {
527 name: "X".to_string(),
528 },
529 TermIpld::Atom {
530 value: "chocolate".to_string(),
531 },
532 ],
533 },
534 body: vec![],
535 metadata: HashMap::new(),
536 };
537
538 let cid1 = rule_cid(&make_rule()).expect("cid1");
539 let cid2 = rule_cid(&make_rule()).expect("cid2");
540 assert_eq!(cid1, cid2, "Identical rules must yield the same CID");
541 }
542
543 #[test]
544 fn test_different_rules_different_cid() {
545 let rule1 = RuleIpld {
546 head: TermIpld::Compound {
547 functor: "a".to_string(),
548 args: vec![],
549 },
550 body: vec![],
551 metadata: HashMap::new(),
552 };
553 let rule2 = RuleIpld {
554 head: TermIpld::Compound {
555 functor: "b".to_string(),
556 args: vec![],
557 },
558 body: vec![],
559 metadata: HashMap::new(),
560 };
561
562 let cid1 = rule_cid(&rule1).expect("cid1");
563 let cid2 = rule_cid(&rule2).expect("cid2");
564 assert_ne!(cid1, cid2, "Different rules must yield different CIDs");
565 }
566
567 #[test]
570 fn test_fact_roundtrip() {
571 let fact = FactIpld {
572 predicate: "parent".to_string(),
573 args: vec![
574 TermIpld::Atom {
575 value: "alice".to_string(),
576 },
577 TermIpld::Atom {
578 value: "bob".to_string(),
579 },
580 ],
581 };
582
583 let block = fact_to_block(&fact).expect("encode fact");
584 let decoded = block_to_fact(&block).expect("decode fact");
585
586 assert_eq!(decoded.predicate, fact.predicate);
587 assert_eq!(decoded.args.len(), fact.args.len());
588 }
589
590 #[test]
593 fn test_knowledge_base_snapshot() {
594 let rule = RuleIpld {
595 head: TermIpld::Compound {
596 functor: "mortal".to_string(),
597 args: vec![TermIpld::Variable {
598 name: "X".to_string(),
599 }],
600 },
601 body: vec![TermIpld::Compound {
602 functor: "human".to_string(),
603 args: vec![TermIpld::Variable {
604 name: "X".to_string(),
605 }],
606 }],
607 metadata: HashMap::new(),
608 };
609
610 let cid = rule_cid(&rule).expect("rule cid");
611
612 let kb = KnowledgeBaseIpld {
613 rules: vec![cid.to_string()],
614 facts: vec![FactIpld {
615 predicate: "human".to_string(),
616 args: vec![TermIpld::Atom {
617 value: "socrates".to_string(),
618 }],
619 }],
620 version: "1.0.0".to_string(),
621 };
622
623 let block = kb_to_block(&kb).expect("encode kb");
624 let decoded = block_to_kb(&block).expect("decode kb");
625
626 assert_eq!(decoded.rules.len(), 1);
627 assert_eq!(decoded.facts.len(), 1);
628 assert_eq!(decoded.version, "1.0.0");
629 assert_eq!(decoded.rules[0], cid.to_string());
630 }
631
632 #[test]
635 fn test_term_ir_to_ipld_atom() {
636 let term = Term::Const(Constant::String("alice".to_string()));
637 let ipld = TermIpld::try_from(&term).expect("convert");
638 assert_eq!(
639 ipld,
640 TermIpld::Atom {
641 value: "alice".to_string()
642 }
643 );
644 }
645
646 #[test]
647 fn test_term_ir_to_ipld_variable() {
648 let term = Term::Var("X".to_string());
649 let ipld = TermIpld::try_from(&term).expect("convert");
650 assert_eq!(
651 ipld,
652 TermIpld::Variable {
653 name: "X".to_string()
654 }
655 );
656 }
657
658 #[test]
659 fn test_term_ir_to_ipld_int() {
660 let term = Term::Const(Constant::Int(42));
661 let ipld = TermIpld::try_from(&term).expect("convert");
662 assert_eq!(ipld, TermIpld::Number { value: 42.0 });
663 }
664
665 #[test]
666 fn test_term_ir_to_ipld_compound() {
667 let term = Term::Fun(
668 "parent".to_string(),
669 vec![
670 Term::Const(Constant::String("alice".to_string())),
671 Term::Var("X".to_string()),
672 ],
673 );
674 let ipld = TermIpld::try_from(&term).expect("convert");
675 match ipld {
676 TermIpld::Compound { functor, args } => {
677 assert_eq!(functor, "parent");
678 assert_eq!(args.len(), 2);
679 }
680 other => panic!("Expected Compound, got {:?}", other),
681 }
682 }
683
684 #[test]
685 fn test_term_ipld_to_ir_roundtrip() {
686 let original = Term::Fun(
687 "grandparent".to_string(),
688 vec![
689 Term::Var("X".to_string()),
690 Term::Const(Constant::String("eve".to_string())),
691 ],
692 );
693 let ipld = TermIpld::try_from(&original).expect("to ipld");
694 let recovered = Term::try_from(&ipld).expect("to ir");
695 assert_eq!(original, recovered);
696 }
697
698 #[test]
699 fn test_predicate_to_term_ipld_roundtrip() {
700 let pred = Predicate::new(
701 "likes".to_string(),
702 vec![
703 Term::Const(Constant::String("alice".to_string())),
704 Term::Const(Constant::String("chocolate".to_string())),
705 ],
706 );
707 let ipld = predicate_to_term_ipld(&pred).expect("to ipld");
708 let recovered = term_ipld_to_predicate(&ipld).expect("to ir");
709 assert_eq!(recovered.name, pred.name);
710 assert_eq!(recovered.args, pred.args);
711 }
712
713 #[test]
714 fn test_rule_ir_to_ipld_roundtrip() {
715 use crate::ir::Rule;
716
717 let head = Predicate::new(
718 "ancestor".to_string(),
719 vec![Term::Var("X".to_string()), Term::Var("Z".to_string())],
720 );
721 let body = vec![
722 Predicate::new(
723 "parent".to_string(),
724 vec![Term::Var("X".to_string()), Term::Var("Y".to_string())],
725 ),
726 Predicate::new(
727 "ancestor".to_string(),
728 vec![Term::Var("Y".to_string()), Term::Var("Z".to_string())],
729 ),
730 ];
731 let rule = Rule::new(head.clone(), body.clone());
732
733 let rule_ipld = rule_to_rule_ipld(&rule).expect("to ipld");
734 let recovered = rule_ipld_to_rule(&rule_ipld).expect("to ir");
735
736 assert_eq!(recovered.head.name, rule.head.name);
737 assert_eq!(recovered.body.len(), rule.body.len());
738 }
739
740 #[test]
741 fn test_dag_cbor_cid_codec() {
742 let rule = RuleIpld {
743 head: TermIpld::Atom {
744 value: "test".to_string(),
745 },
746 body: vec![],
747 metadata: HashMap::new(),
748 };
749 let block = rule_to_block(&rule).expect("block");
750 assert_eq!(block.cid().codec(), DAG_CBOR_CODEC);
752 }
753
754 #[test]
755 fn test_fact_cid_determinism() {
756 let make_fact = || FactIpld {
757 predicate: "human".to_string(),
758 args: vec![TermIpld::Atom {
759 value: "socrates".to_string(),
760 }],
761 };
762 let c1 = fact_cid(&make_fact()).expect("c1");
763 let c2 = fact_cid(&make_fact()).expect("c2");
764 assert_eq!(c1, c2);
765 }
766}