1use crate::ast_graph::{AstGraph, AstNodeForm, AstNodeKind, AttrEntry, NodeId as AstNodeId};
52use crate::module_graph::{
53 ConfigSetter, EnvPrefixBinding, EnvPrefixKind, ImportEdge, ModuleId, ModuleNode, OptionDecl,
54 SetterId,
55};
56
57#[derive(Debug, thiserror::Error)]
59pub enum ModuleCompilerError {
60 #[error("module root expression is not an attrset or lambda — got {kind:?}")]
61 UnexpectedRootShape { kind: &'static str },
62}
63
64pub fn compile_module(
81 label: &str,
82 ast: &AstGraph,
83 id: ModuleId,
84) -> Result<ModuleNode, ModuleCompilerError> {
85 let mut node = ModuleNode {
86 id,
87 label: label.to_string(),
88 ast_graph_hash: ast.canonical_hash.bytes,
89 option_decls: Vec::new(),
90 setters: Vec::new(),
91 imports: Vec::new(),
92 body_env_prefix: Vec::new(),
93 };
94
95 let (body_id_opt, prefix) = resolve_module_body_with_prefix(ast)?;
100 node.body_env_prefix = prefix;
101 let body_id = match body_id_opt {
102 Some(id) => id,
103 None => return Ok(node), };
105
106 let body = node_at(ast, body_id);
107
108 if let AstNodeKind::AttrSet { entries, .. } = &body.kind {
113 for entry in entries {
114 classify_top_level_entry(ast, entry, &mut node);
115 }
116 }
117
118 Ok(node)
119}
120
121fn node_at(ast: &AstGraph, id: AstNodeId) -> &AstNodeForm {
124 &ast.nodes[id as usize]
125}
126
127fn resolve_module_body_with_prefix(
146 ast: &AstGraph,
147) -> Result<(Option<AstNodeId>, Vec<EnvPrefixBinding>), ModuleCompilerError> {
148 let mut cursor = ast.root_id;
149 let mut prefix: Vec<EnvPrefixBinding> = Vec::new();
150 let mut with_depth = 0u32;
151 for _ in 0..32 {
152 let node = node_at(ast, cursor);
153 match &node.kind {
154 AstNodeKind::Lambda { body, .. } => {
155 cursor = *body;
156 continue;
157 }
158 AstNodeKind::LetIn { bindings, body, .. } => {
159 for entry in bindings {
160 if entry.path.len() == 1 {
161 prefix.push(EnvPrefixBinding {
162 name: entry.path[0].clone(),
163 value_node_id: entry.value,
164 kind: EnvPrefixKind::Let,
165 });
166 }
167 }
168 cursor = *body;
169 continue;
170 }
171 AstNodeKind::With { env: scope, body } => {
172 prefix.push(EnvPrefixBinding {
173 name: format!("__with_{with_depth}"),
174 value_node_id: *scope,
175 kind: EnvPrefixKind::With,
176 });
177 with_depth += 1;
178 cursor = *body;
179 continue;
180 }
181 AstNodeKind::Assert { body, .. } => {
182 cursor = *body;
183 continue;
184 }
185 AstNodeKind::AttrSet { .. } => return Ok((Some(cursor), prefix)),
186 AstNodeKind::Unknown { .. } | AstNodeKind::Null => {
190 return Ok((None, prefix));
191 }
192 other => {
193 return Err(ModuleCompilerError::UnexpectedRootShape {
194 kind: kind_name(other),
195 });
196 }
197 }
198 }
199 Ok((None, prefix))
200}
201
202fn kind_name(k: &AstNodeKind) -> &'static str {
203 match k {
204 AstNodeKind::Int(_) => "Int",
205 AstNodeKind::Float(_) => "Float",
206 AstNodeKind::Bool(_) => "Bool",
207 AstNodeKind::Null => "Null",
208 AstNodeKind::Str { .. } => "Str",
209 AstNodeKind::IndentedStr { .. } => "IndentedStr",
210 AstNodeKind::Path(_) => "Path",
211 AstNodeKind::Ident(_) => "Ident",
212 AstNodeKind::Select { .. } => "Select",
213 AstNodeKind::HasAttr { .. } => "HasAttr",
214 AstNodeKind::List(_) => "List",
215 AstNodeKind::AttrSet { .. } => "AttrSet",
216 AstNodeKind::LetIn { .. } => "LetIn",
217 AstNodeKind::With { .. } => "With",
218 AstNodeKind::Assert { .. } => "Assert",
219 AstNodeKind::Lambda { .. } => "Lambda",
220 AstNodeKind::Apply { .. } => "Apply",
221 AstNodeKind::IfThenElse { .. } => "IfThenElse",
222 AstNodeKind::BinOp { .. } => "BinOp",
223 AstNodeKind::UnaryOp { .. } => "UnaryOp",
224 AstNodeKind::Unknown { .. } => "Unknown",
225 }
226}
227
228fn classify_top_level_entry(ast: &AstGraph, entry: &AttrEntry, node: &mut ModuleNode) {
233 if entry.path.is_empty() {
234 return;
235 }
236 match entry.path[0].as_str() {
237 "options" => harvest_options(ast, &entry.path[1..], entry.value, node),
238 "config" => harvest_config(ast, &entry.path[1..], entry.value, node, None, 100),
239 "imports" => harvest_imports(ast, entry.value, node),
240 _ => {
241 harvest_config(ast, &entry.path, entry.value, node, None, 100);
245 }
246 }
247}
248
249fn harvest_options(ast: &AstGraph, path: &[String], value: AstNodeId, node: &mut ModuleNode) {
257 let v = node_at(ast, value);
258 match &v.kind {
259 AstNodeKind::Apply { function, argument } => {
261 if is_call_to(ast, *function, "mkOption") {
262 if let Some(decl) = mk_option_decl(ast, path, *argument) {
263 node.option_decls.push(decl);
264 }
265 }
266 }
267 AstNodeKind::AttrSet { entries, .. } => {
269 for child in entries {
270 let mut sub = path.to_vec();
271 sub.extend(child.path.iter().cloned());
272 harvest_options(ast, &sub, child.value, node);
273 }
274 }
275 _ => {
276 node.option_decls.push(OptionDecl {
280 path: path.to_vec(),
281 type_tag: "unknown".to_string(),
282 has_default: false,
283 description: None,
284 });
285 }
286 }
287}
288
289fn mk_option_decl(ast: &AstGraph, path: &[String], args: AstNodeId) -> Option<OptionDecl> {
290 let a = node_at(ast, args);
291 if let AstNodeKind::AttrSet { entries, .. } = &a.kind {
292 let mut type_tag = "unknown".to_string();
293 let mut has_default = false;
294 let mut description: Option<String> = None;
295 for e in entries {
296 if e.path.len() != 1 {
297 continue;
298 }
299 match e.path[0].as_str() {
300 "type" => {
301 let t = node_at(ast, e.value);
302 if let AstNodeKind::Ident(name) = &t.kind {
303 type_tag = name.clone();
304 } else if let AstNodeKind::Select { path: p, .. } = &t.kind {
305 if let Some(last) = p.last() {
307 type_tag = last.clone();
308 }
309 }
310 }
311 "default" => has_default = true,
312 "description" => {
313 let d = node_at(ast, e.value);
314 if let AstNodeKind::Str { segments } = &d.kind {
315 let mut buf = String::new();
316 for s in segments {
317 if let crate::ast_graph::StrSegment::Literal(t) = s {
318 buf.push_str(t);
319 }
320 }
321 description = Some(buf);
322 }
323 }
324 _ => {}
325 }
326 }
327 Some(OptionDecl {
328 path: path.to_vec(),
329 type_tag,
330 has_default,
331 description,
332 })
333 } else {
334 None
335 }
336}
337
338fn harvest_config(
352 ast: &AstGraph,
353 path: &[String],
354 value: AstNodeId,
355 node: &mut ModuleNode,
356 condition: Option<AstNodeId>,
357 priority: u32,
358) {
359 let v = node_at(ast, value);
360 match &v.kind {
361 AstNodeKind::Apply { function, argument } => {
362 if is_call_to(ast, *function, "mkIf") {
363 if let Some((cond, body)) = split_mkif_args(ast, *function, *argument) {
367 harvest_config(ast, path, body, node, Some(cond), priority);
368 return;
369 }
370 }
371 if is_call_to(ast, *function, "mkForce") {
372 harvest_config(ast, path, *argument, node, condition, 50);
373 return;
374 }
375 if is_call_to(ast, *function, "mkVMOverride") {
376 harvest_config(ast, path, *argument, node, condition, 10);
377 return;
378 }
379 if is_call_to(ast, *function, "mkMerge") {
380 let arg = node_at(ast, *argument);
383 if let AstNodeKind::List(items) = &arg.kind {
384 for item in items {
385 harvest_config(ast, path, *item, node, condition, priority);
386 }
387 return;
388 }
389 }
390 emit_setter(ast, node, path, value, condition, priority);
393 }
394 AstNodeKind::AttrSet { entries, .. } => {
395 for child in entries {
397 let mut sub = path.to_vec();
398 sub.extend(child.path.iter().cloned());
399 harvest_config(ast, &sub, child.value, node, condition, priority);
400 }
401 }
402 _ => {
403 emit_setter(ast, node, path, value, condition, priority);
405 }
406 }
407}
408
409fn split_mkif_args(
410 ast: &AstGraph,
411 function: AstNodeId,
412 body: AstNodeId,
413) -> Option<(AstNodeId, AstNodeId)> {
414 let f = node_at(ast, function);
417 if let AstNodeKind::Apply { argument, .. } = &f.kind {
418 Some((*argument, body))
419 } else {
420 None
421 }
422}
423
424fn emit_setter(
425 ast: &AstGraph,
426 node: &mut ModuleNode,
427 path: &[String],
428 body_ast_root: AstNodeId,
429 condition_ast_root: Option<AstNodeId>,
430 priority: u32,
431) {
432 let id = node.setters.len() as SetterId;
433 let mut slice = collect_config_read_slice(ast, body_ast_root);
438 if let Some(cond_id) = condition_ast_root {
439 let cond_slice = collect_config_read_slice(ast, cond_id);
440 slice.extend(cond_slice);
441 slice.sort();
442 slice.dedup();
443 }
444 node.setters.push(ConfigSetter {
445 id,
446 assigns_path: path.to_vec(),
447 slice,
448 body_ast_root,
449 condition_ast_root,
450 priority,
451 });
452}
453
454#[must_use]
461pub fn collect_config_read_slice(ast: &AstGraph, body_ast_root: AstNodeId) -> Vec<Vec<String>> {
462 let mut out: Vec<Vec<String>> = Vec::new();
463 walk_for_config_reads(ast, body_ast_root, &mut out);
464 out.sort();
465 out.dedup();
466 out
467}
468
469fn walk_for_config_reads(ast: &AstGraph, id: AstNodeId, out: &mut Vec<Vec<String>>) {
470 let n = node_at(ast, id);
471 match &n.kind {
472 AstNodeKind::Select { target, path, fallback } => {
473 let t = node_at(ast, *target);
474 if matches!(&t.kind, AstNodeKind::Ident(s) if s == "config") {
475 out.push(path.clone());
476 } else {
477 walk_for_config_reads(ast, *target, out);
478 }
479 if let Some(f) = fallback {
480 walk_for_config_reads(ast, *f, out);
481 }
482 }
483 AstNodeKind::HasAttr { target, path } => {
484 let t = node_at(ast, *target);
485 if matches!(&t.kind, AstNodeKind::Ident(s) if s == "config") {
486 out.push(path.clone());
487 } else {
488 walk_for_config_reads(ast, *target, out);
489 }
490 }
491 AstNodeKind::Apply { function, argument } => {
492 walk_for_config_reads(ast, *function, out);
493 walk_for_config_reads(ast, *argument, out);
494 }
495 AstNodeKind::List(items) => {
496 for item in items {
497 walk_for_config_reads(ast, *item, out);
498 }
499 }
500 AstNodeKind::AttrSet { entries, inherits, .. } => {
501 for e in entries {
502 walk_for_config_reads(ast, e.value, out);
503 }
504 for i in inherits {
505 if let Some(s) = i.source {
506 walk_for_config_reads(ast, s, out);
507 }
508 }
509 }
510 AstNodeKind::LetIn { bindings, inherits, body } => {
511 for b in bindings {
512 walk_for_config_reads(ast, b.value, out);
513 }
514 for i in inherits {
515 if let Some(s) = i.source {
516 walk_for_config_reads(ast, s, out);
517 }
518 }
519 walk_for_config_reads(ast, *body, out);
520 }
521 AstNodeKind::With { env, body } => {
522 walk_for_config_reads(ast, *env, out);
523 walk_for_config_reads(ast, *body, out);
524 }
525 AstNodeKind::Assert { condition, body } => {
526 walk_for_config_reads(ast, *condition, out);
527 walk_for_config_reads(ast, *body, out);
528 }
529 AstNodeKind::Lambda { body, .. } => {
530 walk_for_config_reads(ast, *body, out);
531 }
532 AstNodeKind::IfThenElse {
533 condition,
534 then_branch,
535 else_branch,
536 } => {
537 walk_for_config_reads(ast, *condition, out);
538 walk_for_config_reads(ast, *then_branch, out);
539 walk_for_config_reads(ast, *else_branch, out);
540 }
541 AstNodeKind::BinOp { left, right, .. } => {
542 walk_for_config_reads(ast, *left, out);
543 walk_for_config_reads(ast, *right, out);
544 }
545 AstNodeKind::UnaryOp { operand, .. } => {
546 walk_for_config_reads(ast, *operand, out);
547 }
548 AstNodeKind::Str { segments } | AstNodeKind::IndentedStr { segments } => {
549 for s in segments {
550 if let crate::ast_graph::StrSegment::Interpolation(id) = s {
551 walk_for_config_reads(ast, *id, out);
552 }
553 }
554 }
555 AstNodeKind::Int(_)
557 | AstNodeKind::Float(_)
558 | AstNodeKind::Bool(_)
559 | AstNodeKind::Null
560 | AstNodeKind::Path(_)
561 | AstNodeKind::Ident(_)
562 | AstNodeKind::Unknown { .. } => {}
563 }
564}
565
566fn harvest_imports(ast: &AstGraph, value: AstNodeId, node: &mut ModuleNode) {
575 let v = node_at(ast, value);
576 if let AstNodeKind::List(items) = &v.kind {
577 for item in items {
578 node.imports.push(ImportEdge {
579 target: u32::MAX, condition_ast_root: None,
581 });
582 let _ = item; }
584 }
585}
586
587fn is_call_to(ast: &AstGraph, function: AstNodeId, name: &str) -> bool {
592 let f = node_at(ast, function);
593 match &f.kind {
594 AstNodeKind::Ident(s) => s == name,
595 AstNodeKind::Select { path, .. } => {
596 path.last().map_or(false, |last| last == name)
597 }
598 AstNodeKind::Apply { function: inner, .. } => {
599 is_call_to(ast, *inner, name)
602 }
603 _ => false,
604 }
605}
606
607#[cfg(test)]
608mod tests {
609 use super::*;
610 use pretty_assertions::assert_eq;
611
612 fn ast(src: &str) -> AstGraph {
613 AstGraph::from_source(src).expect("parse")
614 }
615
616 #[test]
617 fn empty_module_compiles_partial() {
618 let m = compile_module("empty.nix", &ast("{ }"), 0).unwrap();
619 assert_eq!(m.label, "empty.nix");
620 assert!(m.option_decls.is_empty());
621 assert!(m.setters.is_empty());
622 assert!(m.imports.is_empty());
623 }
624
625 #[test]
626 fn lambda_wrapped_module_dives_to_body() {
627 let m = compile_module(
628 "wrapped.nix",
629 &ast("{ config, lib, pkgs, ... }: { config.networking.hostName = \"rio\"; }"),
630 0,
631 )
632 .unwrap();
633 assert_eq!(m.setters.len(), 1);
634 assert_eq!(
635 m.setters[0].assigns_path,
636 vec!["networking", "hostName"]
637 );
638 }
639
640 #[test]
641 fn top_level_sugar_treats_as_config() {
642 let m = compile_module(
646 "sugar.nix",
647 &ast("{ networking.hostName = \"rio\"; }"),
648 0,
649 )
650 .unwrap();
651 assert_eq!(m.setters.len(), 1);
652 assert_eq!(
653 m.setters[0].assigns_path,
654 vec!["networking", "hostName"]
655 );
656 }
657
658 #[test]
659 fn mkOption_extracts_typed_decl() {
660 let m = compile_module(
662 "opt.nix",
663 &ast(
664 "{ config, ... }: { options.services.atticd.enable = \
665 mkOption { type = types.bool; default = false; \
666 description = \"enable atticd\"; }; }",
667 ),
668 0,
669 )
670 .unwrap();
671 assert_eq!(m.option_decls.len(), 1);
672 let d = &m.option_decls[0];
673 assert_eq!(d.path, vec!["services", "atticd", "enable"]);
674 assert_eq!(d.type_tag, "bool");
675 assert!(d.has_default);
676 assert_eq!(d.description.as_deref(), Some("enable atticd"));
677 }
678
679 #[test]
680 fn mkForce_sets_priority_to_50() {
681 let m = compile_module(
682 "force.nix",
683 &ast(
684 "{ config, ... }: { config.services.atticd.enable = mkForce true; }",
685 ),
686 0,
687 )
688 .unwrap();
689 assert_eq!(m.setters.len(), 1);
690 assert_eq!(m.setters[0].priority, 50);
691 }
692
693 #[test]
694 fn mkMerge_decomposes_into_per_attr_setters() {
695 let m = compile_module(
696 "merge.nix",
697 &ast(
698 "{ config, ... }: { config = mkMerge [ \
699 { networking.hostName = \"rio\"; } \
700 { boot.kernelParams = []; } \
701 ]; }",
702 ),
703 0,
704 )
705 .unwrap();
706 let paths: Vec<&[String]> =
708 m.setters.iter().map(|s| s.assigns_path.as_slice()).collect();
709 assert!(paths.iter().any(|p| p == &["networking", "hostName"]));
710 assert!(paths.iter().any(|p| p == &["boot", "kernelParams"]));
711 }
712
713 #[test]
714 fn slice_analysis_picks_up_config_reads() {
715 let g = ast(
716 "{ config, ... }: { config.boot.kernelParams = \
717 if config.networking.hostName == \"rio\" \
718 then [ \"amd_pstate=active\" ] \
719 else []; }",
720 );
721 let m = compile_module("slice.nix", &g, 0).unwrap();
722 assert_eq!(m.setters.len(), 1);
723 assert!(
726 m.setters[0]
727 .slice
728 .iter()
729 .any(|p| p == &vec!["networking", "hostName"]),
730 "expected setter.slice to contain networking.hostName; got {:?}",
731 m.setters[0].slice
732 );
733 let walker_slice =
736 collect_config_read_slice(&g, m.setters[0].body_ast_root);
737 assert!(walker_slice
738 .iter()
739 .any(|p| p == &vec!["networking", "hostName"]));
740 }
741
742 #[test]
743 fn slice_includes_mkif_condition_reads() {
744 let g = ast(
749 "{ config, ... }: { config.boot.kernelParams = \
750 mkIf config.services.atticd.enable [ \"foo\" ]; }",
751 );
752 let m = compile_module("slice_cond.nix", &g, 0).unwrap();
753 assert_eq!(m.setters.len(), 1);
754 assert!(
755 m.setters[0]
756 .slice
757 .iter()
758 .any(|p| p == &vec!["services", "atticd", "enable"]),
759 "expected slice to include condition's reads; got {:?}",
760 m.setters[0].slice
761 );
762 }
763
764 #[test]
765 fn imports_list_captures_count() {
766 let m = compile_module(
767 "with-imports.nix",
768 &ast(
769 "{ config, ... }: { \
770 imports = [ ./a.nix ./b.nix ./c.nix ]; \
771 config.x = 1; }",
772 ),
773 0,
774 )
775 .unwrap();
776 assert_eq!(m.imports.len(), 3);
777 for edge in &m.imports {
778 assert_eq!(edge.target, u32::MAX);
780 }
781 }
782
783 #[test]
784 fn id_threads_through() {
785 let m = compile_module("x.nix", &ast("{ }"), 42).unwrap();
786 assert_eq!(m.id, 42);
787 }
788
789 #[test]
790 fn ast_graph_hash_carries_to_module_node() {
791 let g = ast("{ x = 1; }");
792 let m = compile_module("hash.nix", &g, 0).unwrap();
793 assert_eq!(m.ast_graph_hash, g.canonical_hash.bytes);
794 }
795}