1use rkyv::{Archive, Deserialize as RkyvDeserialize, Serialize as RkyvSerialize};
59use rnix::ast::{self, AstToken, HasEntry};
60use rowan::ast::AstNode;
61use serde::{Deserialize, Serialize};
62use tatara_lisp::DeriveTataraDomain;
63
64use crate::SpecError;
65
66pub type NodeId = u32;
68
69#[derive(
73 DeriveTataraDomain,
74 Serialize,
75 Deserialize,
76 Archive,
77 RkyvSerialize,
78 RkyvDeserialize,
79 Debug,
80 Clone,
81 Copy,
82 PartialEq,
83 Eq,
84 Hash,
85)]
86#[tatara(keyword = "defast-graph-hash")]
87#[rkyv(derive(Debug))]
88pub struct AstGraphHash {
89 pub bytes: [u8; 32],
90}
91
92#[derive(
102 Serialize,
103 Deserialize,
104 Archive,
105 RkyvSerialize,
106 RkyvDeserialize,
107 Debug,
108 Clone,
109 Copy,
110 PartialEq,
111 Eq,
112 Hash,
113)]
114#[rkyv(derive(Debug))]
115pub enum SourceDialect {
116 Nix,
118 Tlisp,
123 Mixed,
129}
130
131#[derive(
133 DeriveTataraDomain,
134 Serialize,
135 Deserialize,
136 Archive,
137 RkyvSerialize,
138 RkyvDeserialize,
139 Debug,
140 Clone,
141 PartialEq,
142)]
143#[tatara(keyword = "defast-graph")]
144#[rkyv(derive(Debug))]
145pub struct AstGraph {
146 pub dialect: SourceDialect,
155 pub grammar_version: u32,
159 pub root_id: NodeId,
163 pub nodes: Vec<AstNodeForm>,
165 pub canonical_hash: AstGraphHash,
168}
169
170#[derive(
172 DeriveTataraDomain,
173 Serialize,
174 Deserialize,
175 Archive,
176 RkyvSerialize,
177 RkyvDeserialize,
178 Debug,
179 Clone,
180 PartialEq,
181)]
182#[tatara(keyword = "defast-node")]
183#[rkyv(derive(Debug))]
184pub struct AstNodeForm {
185 pub id: NodeId,
186 pub kind: AstNodeKind,
187}
188
189#[derive(
193 Serialize,
194 Deserialize,
195 Archive,
196 RkyvSerialize,
197 RkyvDeserialize,
198 Debug,
199 Clone,
200 PartialEq,
201)]
202#[rkyv(derive(Debug))]
203pub enum AstNodeKind {
204 Int(i64),
206 Float(f64),
207 Bool(bool),
208 Null,
209 Str { segments: Vec<StrSegment> },
212 IndentedStr { segments: Vec<StrSegment> },
214 Path(String),
215
216 Ident(String),
218 Select {
220 target: NodeId,
221 path: Vec<String>,
222 fallback: Option<NodeId>,
223 },
224 HasAttr { target: NodeId, path: Vec<String> },
226
227 List(Vec<NodeId>),
229 AttrSet {
230 recursive: bool,
231 entries: Vec<AttrEntry>,
232 inherits: Vec<InheritClause>,
233 },
234
235 LetIn {
237 bindings: Vec<AttrEntry>,
238 inherits: Vec<InheritClause>,
239 body: NodeId,
240 },
241 With {
242 env: NodeId,
243 body: NodeId,
244 },
245 Assert {
246 condition: NodeId,
247 body: NodeId,
248 },
249
250 Lambda {
252 param: LambdaParam,
253 body: NodeId,
254 },
255 Apply {
256 function: NodeId,
257 argument: NodeId,
258 },
259
260 IfThenElse {
262 condition: NodeId,
263 then_branch: NodeId,
264 else_branch: NodeId,
265 },
266
267 BinOp {
269 op: BinaryOp,
270 left: NodeId,
271 right: NodeId,
272 },
273 UnaryOp {
274 op: UnaryOp,
275 operand: NodeId,
276 },
277
278 Unknown {
282 kind: String,
283 source_text: String,
284 },
285}
286
287#[derive(
289 Serialize,
290 Deserialize,
291 Archive,
292 RkyvSerialize,
293 RkyvDeserialize,
294 Debug,
295 Clone,
296 PartialEq,
297)]
298#[rkyv(derive(Debug))]
299pub struct AttrEntry {
300 pub path: Vec<String>,
302 pub value: NodeId,
303}
304
305#[derive(
308 Serialize,
309 Deserialize,
310 Archive,
311 RkyvSerialize,
312 RkyvDeserialize,
313 Debug,
314 Clone,
315 PartialEq,
316 Eq,
317)]
318#[rkyv(derive(Debug))]
319pub struct InheritClause {
320 pub source: Option<NodeId>,
321 pub attrs: Vec<String>,
322}
323
324#[derive(
327 Serialize,
328 Deserialize,
329 Archive,
330 RkyvSerialize,
331 RkyvDeserialize,
332 Debug,
333 Clone,
334 PartialEq,
335)]
336#[rkyv(derive(Debug))]
337pub enum LambdaParam {
338 Ident(String),
340 Pattern {
342 binding_name: Option<String>,
343 formals: Vec<Formal>,
344 accepts_extra: bool,
345 },
346}
347
348#[derive(
350 Serialize,
351 Deserialize,
352 Archive,
353 RkyvSerialize,
354 RkyvDeserialize,
355 Debug,
356 Clone,
357 PartialEq,
358 Eq,
359)]
360#[rkyv(derive(Debug))]
361pub struct Formal {
362 pub name: String,
363 pub default: Option<NodeId>,
364}
365
366#[derive(
368 Serialize,
369 Deserialize,
370 Archive,
371 RkyvSerialize,
372 RkyvDeserialize,
373 Debug,
374 Clone,
375 PartialEq,
376)]
377#[rkyv(derive(Debug))]
378pub enum StrSegment {
379 Literal(String),
380 Interpolation(NodeId),
382}
383
384#[derive(
386 Serialize,
387 Deserialize,
388 Archive,
389 RkyvSerialize,
390 RkyvDeserialize,
391 Debug,
392 Clone,
393 Copy,
394 PartialEq,
395 Eq,
396 Hash,
397)]
398#[rkyv(derive(Debug))]
399pub enum BinaryOp {
400 Add,
402 Sub,
404 Mul,
406 Div,
408 Eq,
410 NotEq,
412 Lt,
414 Le,
416 Gt,
418 Ge,
420 And,
422 Or,
424 Implies,
426 Update,
428 Concat,
430 PipeRight,
432 PipeLeft,
434}
435
436#[derive(
438 Serialize,
439 Deserialize,
440 Archive,
441 RkyvSerialize,
442 RkyvDeserialize,
443 Debug,
444 Clone,
445 Copy,
446 PartialEq,
447 Eq,
448 Hash,
449)]
450#[rkyv(derive(Debug))]
451pub enum UnaryOp {
452 Neg,
454 Not,
456}
457
458#[derive(Debug, thiserror::Error)]
460pub enum AstGraphError {
461 #[error("rnix parse error(s): {0}")]
462 Parse(String),
463 #[error("rnix produced no root expression")]
464 NoRoot,
465 #[error("rkyv archive of canonical AST graph failed: {0}")]
466 Archive(String),
467 #[error("{what} (not yet implemented; tracked in the dialect-rendering ship)")]
468 Unimplemented { what: &'static str },
469}
470
471impl AstGraph {
472 pub fn from_source(source: &str) -> Result<Self, AstGraphError> {
480 let parse = rnix::Root::parse(source);
481 if !parse.errors().is_empty() {
482 let msg = parse
483 .errors()
484 .iter()
485 .map(|e| e.to_string())
486 .collect::<Vec<_>>()
487 .join("; ");
488 return Err(AstGraphError::Parse(msg));
489 }
490 let root = parse.tree();
491 let expr = root.expr().ok_or(AstGraphError::NoRoot)?;
492
493 let mut builder = Builder::default();
494 let root_id = builder.lower_expr(&expr);
495 debug_assert_eq!(root_id as usize + 1, builder.nodes.len());
499
500 Ok(Self {
501 dialect: SourceDialect::Nix,
502 grammar_version: RNIX_GRAMMAR_VERSION,
503 root_id,
504 nodes: builder.nodes,
505 canonical_hash: AstGraphHash { bytes: [0u8; 32] },
506 })
507 }
508
509 pub fn from_tlisp_source(source: &str) -> Result<Self, AstGraphError> {
524 let mut nodes = Vec::with_capacity(1);
525 nodes.push(AstNodeForm {
526 id: 0,
527 kind: AstNodeKind::Unknown {
528 kind: "tlisp-source-pending-lowering".to_string(),
529 source_text: source.to_string(),
530 },
531 });
532 Ok(Self {
533 dialect: SourceDialect::Tlisp,
534 grammar_version: TLISP_GRAMMAR_VERSION,
535 root_id: 0,
536 nodes,
537 canonical_hash: AstGraphHash { bytes: [0u8; 32] },
538 })
539 }
540
541 pub fn to_nix_source(&self) -> Result<String, AstGraphError> {
551 Err(AstGraphError::Unimplemented {
552 what: "AstGraph::to_nix_source — queued",
553 })
554 }
555
556 pub fn to_tlisp_source(&self) -> Result<String, AstGraphError> {
564 Err(AstGraphError::Unimplemented {
565 what: "AstGraph::to_tlisp_source — queued",
566 })
567 }
568
569 pub fn archive_and_hash(mut self) -> Result<(Self, Vec<u8>), AstGraphError> {
577 let initial = rkyv::to_bytes::<rkyv::rancor::Error>(&self)
578 .map_err(|e| AstGraphError::Archive(e.to_string()))?;
579 let hash = blake3::hash(&initial);
580 self.canonical_hash = AstGraphHash { bytes: hash.into() };
581 let stamped = rkyv::to_bytes::<rkyv::rancor::Error>(&self)
582 .map_err(|e| AstGraphError::Archive(e.to_string()))?;
583 Ok((self, stamped.to_vec()))
584 }
585}
586
587pub const RNIX_GRAMMAR_VERSION: u32 = 1;
592
593pub const TLISP_GRAMMAR_VERSION: u32 = 1;
596
597#[derive(Default)]
600struct Builder {
601 nodes: Vec<AstNodeForm>,
602}
603
604impl Builder {
605 fn push(&mut self, kind: AstNodeKind) -> NodeId {
606 let id = self.nodes.len() as NodeId;
607 self.nodes.push(AstNodeForm { id, kind });
608 id
609 }
610
611 fn unknown(&mut self, expr: &ast::Expr, label: &str) -> NodeId {
612 let source_text = expr.syntax().to_string();
613 self.push(AstNodeKind::Unknown {
614 kind: label.to_string(),
615 source_text,
616 })
617 }
618
619 fn lower_expr(&mut self, expr: &ast::Expr) -> NodeId {
620 match expr {
621 ast::Expr::Literal(lit) => self.lower_literal(lit),
622 ast::Expr::Str(s) => self.lower_str(s, false),
623 ast::Expr::Ident(ident) => {
624 let text = ident
625 .ident_token()
626 .map(|t| t.text().to_string())
627 .unwrap_or_default();
628 self.push(AstNodeKind::Ident(text))
629 }
630 ast::Expr::PathAbs(p) => {
636 let text = p.syntax().to_string();
637 self.push(AstNodeKind::Path(text))
638 }
639 ast::Expr::PathRel(p) => {
640 let text = p.syntax().to_string();
641 self.push(AstNodeKind::Path(text))
642 }
643 ast::Expr::PathHome(p) => {
644 let text = p.syntax().to_string();
645 self.push(AstNodeKind::Path(text))
646 }
647 ast::Expr::PathSearch(p) => {
648 let text = p.syntax().to_string();
649 self.push(AstNodeKind::Path(text))
650 }
651 ast::Expr::CurPos(c) => {
653 let text = c.syntax().to_string();
654 self.push(AstNodeKind::Unknown {
655 kind: "cur-pos".to_string(),
656 source_text: text,
657 })
658 }
659 ast::Expr::LegacyLet(l) => {
662 let text = l.syntax().to_string();
663 self.push(AstNodeKind::Unknown {
664 kind: "legacy-let".to_string(),
665 source_text: text,
666 })
667 }
668 ast::Expr::Root(r) => match r.expr() {
672 Some(inner) => self.lower_expr(&inner),
673 None => self.push(AstNodeKind::Null),
674 },
675 ast::Expr::Error(e) => {
680 let text = e.syntax().to_string();
681 self.push(AstNodeKind::Unknown {
682 kind: "parse-error".to_string(),
683 source_text: text,
684 })
685 }
686 ast::Expr::List(list) => {
687 let items: Vec<NodeId> = list.items().map(|e| self.lower_expr(&e)).collect();
688 self.push(AstNodeKind::List(items))
689 }
690 ast::Expr::AttrSet(set) => self.lower_attrset(set),
691 ast::Expr::LetIn(letin) => self.lower_letin(letin),
692 ast::Expr::With(with) => {
693 let env = with.namespace().map(|e| self.lower_expr(&e));
694 let body = with.body().map(|e| self.lower_expr(&e));
695 match (env, body) {
696 (Some(env), Some(body)) => self.push(AstNodeKind::With { env, body }),
697 _ => self.unknown(expr, "with-missing-side"),
698 }
699 }
700 ast::Expr::Assert(assert) => {
701 let condition = assert.condition().map(|e| self.lower_expr(&e));
702 let body = assert.body().map(|e| self.lower_expr(&e));
703 match (condition, body) {
704 (Some(condition), Some(body)) => {
705 self.push(AstNodeKind::Assert { condition, body })
706 }
707 _ => self.unknown(expr, "assert-missing-side"),
708 }
709 }
710 ast::Expr::Lambda(lambda) => self.lower_lambda(lambda),
711 ast::Expr::Apply(apply) => {
712 let function = apply.lambda().map(|e| self.lower_expr(&e));
713 let argument = apply.argument().map(|e| self.lower_expr(&e));
714 match (function, argument) {
715 (Some(function), Some(argument)) => self.push(AstNodeKind::Apply {
716 function,
717 argument,
718 }),
719 _ => self.unknown(expr, "apply-missing-side"),
720 }
721 }
722 ast::Expr::IfElse(ifelse) => {
723 let condition = ifelse.condition().map(|e| self.lower_expr(&e));
724 let then_branch = ifelse.body().map(|e| self.lower_expr(&e));
725 let else_branch = ifelse.else_body().map(|e| self.lower_expr(&e));
726 match (condition, then_branch, else_branch) {
727 (Some(c), Some(t), Some(e)) => self.push(AstNodeKind::IfThenElse {
728 condition: c,
729 then_branch: t,
730 else_branch: e,
731 }),
732 _ => self.unknown(expr, "if-else-missing-branch"),
733 }
734 }
735 ast::Expr::Select(select) => self.lower_select(select),
736 ast::Expr::HasAttr(has) => {
737 let target = has.expr().map(|e| self.lower_expr(&e));
738 let path = has
739 .attrpath()
740 .map(|p| attrpath_to_strings(&p))
741 .unwrap_or_default();
742 match target {
743 Some(target) => self.push(AstNodeKind::HasAttr { target, path }),
744 None => self.unknown(expr, "hasattr-missing-target"),
745 }
746 }
747 ast::Expr::BinOp(binop) => self.lower_binop(binop),
748 ast::Expr::UnaryOp(unaryop) => self.lower_unaryop(unaryop),
749 ast::Expr::Paren(p) => match p.expr() {
750 Some(inner) => self.lower_expr(&inner),
751 None => self.unknown(expr, "paren-empty"),
752 },
753 }
758 }
759
760 fn lower_literal(&mut self, lit: &ast::Literal) -> NodeId {
761 match lit.kind() {
762 ast::LiteralKind::Integer(t) => {
763 let n: i64 = t.value().unwrap_or(0);
764 self.push(AstNodeKind::Int(n))
765 }
766 ast::LiteralKind::Float(t) => {
767 let f: f64 = t.value().unwrap_or(0.0);
768 self.push(AstNodeKind::Float(f))
769 }
770 ast::LiteralKind::Uri(t) => {
771 self.push(AstNodeKind::Str {
773 segments: vec![StrSegment::Literal(t.syntax().text().to_string())],
774 })
775 }
776 }
777 }
778
779 fn lower_str(&mut self, s: &ast::Str, _indented: bool) -> NodeId {
780 let mut segments = Vec::new();
781 for part in s.normalized_parts() {
782 match part {
783 ast::InterpolPart::Literal(text) => {
784 segments.push(StrSegment::Literal(text));
785 }
786 ast::InterpolPart::Interpolation(interp) => {
787 let id = match interp.expr() {
788 Some(expr) => self.lower_expr(&expr),
789 None => {
790 segments.push(StrSegment::Literal(String::new()));
792 continue;
793 }
794 };
795 segments.push(StrSegment::Interpolation(id));
796 }
797 }
798 }
799 self.push(AstNodeKind::Str { segments })
800 }
801
802 fn lower_attrset(&mut self, set: &ast::AttrSet) -> NodeId {
803 let recursive = set.rec_token().is_some();
804 let mut entries = Vec::new();
805 let mut inherits = Vec::new();
806 for entry in set.entries() {
807 match entry {
808 ast::Entry::AttrpathValue(av) => {
809 let path = av
810 .attrpath()
811 .map(|p| attrpath_to_strings(&p))
812 .unwrap_or_default();
813 let value = av
814 .value()
815 .map(|e| self.lower_expr(&e))
816 .unwrap_or_else(|| self.push(AstNodeKind::Null));
817 entries.push(AttrEntry { path, value });
818 }
819 ast::Entry::Inherit(inherit) => {
820 let source = inherit.from().and_then(|f| f.expr()).map(|e| self.lower_expr(&e));
821 let attrs: Vec<String> = inherit
822 .attrs()
823 .filter_map(|a| attr_to_string(&a))
824 .collect();
825 inherits.push(InheritClause { source, attrs });
826 }
827 }
828 }
829 self.push(AstNodeKind::AttrSet {
830 recursive,
831 entries,
832 inherits,
833 })
834 }
835
836 fn lower_letin(&mut self, letin: &ast::LetIn) -> NodeId {
837 let mut bindings = Vec::new();
838 let mut inherits = Vec::new();
839 for entry in letin.entries() {
840 match entry {
841 ast::Entry::AttrpathValue(av) => {
842 let path = av
843 .attrpath()
844 .map(|p| attrpath_to_strings(&p))
845 .unwrap_or_default();
846 let value = av
847 .value()
848 .map(|e| self.lower_expr(&e))
849 .unwrap_or_else(|| self.push(AstNodeKind::Null));
850 bindings.push(AttrEntry { path, value });
851 }
852 ast::Entry::Inherit(inherit) => {
853 let source = inherit.from().and_then(|f| f.expr()).map(|e| self.lower_expr(&e));
854 let attrs: Vec<String> = inherit
855 .attrs()
856 .filter_map(|a| attr_to_string(&a))
857 .collect();
858 inherits.push(InheritClause { source, attrs });
859 }
860 }
861 }
862 let body = letin
863 .body()
864 .map(|e| self.lower_expr(&e))
865 .unwrap_or_else(|| self.push(AstNodeKind::Null));
866 self.push(AstNodeKind::LetIn {
867 bindings,
868 inherits,
869 body,
870 })
871 }
872
873 fn lower_lambda(&mut self, lambda: &ast::Lambda) -> NodeId {
874 let param = match lambda.param() {
875 Some(ast::Param::IdentParam(ip)) => {
876 let name = ip
877 .ident()
878 .and_then(|i| i.ident_token().map(|t| t.text().to_string()))
879 .unwrap_or_default();
880 LambdaParam::Ident(name)
881 }
882 Some(ast::Param::Pattern(pattern)) => {
883 let binding_name = pattern
884 .pat_bind()
885 .and_then(|b| b.ident())
886 .and_then(|i| i.ident_token().map(|t| t.text().to_string()));
887 let accepts_extra = pattern.ellipsis_token().is_some();
888 let mut formals: Vec<Formal> = Vec::new();
889 for entry in pattern.pat_entries() {
890 let name = entry
891 .ident()
892 .and_then(|i| i.ident_token().map(|t| t.text().to_string()))
893 .unwrap_or_default();
894 let default = entry.default().map(|e| self.lower_expr(&e));
895 formals.push(Formal { name, default });
896 }
897 LambdaParam::Pattern {
898 binding_name,
899 formals,
900 accepts_extra,
901 }
902 }
903 None => LambdaParam::Ident(String::new()),
904 };
905 let body = lambda
906 .body()
907 .map(|e| self.lower_expr(&e))
908 .unwrap_or_else(|| self.push(AstNodeKind::Null));
909 self.push(AstNodeKind::Lambda { param, body })
910 }
911
912 fn lower_select(&mut self, select: &ast::Select) -> NodeId {
913 let target = match select.expr() {
914 Some(e) => self.lower_expr(&e),
915 None => return self.push(AstNodeKind::Null),
916 };
917 let path = select
918 .attrpath()
919 .map(|p| attrpath_to_strings(&p))
920 .unwrap_or_default();
921 let fallback = select.default_expr().map(|e| self.lower_expr(&e));
922 self.push(AstNodeKind::Select {
923 target,
924 path,
925 fallback,
926 })
927 }
928
929 fn lower_binop(&mut self, binop: &ast::BinOp) -> NodeId {
930 let left = binop
931 .lhs()
932 .map(|e| self.lower_expr(&e))
933 .unwrap_or_else(|| self.push(AstNodeKind::Null));
934 let right = binop
935 .rhs()
936 .map(|e| self.lower_expr(&e))
937 .unwrap_or_else(|| self.push(AstNodeKind::Null));
938 let op = binop
939 .operator()
940 .map(map_binop)
941 .unwrap_or(BinaryOp::Concat);
942 self.push(AstNodeKind::BinOp { op, left, right })
943 }
944
945 fn lower_unaryop(&mut self, unaryop: &ast::UnaryOp) -> NodeId {
946 let operand = unaryop
947 .expr()
948 .map(|e| self.lower_expr(&e))
949 .unwrap_or_else(|| self.push(AstNodeKind::Null));
950 let op = match unaryop.operator() {
951 Some(ast::UnaryOpKind::Negate) => UnaryOp::Neg,
952 Some(ast::UnaryOpKind::Invert) => UnaryOp::Not,
953 None => UnaryOp::Neg,
954 };
955 self.push(AstNodeKind::UnaryOp { op, operand })
956 }
957}
958
959fn map_binop(op: ast::BinOpKind) -> BinaryOp {
960 match op {
961 ast::BinOpKind::Add => BinaryOp::Add,
962 ast::BinOpKind::Sub => BinaryOp::Sub,
963 ast::BinOpKind::Mul => BinaryOp::Mul,
964 ast::BinOpKind::Div => BinaryOp::Div,
965 ast::BinOpKind::Equal => BinaryOp::Eq,
966 ast::BinOpKind::NotEqual => BinaryOp::NotEq,
967 ast::BinOpKind::Less => BinaryOp::Lt,
968 ast::BinOpKind::LessOrEq => BinaryOp::Le,
969 ast::BinOpKind::More => BinaryOp::Gt,
970 ast::BinOpKind::MoreOrEq => BinaryOp::Ge,
971 ast::BinOpKind::And => BinaryOp::And,
972 ast::BinOpKind::Or => BinaryOp::Or,
973 ast::BinOpKind::Implication => BinaryOp::Implies,
974 ast::BinOpKind::Update => BinaryOp::Update,
975 ast::BinOpKind::Concat => BinaryOp::Concat,
976 ast::BinOpKind::PipeRight => BinaryOp::PipeRight,
977 ast::BinOpKind::PipeLeft => BinaryOp::PipeLeft,
978 }
979}
980
981fn attrpath_to_strings(path: &ast::Attrpath) -> Vec<String> {
982 path.attrs().filter_map(|a| attr_to_string(&a)).collect()
983}
984
985fn attr_to_string(attr: &ast::Attr) -> Option<String> {
986 match attr {
987 ast::Attr::Ident(i) => i.ident_token().map(|t| t.text().to_string()),
988 ast::Attr::Dynamic(d) => Some(d.syntax().to_string()),
989 ast::Attr::Str(s) => Some(s.syntax().to_string()),
990 }
991}
992
993#[derive(DeriveTataraDomain, Serialize, Deserialize, Debug, Clone)]
996#[tatara(keyword = "defast-graph-fixture")]
997pub struct AstGraphFixture {
998 pub name: String,
999 pub source: String,
1000 #[serde(rename = "rootKind")]
1001 pub root_kind: String,
1002 #[serde(rename = "nodeCount")]
1003 pub node_count: u32,
1004 pub notes: String,
1005}
1006
1007pub const CANONICAL_AST_GRAPH_FIXTURES_LISP: &str =
1008 include_str!("../specs/ast_graph.lisp");
1009
1010pub fn load_fixtures() -> Result<Vec<AstGraphFixture>, SpecError> {
1016 crate::loader::load_all::<AstGraphFixture>(CANONICAL_AST_GRAPH_FIXTURES_LISP)
1017}
1018
1019#[cfg(test)]
1020mod tests {
1021 use super::*;
1022 use pretty_assertions::assert_eq;
1023
1024 #[test]
1025 fn integer_literal_produces_one_node() {
1026 let g = AstGraph::from_source("42").unwrap();
1027 assert_eq!(g.nodes.len(), 1);
1028 assert_eq!(g.root_id, 0);
1029 matches!(g.nodes[0].kind, AstNodeKind::Int(42));
1030 }
1031
1032 #[test]
1033 fn binop_produces_three_nodes() {
1034 let g = AstGraph::from_source("1 + 2").unwrap();
1036 assert!(g.nodes.len() >= 3);
1037 assert!(matches!(
1038 g.nodes[g.root_id as usize].kind,
1039 AstNodeKind::BinOp { op: BinaryOp::Add, .. }
1040 ));
1041 }
1042
1043 #[test]
1044 fn let_in_with_binop_resolves_identifiers() {
1045 let g = AstGraph::from_source("let x = 1; in x + 2").unwrap();
1046 assert!(matches!(
1047 g.nodes[g.root_id as usize].kind,
1048 AstNodeKind::LetIn { .. }
1049 ));
1050 assert!(g.nodes.len() >= 5);
1053 }
1054
1055 #[test]
1056 fn nixos_module_lambda_destructures_formals() {
1057 let g = AstGraph::from_source(
1058 "{ config, lib, pkgs, ... }: { networking.hostName = \"rio\"; }",
1059 )
1060 .unwrap();
1061 let root_kind = &g.nodes[g.root_id as usize].kind;
1063 match root_kind {
1064 AstNodeKind::Lambda {
1065 param: LambdaParam::Pattern { formals, accepts_extra, .. },
1066 ..
1067 } => {
1068 assert!(*accepts_extra);
1069 let names: Vec<&str> = formals.iter().map(|f| f.name.as_str()).collect();
1070 assert!(names.contains(&"config"));
1071 assert!(names.contains(&"lib"));
1072 assert!(names.contains(&"pkgs"));
1073 }
1074 other => panic!("expected Lambda Pattern at root, got {other:?}"),
1075 }
1076 }
1077
1078 #[test]
1079 fn archive_and_hash_stamps_canonical_hash() {
1080 let g = AstGraph::from_source("let x = 1; in x + 2").unwrap();
1081 assert_eq!(g.canonical_hash.bytes, [0u8; 32]);
1082 let (stamped, bytes) = g.archive_and_hash().unwrap();
1083 assert_ne!(stamped.canonical_hash.bytes, [0u8; 32]);
1084 assert!(!bytes.is_empty());
1085 }
1086
1087 #[test]
1088 fn archive_roundtrips_via_rkyv() {
1089 let g = AstGraph::from_source("let x = 1; in x + 2").unwrap();
1090 let (_stamped, bytes) = g.clone().archive_and_hash().unwrap();
1091 let archived =
1092 rkyv::access::<ArchivedAstGraph, rkyv::rancor::Error>(&bytes).unwrap();
1093 assert_eq!(archived.root_id, (g.nodes.len() - 1) as u32);
1095 assert_eq!(archived.grammar_version, RNIX_GRAMMAR_VERSION);
1096 assert_eq!(archived.nodes.len(), g.nodes.len());
1097 }
1098
1099 #[test]
1100 fn archive_is_deterministic_for_same_source() {
1101 let g1 = AstGraph::from_source("let x = 1; in x + 2").unwrap();
1102 let g2 = AstGraph::from_source("let x = 1; in x + 2").unwrap();
1103 let (s1, _) = g1.archive_and_hash().unwrap();
1104 let (s2, _) = g2.archive_and_hash().unwrap();
1105 assert_eq!(s1.canonical_hash.bytes, s2.canonical_hash.bytes);
1106 }
1107
1108 #[test]
1109 fn fixtures_load_from_lisp() {
1110 let fixtures = load_fixtures().unwrap();
1111 let names: Vec<_> = fixtures.iter().map(|f| f.name.as_str()).collect();
1112 assert!(names.contains(&"literal-int"));
1113 assert!(names.contains(&"let-in-with-binop"));
1114 assert!(names.contains(&"nixos-module-skeleton"));
1115 }
1116
1117 #[test]
1118 fn nix_source_marks_dialect_as_nix() {
1119 let g = AstGraph::from_source("42").unwrap();
1120 assert!(matches!(g.dialect, SourceDialect::Nix));
1121 assert_eq!(g.grammar_version, RNIX_GRAMMAR_VERSION);
1122 }
1123
1124 #[test]
1125 fn tlisp_source_stub_marks_dialect_as_tlisp() {
1126 let g = AstGraph::from_tlisp_source("(+ 1 2)").unwrap();
1127 assert!(matches!(g.dialect, SourceDialect::Tlisp));
1128 assert_eq!(g.grammar_version, TLISP_GRAMMAR_VERSION);
1129 assert_eq!(g.nodes.len(), 1);
1130 match &g.nodes[0].kind {
1131 AstNodeKind::Unknown { kind, source_text } => {
1132 assert!(kind.contains("tlisp"));
1133 assert_eq!(source_text, "(+ 1 2)");
1134 }
1135 other => panic!("expected Unknown stub, got {other:?}"),
1136 }
1137 }
1138
1139 #[test]
1140 fn render_seams_return_unimplemented_today() {
1141 let g = AstGraph::from_source("42").unwrap();
1142 assert!(matches!(
1143 g.to_nix_source(),
1144 Err(AstGraphError::Unimplemented { .. })
1145 ));
1146 assert!(matches!(
1147 g.to_tlisp_source(),
1148 Err(AstGraphError::Unimplemented { .. })
1149 ));
1150 }
1151
1152 #[test]
1153 fn unmodeled_construct_lands_in_unknown_with_source() {
1154 let result = AstGraph::from_source("let a = { foo = 1; }; in a.bar or 0");
1158 let g = result.unwrap();
1159 assert!(g.nodes.len() >= 2);
1160 }
1161}