1use std::collections::BTreeMap;
47
48use serde::{Deserialize, Serialize};
49
50use crate::ast_graph::{AstGraph, AstNodeKind, BinaryOp, NodeId, StrSegment, UnaryOp};
51
52#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
54pub struct PatternFormal {
55 pub name: String,
56 pub default_node_id: Option<NodeId>,
60}
61
62#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
65pub enum EvalValue {
66 Int(i64),
67 Float(f64),
68 Bool(bool),
69 Null,
70 Str(String),
71 Path(String),
72 List(Vec<EvalValue>),
73 AttrSet(BTreeMap<String, EvalValue>),
74 Closure {
78 param: String,
79 body_node_id: NodeId,
80 captured_env: BTreeMap<String, EvalValue>,
81 },
82 PatternClosure {
87 formals: Vec<PatternFormal>,
89 accepts_extra: bool,
91 binding_name: Option<String>,
93 body_node_id: NodeId,
94 captured_env: BTreeMap<String, EvalValue>,
95 },
96 Builtin {
101 kind: String,
102 payload: Box<EvalValue>,
103 },
104 Derivation {
111 name: String,
112 system: String,
113 drv_path: String,
114 out_path: String,
115 attrs: BTreeMap<String, EvalValue>,
116 },
117 Opaque {
121 kind: String,
122 node_id: NodeId,
123 },
124}
125
126#[derive(Debug, thiserror::Error)]
128pub enum EvalError {
129 #[error("type mismatch in {context}: expected {expected}, got {got}")]
130 TypeMismatch {
131 context: &'static str,
132 expected: &'static str,
133 got: &'static str,
134 },
135
136 #[error("division by zero")]
137 DivisionByZero,
138
139 #[error("undefined identifier: {0}")]
140 UndefinedIdent(String),
141
142 #[error("config select walked off the env at {path:?}")]
143 ConfigMiss { path: Vec<String> },
144
145 #[error("attempted to recurse past max depth ({0})")]
146 DepthExceeded(u32),
147}
148
149#[derive(Debug, Default, Clone)]
154pub struct EvalEnv {
155 pub bindings: BTreeMap<String, EvalValue>,
156}
157
158impl EvalEnv {
159 #[must_use]
161 pub fn new() -> Self {
162 Self::default()
163 }
164
165 #[must_use]
167 pub fn with_binding(mut self, name: impl Into<String>, value: EvalValue) -> Self {
168 self.bindings.insert(name.into(), value);
169 self
170 }
171
172 pub fn get(&self, name: &str) -> Option<&EvalValue> {
174 self.bindings.get(name)
175 }
176}
177
178const MAX_DEPTH: u32 = 256;
182
183pub fn eval_node(ast: &AstGraph, id: NodeId, env: &EvalEnv) -> Result<EvalValue, EvalError> {
189 eval_at(ast, id, env, 0)
190}
191
192fn eval_at(
193 ast: &AstGraph,
194 id: NodeId,
195 env: &EvalEnv,
196 depth: u32,
197) -> Result<EvalValue, EvalError> {
198 if depth >= MAX_DEPTH {
199 return Err(EvalError::DepthExceeded(MAX_DEPTH));
200 }
201 let node = &ast.nodes[id as usize];
202 match &node.kind {
203 AstNodeKind::Int(n) => Ok(EvalValue::Int(*n)),
204 AstNodeKind::Float(f) => Ok(EvalValue::Float(*f)),
205 AstNodeKind::Bool(b) => Ok(EvalValue::Bool(*b)),
206 AstNodeKind::Null => Ok(EvalValue::Null),
207 AstNodeKind::Path(p) => Ok(EvalValue::Path(p.clone())),
208 AstNodeKind::Str { segments } | AstNodeKind::IndentedStr { segments } => {
209 eval_string_segments(ast, segments, env, depth + 1, id)
210 }
211 AstNodeKind::Ident(name) => {
212 match name.as_str() {
217 "true" => return Ok(EvalValue::Bool(true)),
218 "false" => return Ok(EvalValue::Bool(false)),
219 "null" => return Ok(EvalValue::Null),
220 _ => {}
221 }
222 env.get(name)
223 .cloned()
224 .ok_or_else(|| EvalError::UndefinedIdent(name.clone()))
225 }
226 AstNodeKind::Select {
227 target,
228 path,
229 fallback,
230 } => eval_select(ast, *target, path, *fallback, env, depth + 1),
231 AstNodeKind::HasAttr { target, path } => {
232 eval_has_attr(ast, *target, path, env, depth + 1)
233 }
234 AstNodeKind::List(items) => {
235 let mut out = Vec::with_capacity(items.len());
236 for item in items {
237 out.push(eval_at(ast, *item, env, depth + 1)?);
238 }
239 Ok(EvalValue::List(out))
240 }
241 AstNodeKind::AttrSet { entries, .. } => {
242 let mut out: BTreeMap<String, EvalValue> = BTreeMap::new();
243 for entry in entries {
244 let value = eval_at(ast, entry.value, env, depth + 1)?;
245 insert_at_path(&mut out, &entry.path, value);
246 }
247 Ok(EvalValue::AttrSet(out))
248 }
249 AstNodeKind::IfThenElse {
250 condition,
251 then_branch,
252 else_branch,
253 } => {
254 let c = eval_at(ast, *condition, env, depth + 1)?;
255 match c {
256 EvalValue::Bool(true) => eval_at(ast, *then_branch, env, depth + 1),
257 EvalValue::Bool(false) => eval_at(ast, *else_branch, env, depth + 1),
258 other => Err(EvalError::TypeMismatch {
259 context: "if-then-else condition",
260 expected: "bool",
261 got: value_kind(&other),
262 }),
263 }
264 }
265 AstNodeKind::BinOp { op, left, right } => {
266 let l = eval_at(ast, *left, env, depth + 1)?;
267 let r = eval_at(ast, *right, env, depth + 1)?;
268 eval_binop(*op, l, r)
269 }
270 AstNodeKind::UnaryOp { op, operand } => {
271 let v = eval_at(ast, *operand, env, depth + 1)?;
272 eval_unaryop(*op, v)
273 }
274 AstNodeKind::Apply { function, argument } => {
275 eval_apply(ast, *function, *argument, env, depth + 1)
276 }
277 AstNodeKind::Lambda { param, body } => {
278 match param {
282 crate::ast_graph::LambdaParam::Ident(name) => {
283 Ok(EvalValue::Closure {
284 param: name.clone(),
285 body_node_id: *body,
286 captured_env: env.bindings.clone(),
287 })
288 }
289 crate::ast_graph::LambdaParam::Pattern {
290 binding_name,
291 formals,
292 accepts_extra,
293 } => Ok(EvalValue::PatternClosure {
294 formals: formals
295 .iter()
296 .map(|f| PatternFormal {
297 name: f.name.clone(),
298 default_node_id: f.default,
299 })
300 .collect(),
301 accepts_extra: *accepts_extra,
302 binding_name: binding_name.clone(),
303 body_node_id: *body,
304 captured_env: env.bindings.clone(),
305 }),
306 }
307 }
308 AstNodeKind::LetIn { bindings, inherits, body } => {
309 let mut env = env.clone();
310 for entry in bindings {
315 if entry.path.len() == 1 {
316 let value = eval_at(ast, entry.value, &env, depth + 1)?;
317 env.bindings.insert(entry.path[0].clone(), value);
318 }
319 }
322 for inherit in inherits {
324 if let Some(source_id) = inherit.source {
325 let source_val = eval_at(ast, source_id, &env, depth + 1)?;
326 if let EvalValue::AttrSet(map) = source_val {
327 for attr in &inherit.attrs {
328 if let Some(v) = map.get(attr) {
329 env.bindings.insert(attr.clone(), v.clone());
330 }
331 }
332 }
333 } else {
334 }
337 }
338 eval_at(ast, *body, &env, depth + 1)
339 }
340 AstNodeKind::With { env: scope_expr, body } => {
341 let scope_value = eval_at(ast, *scope_expr, env, depth + 1)?;
342 let mut extended = env.clone();
343 if let EvalValue::AttrSet(map) = scope_value {
344 for (k, v) in map {
348 extended.bindings.entry(k).or_insert(v);
349 }
350 }
351 eval_at(ast, *body, &extended, depth + 1)
352 }
353 AstNodeKind::Assert { body, .. } => {
354 eval_at(ast, *body, env, depth + 1)
357 }
358 AstNodeKind::Unknown { kind, .. } => Ok(EvalValue::Opaque {
359 kind: kind.clone(),
360 node_id: id,
361 }),
362 }
363}
364
365fn eval_apply(
377 ast: &AstGraph,
378 function: NodeId,
379 argument: NodeId,
380 env: &EvalEnv,
381 depth: u32,
382) -> Result<EvalValue, EvalError> {
383 let mut args: Vec<NodeId> = vec![argument];
386 let mut cursor = function;
387 loop {
388 let node = &ast.nodes[cursor as usize];
389 match &node.kind {
390 AstNodeKind::Apply { function: f, argument: a } => {
391 args.push(*a);
392 cursor = *f;
393 }
394 _ => break,
395 }
396 }
397 args.reverse();
398
399 let root_node = &ast.nodes[cursor as usize];
400
401 let builtin_name = match &root_node.kind {
403 AstNodeKind::Ident(name) => Some(name.clone()),
404 AstNodeKind::Select { path, .. } => path.last().cloned(),
405 _ => None,
406 };
407
408 if let Some(name) = builtin_name.as_deref() {
409 if let Some(result) = try_dispatch_builtin(ast, name, &args, env, depth)? {
410 return Ok(result);
411 }
412 }
413
414 let func_value = eval_at(ast, cursor, env, depth + 1);
416 if let Ok(callable) = func_value {
417 if matches!(
418 callable,
419 EvalValue::Closure { .. } | EvalValue::PatternClosure { .. }
420 ) {
421 return apply_callable(ast, callable, &args, env, depth + 1, function);
422 }
423 }
424
425 Ok(EvalValue::Opaque {
426 kind: "Apply".to_string(),
427 node_id: function,
428 })
429}
430
431fn apply_callable(
435 ast: &AstGraph,
436 callable: EvalValue,
437 args: &[NodeId],
438 caller_env: &EvalEnv,
439 depth: u32,
440 fallback_node_id: NodeId,
441) -> Result<EvalValue, EvalError> {
442 let mut arg_iter = args.iter().copied();
443 let first_arg_node = match arg_iter.next() {
444 Some(a) => a,
445 None => return Ok(EvalValue::Null),
446 };
447
448 let mut result = match callable {
449 EvalValue::Closure {
450 param,
451 body_node_id,
452 captured_env,
453 } => {
454 let first_arg = eval_at(ast, first_arg_node, caller_env, depth)?;
455 let mut call_env = EvalEnv {
456 bindings: captured_env,
457 };
458 call_env.bindings.insert(param, first_arg);
459 eval_at(ast, body_node_id, &call_env, depth)?
460 }
461 EvalValue::PatternClosure {
462 formals,
463 accepts_extra,
464 binding_name,
465 body_node_id,
466 captured_env,
467 } => {
468 let arg_value = eval_at(ast, first_arg_node, caller_env, depth)?;
470 let arg_map = match arg_value {
471 EvalValue::AttrSet(m) => m,
472 other => {
473 return Err(EvalError::TypeMismatch {
474 context: "pattern-closure arg",
475 expected: "attrset",
476 got: value_kind(&other),
477 });
478 }
479 };
480
481 let mut call_env = EvalEnv {
482 bindings: captured_env,
483 };
484
485 for formal in &formals {
489 if let Some(v) = arg_map.get(&formal.name) {
490 call_env.bindings.insert(formal.name.clone(), v.clone());
491 } else if let Some(default_node) = formal.default_node_id {
492 let default_v = eval_at(ast, default_node, &call_env, depth)?;
493 call_env.bindings.insert(formal.name.clone(), default_v);
494 } else {
495 return Err(EvalError::TypeMismatch {
496 context: "pattern-closure missing required arg",
497 expected: "formal arg without default",
498 got: "missing key",
499 });
500 }
501 }
502
503 if !accepts_extra {
505 let known: std::collections::HashSet<&str> =
506 formals.iter().map(|f| f.name.as_str()).collect();
507 for k in arg_map.keys() {
508 if !known.contains(k.as_str()) {
509 return Err(EvalError::TypeMismatch {
510 context: "pattern-closure extra arg",
511 expected: "only declared formals",
512 got: "extra key",
513 });
514 }
515 }
516 }
517
518 if let Some(name) = binding_name {
520 call_env
521 .bindings
522 .insert(name, EvalValue::AttrSet(arg_map));
523 }
524
525 eval_at(ast, body_node_id, &call_env, depth)?
526 }
527 _ => unreachable!("apply_callable called with non-callable"),
528 };
529
530 for next_arg_node in arg_iter {
533 let arg_val = eval_at(ast, next_arg_node, caller_env, depth)?;
534 match result {
535 EvalValue::Closure {
536 param,
537 body_node_id,
538 captured_env,
539 } => {
540 let mut next_env = EvalEnv {
541 bindings: captured_env,
542 };
543 next_env.bindings.insert(param, arg_val);
544 result = eval_at(ast, body_node_id, &next_env, depth)?;
545 }
546 EvalValue::PatternClosure { .. } => {
547 let arg_iter_one = &[next_arg_node];
551 let _ = arg_val; result = apply_callable(
553 ast,
554 result,
555 arg_iter_one,
556 caller_env,
557 depth,
558 fallback_node_id,
559 )?;
560 }
561 _ => {
562 return Ok(EvalValue::Opaque {
563 kind: "Apply-non-callable-result".to_string(),
564 node_id: fallback_node_id,
565 });
566 }
567 }
568 }
569 Ok(result)
570}
571
572fn try_dispatch_builtin(
576 ast: &AstGraph,
577 name: &str,
578 args: &[NodeId],
579 env: &EvalEnv,
580 depth: u32,
581) -> Result<Option<EvalValue>, EvalError> {
582 let arg = |i: usize| -> Result<EvalValue, EvalError> {
583 eval_at(ast, args[i], env, depth + 1)
584 };
585 match name {
586 "mkIf" if args.len() == 2 => {
587 let cond = arg(0)?;
588 match cond {
589 EvalValue::Bool(true) => {
590 let body = arg(1)?;
591 Ok(Some(EvalValue::Builtin {
592 kind: "mkIf".to_string(),
593 payload: Box::new(body),
594 }))
595 }
596 EvalValue::Bool(false) => {
597 Ok(Some(EvalValue::Builtin {
601 kind: "mkIf-disabled".to_string(),
602 payload: Box::new(EvalValue::Null),
603 }))
604 }
605 other => Err(EvalError::TypeMismatch {
606 context: "mkIf condition",
607 expected: "bool",
608 got: value_kind(&other),
609 }),
610 }
611 }
612 "mkForce" if args.len() == 1 => Ok(Some(EvalValue::Builtin {
613 kind: "mkForce".to_string(),
614 payload: Box::new(arg(0)?),
615 })),
616 "mkVMOverride" if args.len() == 1 => Ok(Some(EvalValue::Builtin {
617 kind: "mkVMOverride".to_string(),
618 payload: Box::new(arg(0)?),
619 })),
620 "mkDefault" if args.len() == 1 => Ok(Some(EvalValue::Builtin {
621 kind: "mkDefault".to_string(),
622 payload: Box::new(arg(0)?),
623 })),
624 "mkOverride" if args.len() == 2 => {
625 let prio = arg(0)?;
628 let value = arg(1)?;
629 let kind = match prio {
630 EvalValue::Int(p) => format!("mkOverride-{p}"),
631 _ => "mkOverride-bad-priority".to_string(),
632 };
633 Ok(Some(EvalValue::Builtin {
634 kind,
635 payload: Box::new(value),
636 }))
637 }
638 "mkMerge" if args.len() == 1 => {
639 let list = arg(0)?;
640 match list {
641 EvalValue::List(items) => Ok(Some(EvalValue::Builtin {
642 kind: "mkMerge".to_string(),
643 payload: Box::new(EvalValue::List(items)),
644 })),
645 _ => Err(EvalError::TypeMismatch {
646 context: "mkMerge arg",
647 expected: "list",
648 got: "non-list",
649 }),
650 }
651 }
652 "mkOption" if args.len() == 1 => {
653 Ok(Some(EvalValue::Builtin {
655 kind: "mkOption".to_string(),
656 payload: Box::new(arg(0)?),
657 }))
658 }
659
660 "toString" if args.len() == 1 => Ok(Some(builtin_to_string(arg(0)?))),
662 "isString" if args.len() == 1 => {
663 Ok(Some(EvalValue::Bool(matches!(arg(0)?, EvalValue::Str(_)))))
664 }
665 "isInt" if args.len() == 1 => {
666 Ok(Some(EvalValue::Bool(matches!(arg(0)?, EvalValue::Int(_)))))
667 }
668 "isFloat" if args.len() == 1 => {
669 Ok(Some(EvalValue::Bool(matches!(arg(0)?, EvalValue::Float(_)))))
670 }
671 "isBool" if args.len() == 1 => {
672 Ok(Some(EvalValue::Bool(matches!(arg(0)?, EvalValue::Bool(_)))))
673 }
674 "isNull" if args.len() == 1 => {
675 Ok(Some(EvalValue::Bool(matches!(arg(0)?, EvalValue::Null))))
676 }
677 "isList" if args.len() == 1 => {
678 Ok(Some(EvalValue::Bool(matches!(arg(0)?, EvalValue::List(_)))))
679 }
680 "isAttrs" if args.len() == 1 => Ok(Some(EvalValue::Bool(matches!(
681 arg(0)?,
682 EvalValue::AttrSet(_)
683 )))),
684 "isFunction" if args.len() == 1 => Ok(Some(EvalValue::Bool(matches!(
685 arg(0)?,
686 EvalValue::Closure { .. }
687 )))),
688 "length" if args.len() == 1 => match arg(0)? {
689 EvalValue::List(items) => Ok(Some(EvalValue::Int(items.len() as i64))),
690 EvalValue::Str(s) => Ok(Some(EvalValue::Int(s.len() as i64))),
691 other => Err(EvalError::TypeMismatch {
692 context: "length arg",
693 expected: "list or string",
694 got: value_kind(&other),
695 }),
696 },
697 "head" if args.len() == 1 => match arg(0)? {
698 EvalValue::List(items) if !items.is_empty() => Ok(Some(items[0].clone())),
699 EvalValue::List(_) => Err(EvalError::TypeMismatch {
700 context: "head arg",
701 expected: "non-empty list",
702 got: "empty list",
703 }),
704 other => Err(EvalError::TypeMismatch {
705 context: "head arg",
706 expected: "list",
707 got: value_kind(&other),
708 }),
709 },
710 "tail" if args.len() == 1 => match arg(0)? {
711 EvalValue::List(items) if !items.is_empty() => {
712 Ok(Some(EvalValue::List(items[1..].to_vec())))
713 }
714 EvalValue::List(_) => Err(EvalError::TypeMismatch {
715 context: "tail arg",
716 expected: "non-empty list",
717 got: "empty list",
718 }),
719 other => Err(EvalError::TypeMismatch {
720 context: "tail arg",
721 expected: "list",
722 got: value_kind(&other),
723 }),
724 },
725 "elem" if args.len() == 2 => {
726 let needle = arg(0)?;
727 match arg(1)? {
728 EvalValue::List(items) => {
729 Ok(Some(EvalValue::Bool(items.iter().any(|v| v == &needle))))
730 }
731 other => Err(EvalError::TypeMismatch {
732 context: "elem second arg",
733 expected: "list",
734 got: value_kind(&other),
735 }),
736 }
737 }
738 "attrNames" if args.len() == 1 => match arg(0)? {
739 EvalValue::AttrSet(map) => Ok(Some(EvalValue::List(
740 map.keys().map(|k| EvalValue::Str(k.clone())).collect(),
741 ))),
742 other => Err(EvalError::TypeMismatch {
743 context: "attrNames arg",
744 expected: "attrset",
745 got: value_kind(&other),
746 }),
747 },
748 "attrValues" if args.len() == 1 => match arg(0)? {
749 EvalValue::AttrSet(map) => {
750 Ok(Some(EvalValue::List(map.into_values().collect())))
751 }
752 other => Err(EvalError::TypeMismatch {
753 context: "attrValues arg",
754 expected: "attrset",
755 got: value_kind(&other),
756 }),
757 },
758 "hasAttr" if args.len() == 2 => {
759 let name = match arg(0)? {
760 EvalValue::Str(s) => s,
761 other => {
762 return Err(EvalError::TypeMismatch {
763 context: "hasAttr first arg",
764 expected: "string",
765 got: value_kind(&other),
766 })
767 }
768 };
769 match arg(1)? {
770 EvalValue::AttrSet(map) => {
771 Ok(Some(EvalValue::Bool(map.contains_key(&name))))
772 }
773 other => Err(EvalError::TypeMismatch {
774 context: "hasAttr second arg",
775 expected: "attrset",
776 got: value_kind(&other),
777 }),
778 }
779 }
780 "getAttr" if args.len() == 2 => {
781 let name = match arg(0)? {
782 EvalValue::Str(s) => s,
783 other => {
784 return Err(EvalError::TypeMismatch {
785 context: "getAttr first arg",
786 expected: "string",
787 got: value_kind(&other),
788 })
789 }
790 };
791 match arg(1)? {
792 EvalValue::AttrSet(map) => map
793 .get(&name)
794 .cloned()
795 .map(Some)
796 .ok_or(EvalError::ConfigMiss { path: vec![name] }),
797 other => Err(EvalError::TypeMismatch {
798 context: "getAttr second arg",
799 expected: "attrset",
800 got: value_kind(&other),
801 }),
802 }
803 }
804 "concatLists" if args.len() == 1 => match arg(0)? {
805 EvalValue::List(items) => {
806 let mut out = Vec::new();
807 for item in items {
808 match item {
809 EvalValue::List(inner) => out.extend(inner),
810 other => {
811 return Err(EvalError::TypeMismatch {
812 context: "concatLists element",
813 expected: "list",
814 got: value_kind(&other),
815 })
816 }
817 }
818 }
819 Ok(Some(EvalValue::List(out)))
820 }
821 other => Err(EvalError::TypeMismatch {
822 context: "concatLists arg",
823 expected: "list of lists",
824 got: value_kind(&other),
825 }),
826 },
827 "concatStringsSep" if args.len() == 2 => {
828 let sep = match arg(0)? {
829 EvalValue::Str(s) => s,
830 other => {
831 return Err(EvalError::TypeMismatch {
832 context: "concatStringsSep first arg",
833 expected: "string",
834 got: value_kind(&other),
835 })
836 }
837 };
838 match arg(1)? {
839 EvalValue::List(items) => {
840 let strs: Result<Vec<String>, _> = items
841 .into_iter()
842 .map(|v| match v {
843 EvalValue::Str(s) => Ok(s),
844 other => Err(EvalError::TypeMismatch {
845 context: "concatStringsSep list element",
846 expected: "string",
847 got: value_kind(&other),
848 }),
849 })
850 .collect();
851 Ok(Some(EvalValue::Str(strs?.join(&sep))))
852 }
853 other => Err(EvalError::TypeMismatch {
854 context: "concatStringsSep second arg",
855 expected: "list",
856 got: value_kind(&other),
857 }),
858 }
859 }
860 "throw" if args.len() == 1 => match arg(0)? {
861 EvalValue::Str(s) => Err(EvalError::UndefinedIdent(format!("throw: {s}"))),
862 other => Err(EvalError::TypeMismatch {
863 context: "throw arg",
864 expected: "string",
865 got: value_kind(&other),
866 }),
867 },
868 "abort" if args.len() == 1 => match arg(0)? {
869 EvalValue::Str(s) => Err(EvalError::UndefinedIdent(format!("abort: {s}"))),
870 other => Err(EvalError::TypeMismatch {
871 context: "abort arg",
872 expected: "string",
873 got: value_kind(&other),
874 }),
875 },
876
877 "optional" if args.len() == 2 => match arg(0)? {
880 EvalValue::Bool(true) => Ok(Some(EvalValue::List(vec![arg(1)?]))),
881 EvalValue::Bool(false) => Ok(Some(EvalValue::List(Vec::new()))),
882 other => Err(EvalError::TypeMismatch {
883 context: "optional first arg",
884 expected: "bool",
885 got: value_kind(&other),
886 }),
887 },
888 "optionals" if args.len() == 2 => match arg(0)? {
890 EvalValue::Bool(true) => match arg(1)? {
891 v @ EvalValue::List(_) => Ok(Some(v)),
892 other => Err(EvalError::TypeMismatch {
893 context: "optionals second arg",
894 expected: "list",
895 got: value_kind(&other),
896 }),
897 },
898 EvalValue::Bool(false) => Ok(Some(EvalValue::List(Vec::new()))),
899 other => Err(EvalError::TypeMismatch {
900 context: "optionals first arg",
901 expected: "bool",
902 got: value_kind(&other),
903 }),
904 },
905 "optionalAttrs" if args.len() == 2 => match arg(0)? {
907 EvalValue::Bool(true) => match arg(1)? {
908 v @ EvalValue::AttrSet(_) => Ok(Some(v)),
909 other => Err(EvalError::TypeMismatch {
910 context: "optionalAttrs second arg",
911 expected: "attrset",
912 got: value_kind(&other),
913 }),
914 },
915 EvalValue::Bool(false) => {
916 Ok(Some(EvalValue::AttrSet(std::collections::BTreeMap::new())))
917 }
918 other => Err(EvalError::TypeMismatch {
919 context: "optionalAttrs first arg",
920 expected: "bool",
921 got: value_kind(&other),
922 }),
923 },
924 "id" if args.len() == 1 => Ok(Some(arg(0)?)),
926 "const" if args.len() == 2 => Ok(Some(arg(0)?)),
929
930 "substring" if args.len() == 3 => {
932 let start = expect_int(arg(0)?, "substring start")?;
933 let len = expect_int(arg(1)?, "substring len")?;
934 let s = expect_str(arg(2)?, "substring source")?;
935 let start = start.max(0) as usize;
936 let len = len.max(0) as usize;
937 let chars: Vec<char> = s.chars().collect();
938 let end = (start + len).min(chars.len());
939 Ok(Some(EvalValue::Str(
940 if start >= chars.len() {
941 String::new()
942 } else {
943 chars[start..end].iter().collect()
944 },
945 )))
946 }
947 "stringLength" if args.len() == 1 => {
948 let s = expect_str(arg(0)?, "stringLength arg")?;
949 Ok(Some(EvalValue::Int(s.chars().count() as i64)))
950 }
951 "replaceStrings" if args.len() == 3 => {
952 let from = expect_list_of_str(arg(0)?, "replaceStrings from")?;
953 let to = expect_list_of_str(arg(1)?, "replaceStrings to")?;
954 let src = expect_str(arg(2)?, "replaceStrings source")?;
955 if from.len() != to.len() {
956 return Err(EvalError::TypeMismatch {
957 context: "replaceStrings",
958 expected: "from.len() == to.len()",
959 got: "mismatched list lengths",
960 });
961 }
962 let mut out = src;
963 for (f, t) in from.iter().zip(to.iter()) {
964 out = out.replace(f, t);
965 }
966 Ok(Some(EvalValue::Str(out)))
967 }
968
969 "map" if args.len() == 2 => {
971 let func = arg(0)?;
972 let list = expect_list(arg(1)?, "map list")?;
973 let mut out = Vec::with_capacity(list.len());
974 for item in list {
975 let single = [synthetic_value_node(ast)];
976 let _ = single;
980 out.push(apply_value_to_one(ast, func.clone(), item, env, depth + 1)?);
981 }
982 Ok(Some(EvalValue::List(out)))
983 }
984 "filter" if args.len() == 2 => {
985 let pred = arg(0)?;
986 let list = expect_list(arg(1)?, "filter list")?;
987 let mut out = Vec::new();
988 for item in list {
989 let keep =
990 apply_value_to_one(ast, pred.clone(), item.clone(), env, depth + 1)?;
991 if matches!(keep, EvalValue::Bool(true)) {
992 out.push(item);
993 }
994 }
995 Ok(Some(EvalValue::List(out)))
996 }
997 "foldl'" if args.len() == 3 => {
998 let func = arg(0)?;
999 let init = arg(1)?;
1000 let list = expect_list(arg(2)?, "foldl' list")?;
1001 let mut acc = init;
1002 for item in list {
1003 acc = apply_value_to_two(
1004 ast,
1005 func.clone(),
1006 acc,
1007 item,
1008 env,
1009 depth + 1,
1010 )?;
1011 }
1012 Ok(Some(acc))
1013 }
1014 "genList" if args.len() == 2 => {
1015 let func = arg(0)?;
1016 let n = expect_int(arg(1)?, "genList count")?;
1017 let mut out = Vec::with_capacity(n.max(0) as usize);
1018 for i in 0..n.max(0) {
1019 out.push(apply_value_to_one(
1020 ast,
1021 func.clone(),
1022 EvalValue::Int(i),
1023 env,
1024 depth + 1,
1025 )?);
1026 }
1027 Ok(Some(EvalValue::List(out)))
1028 }
1029 "concatMap" if args.len() == 2 => {
1030 let func = arg(0)?;
1031 let list = expect_list(arg(1)?, "concatMap list")?;
1032 let mut out = Vec::new();
1033 for item in list {
1034 let v = apply_value_to_one(ast, func.clone(), item, env, depth + 1)?;
1035 match v {
1036 EvalValue::List(items) => out.extend(items),
1037 other => {
1038 return Err(EvalError::TypeMismatch {
1039 context: "concatMap fn result",
1040 expected: "list",
1041 got: value_kind(&other),
1042 });
1043 }
1044 }
1045 }
1046 Ok(Some(EvalValue::List(out)))
1047 }
1048
1049 "intersectAttrs" if args.len() == 2 => {
1051 let a = expect_attrset(arg(0)?, "intersectAttrs first")?;
1052 let b = expect_attrset(arg(1)?, "intersectAttrs second")?;
1053 let mut out: BTreeMap<String, EvalValue> = BTreeMap::new();
1054 for (k, v) in b {
1055 if a.contains_key(&k) {
1056 out.insert(k, v);
1057 }
1058 }
1059 Ok(Some(EvalValue::AttrSet(out)))
1060 }
1061 "removeAttrs" if args.len() == 2 => {
1062 let mut attrs = expect_attrset(arg(0)?, "removeAttrs source")?;
1063 let names = expect_list_of_str(arg(1)?, "removeAttrs names")?;
1064 for n in names {
1065 attrs.remove(&n);
1066 }
1067 Ok(Some(EvalValue::AttrSet(attrs)))
1068 }
1069 "listToAttrs" if args.len() == 1 => {
1070 let entries = expect_list(arg(0)?, "listToAttrs list")?;
1071 let mut out: BTreeMap<String, EvalValue> = BTreeMap::new();
1072 for entry in entries {
1073 let map = expect_attrset(entry, "listToAttrs entry")?;
1074 let name = map
1075 .get("name")
1076 .and_then(|v| match v {
1077 EvalValue::Str(s) => Some(s.clone()),
1078 _ => None,
1079 })
1080 .ok_or(EvalError::TypeMismatch {
1081 context: "listToAttrs entry.name",
1082 expected: "string",
1083 got: "missing or non-string",
1084 })?;
1085 let value = map.get("value").cloned().unwrap_or(EvalValue::Null);
1086 out.insert(name, value);
1087 }
1088 Ok(Some(EvalValue::AttrSet(out)))
1089 }
1090
1091 "readFile" if args.len() == 1 => {
1095 let path = expect_str_or_path(arg(0)?, "readFile arg")?;
1096 let bytes = std::fs::read_to_string(&path).map_err(|e| {
1097 EvalError::TypeMismatch {
1098 context: "readFile io",
1099 expected: "existing file",
1100 got: leak_msg(format!("io error reading {path}: {e}")),
1101 }
1102 })?;
1103 Ok(Some(EvalValue::Str(bytes)))
1104 }
1105 "pathExists" if args.len() == 1 => {
1106 let path = expect_str_or_path(arg(0)?, "pathExists arg")?;
1107 Ok(Some(EvalValue::Bool(std::path::Path::new(&path).exists())))
1108 }
1109 "import" if args.len() == 1 => {
1115 let path = expect_str_or_path(arg(0)?, "import arg")?;
1116 if !std::path::Path::new(&path).is_absolute() {
1117 return Err(EvalError::TypeMismatch {
1118 context: "import",
1119 expected: "absolute path (relative imports need call-site ctx)",
1120 got: leak_msg(format!("relative path: {path}")),
1121 });
1122 }
1123 let source = std::fs::read_to_string(&path).map_err(|e| {
1124 EvalError::TypeMismatch {
1125 context: "import io",
1126 expected: "existing .nix file",
1127 got: leak_msg(format!("io error reading {path}: {e}")),
1128 }
1129 })?;
1130 let imported_graph = AstGraph::from_source(&source).map_err(|e| {
1131 EvalError::TypeMismatch {
1132 context: "import parse",
1133 expected: "valid nix source",
1134 got: leak_msg(format!("parse error: {e}")),
1135 }
1136 })?;
1137 let imported_env = EvalEnv::new();
1140 let v = eval_node(&imported_graph, imported_graph.root_id, &imported_env)
1145 .map_err(|e| EvalError::TypeMismatch {
1146 context: "import eval",
1147 expected: "successful eval",
1148 got: leak_msg(format!("eval error: {e}")),
1149 })?;
1150 Ok(Some(v))
1151 }
1152
1153 "filterAttrs" if args.len() == 2 => {
1156 let pred = arg(0)?;
1157 let attrs = expect_attrset(arg(1)?, "filterAttrs set")?;
1158 let mut out: BTreeMap<String, EvalValue> = BTreeMap::new();
1159 for (k, v) in attrs {
1160 let keep = apply_value_to_two(
1161 ast,
1162 pred.clone(),
1163 EvalValue::Str(k.clone()),
1164 v.clone(),
1165 env,
1166 depth + 1,
1167 )?;
1168 if matches!(keep, EvalValue::Bool(true)) {
1169 out.insert(k, v);
1170 }
1171 }
1172 Ok(Some(EvalValue::AttrSet(out)))
1173 }
1174 "mapAttrs" if args.len() == 2 => {
1176 let func = arg(0)?;
1177 let attrs = expect_attrset(arg(1)?, "mapAttrs set")?;
1178 let mut out: BTreeMap<String, EvalValue> = BTreeMap::new();
1179 for (k, v) in attrs {
1180 let new_v = apply_value_to_two(
1181 ast,
1182 func.clone(),
1183 EvalValue::Str(k.clone()),
1184 v,
1185 env,
1186 depth + 1,
1187 )?;
1188 out.insert(k, new_v);
1189 }
1190 Ok(Some(EvalValue::AttrSet(out)))
1191 }
1192 "flatten" if args.len() == 1 => {
1194 fn flat(out: &mut Vec<EvalValue>, v: EvalValue) {
1195 match v {
1196 EvalValue::List(items) => {
1197 for i in items {
1198 flat(out, i);
1199 }
1200 }
1201 other => out.push(other),
1202 }
1203 }
1204 let mut out = Vec::new();
1205 flat(&mut out, arg(0)?);
1206 Ok(Some(EvalValue::List(out)))
1207 }
1208 "unique" if args.len() == 1 => {
1209 let list = expect_list(arg(0)?, "unique arg")?;
1210 let mut seen: Vec<EvalValue> = Vec::new();
1211 for v in list {
1212 if !seen.contains(&v) {
1213 seen.push(v);
1214 }
1215 }
1216 Ok(Some(EvalValue::List(seen)))
1217 }
1218 "take" if args.len() == 2 => {
1219 let n = expect_int(arg(0)?, "take count")?;
1220 let list = expect_list(arg(1)?, "take list")?;
1221 let n = n.max(0) as usize;
1222 Ok(Some(EvalValue::List(
1223 list.into_iter().take(n).collect(),
1224 )))
1225 }
1226 "drop" if args.len() == 2 => {
1227 let n = expect_int(arg(0)?, "drop count")?;
1228 let list = expect_list(arg(1)?, "drop list")?;
1229 let n = n.max(0) as usize;
1230 Ok(Some(EvalValue::List(
1231 list.into_iter().skip(n).collect(),
1232 )))
1233 }
1234 "derivation" if args.len() == 1 => Ok(Some(eval_derivation(arg(0)?)?)),
1245 "__derivationStrict" if args.len() == 1 => Ok(Some(eval_derivation(arg(0)?)?)),
1246 "derivationStrict" if args.len() == 1 => Ok(Some(eval_derivation(arg(0)?)?)),
1250
1251 "fetchurl" if args.len() == 1 => Ok(Some(eval_fetchurl(arg(0)?)?)),
1260 "fetchTarball" if args.len() == 1 => Ok(Some(eval_fetch_tarball(arg(0)?)?)),
1261 "fetchGit" if args.len() == 1 => Ok(Some(eval_fetch_git(arg(0)?)?)),
1262 "fetchTree" if args.len() == 1 => Ok(Some(eval_fetch_tree(arg(0)?)?)),
1263
1264 "foldr" if args.len() == 3 => {
1266 let func = arg(0)?;
1267 let init = arg(1)?;
1268 let list = expect_list(arg(2)?, "foldr list")?;
1269 let mut acc = init;
1270 for item in list.into_iter().rev() {
1271 acc = apply_value_to_two(
1272 ast,
1273 func.clone(),
1274 item,
1275 acc,
1276 env,
1277 depth + 1,
1278 )?;
1279 }
1280 Ok(Some(acc))
1281 }
1282
1283 _ => Ok(None),
1284 }
1285}
1286
1287fn apply_value_to_one(
1293 ast: &AstGraph,
1294 callable: EvalValue,
1295 arg: EvalValue,
1296 _caller_env: &EvalEnv,
1297 depth: u32,
1298) -> Result<EvalValue, EvalError> {
1299 match callable {
1300 EvalValue::Closure {
1301 param,
1302 body_node_id,
1303 captured_env,
1304 } => {
1305 let mut call_env = EvalEnv {
1306 bindings: captured_env,
1307 };
1308 call_env.bindings.insert(param, arg);
1309 eval_at(ast, body_node_id, &call_env, depth)
1310 }
1311 EvalValue::PatternClosure {
1312 formals,
1313 accepts_extra,
1314 binding_name,
1315 body_node_id,
1316 captured_env,
1317 } => {
1318 let arg_map = match arg {
1319 EvalValue::AttrSet(m) => m,
1320 other => {
1321 return Err(EvalError::TypeMismatch {
1322 context: "pattern-closure arg",
1323 expected: "attrset",
1324 got: value_kind(&other),
1325 });
1326 }
1327 };
1328 let mut call_env = EvalEnv {
1329 bindings: captured_env,
1330 };
1331 for formal in &formals {
1332 if let Some(v) = arg_map.get(&formal.name) {
1333 call_env.bindings.insert(formal.name.clone(), v.clone());
1334 } else if let Some(default_node) = formal.default_node_id {
1335 let d = eval_at(ast, default_node, &call_env, depth)?;
1336 call_env.bindings.insert(formal.name.clone(), d);
1337 } else {
1338 return Err(EvalError::TypeMismatch {
1339 context: "pattern-closure missing required arg",
1340 expected: "formal arg without default",
1341 got: "missing key",
1342 });
1343 }
1344 }
1345 if !accepts_extra {
1346 let known: std::collections::HashSet<&str> =
1347 formals.iter().map(|f| f.name.as_str()).collect();
1348 for k in arg_map.keys() {
1349 if !known.contains(k.as_str()) {
1350 return Err(EvalError::TypeMismatch {
1351 context: "pattern-closure extra arg",
1352 expected: "only declared formals",
1353 got: "extra key",
1354 });
1355 }
1356 }
1357 }
1358 if let Some(name) = binding_name {
1359 call_env
1360 .bindings
1361 .insert(name, EvalValue::AttrSet(arg_map));
1362 }
1363 eval_at(ast, body_node_id, &call_env, depth)
1364 }
1365 other => Err(EvalError::TypeMismatch {
1366 context: "apply_value_to_one",
1367 expected: "callable",
1368 got: value_kind(&other),
1369 }),
1370 }
1371}
1372
1373fn apply_value_to_two(
1377 ast: &AstGraph,
1378 callable: EvalValue,
1379 a: EvalValue,
1380 b: EvalValue,
1381 caller_env: &EvalEnv,
1382 depth: u32,
1383) -> Result<EvalValue, EvalError> {
1384 let after_first = apply_value_to_one(ast, callable, a, caller_env, depth)?;
1385 apply_value_to_one(ast, after_first, b, caller_env, depth)
1386}
1387
1388fn synthetic_value_node(_ast: &AstGraph) -> NodeId {
1392 0
1393}
1394
1395fn expect_int(v: EvalValue, ctx: &'static str) -> Result<i64, EvalError> {
1396 match v {
1397 EvalValue::Int(n) => Ok(n),
1398 other => Err(EvalError::TypeMismatch {
1399 context: ctx,
1400 expected: "int",
1401 got: value_kind(&other),
1402 }),
1403 }
1404}
1405
1406fn expect_str(v: EvalValue, ctx: &'static str) -> Result<String, EvalError> {
1407 match v {
1408 EvalValue::Str(s) => Ok(s),
1409 other => Err(EvalError::TypeMismatch {
1410 context: ctx,
1411 expected: "string",
1412 got: value_kind(&other),
1413 }),
1414 }
1415}
1416
1417fn expect_str_or_path(v: EvalValue, ctx: &'static str) -> Result<String, EvalError> {
1418 match v {
1419 EvalValue::Str(s) | EvalValue::Path(s) => Ok(s),
1420 other => Err(EvalError::TypeMismatch {
1421 context: ctx,
1422 expected: "string or path",
1423 got: value_kind(&other),
1424 }),
1425 }
1426}
1427
1428fn expect_list(v: EvalValue, ctx: &'static str) -> Result<Vec<EvalValue>, EvalError> {
1429 match v {
1430 EvalValue::List(items) => Ok(items),
1431 other => Err(EvalError::TypeMismatch {
1432 context: ctx,
1433 expected: "list",
1434 got: value_kind(&other),
1435 }),
1436 }
1437}
1438
1439fn expect_list_of_str(v: EvalValue, ctx: &'static str) -> Result<Vec<String>, EvalError> {
1440 let items = expect_list(v, ctx)?;
1441 items
1442 .into_iter()
1443 .map(|v| expect_str(v, ctx))
1444 .collect()
1445}
1446
1447fn expect_attrset(
1448 v: EvalValue,
1449 ctx: &'static str,
1450) -> Result<BTreeMap<String, EvalValue>, EvalError> {
1451 match v {
1452 EvalValue::AttrSet(m) => Ok(m),
1453 other => Err(EvalError::TypeMismatch {
1454 context: ctx,
1455 expected: "attrset",
1456 got: value_kind(&other),
1457 }),
1458 }
1459}
1460
1461fn leak_msg(s: String) -> &'static str {
1465 Box::leak(s.into_boxed_str())
1466}
1467
1468fn builtin_to_string(v: EvalValue) -> EvalValue {
1470 match v {
1471 EvalValue::Str(s) => EvalValue::Str(s),
1472 EvalValue::Int(n) => EvalValue::Str(n.to_string()),
1473 EvalValue::Float(f) => EvalValue::Str(f.to_string()),
1474 EvalValue::Bool(b) => EvalValue::Str(if b { "1" } else { "" }.to_string()),
1475 EvalValue::Null => EvalValue::Str(String::new()),
1476 EvalValue::Path(p) => EvalValue::Str(p),
1477 EvalValue::List(items) => {
1478 let parts: Vec<String> = items
1479 .into_iter()
1480 .map(|i| match builtin_to_string(i) {
1481 EvalValue::Str(s) => s,
1482 _ => String::new(),
1483 })
1484 .collect();
1485 EvalValue::Str(parts.join(" "))
1486 }
1487 EvalValue::Derivation { out_path, .. } => EvalValue::Str(out_path),
1490 EvalValue::AttrSet(_)
1493 | EvalValue::Closure { .. }
1494 | EvalValue::PatternClosure { .. }
1495 | EvalValue::Builtin { .. }
1496 | EvalValue::Opaque { .. } => EvalValue::Str(String::new()),
1497 }
1498}
1499
1500fn coerce_to_env_value(v: &EvalValue, key: &str) -> Result<String, EvalError> {
1516 Ok(match v {
1517 EvalValue::Str(s) => s.clone(),
1518 EvalValue::Path(p) => p.clone(),
1519 EvalValue::Int(n) => n.to_string(),
1520 EvalValue::Float(f) => f.to_string(),
1521 EvalValue::Bool(b) => if *b { "1" } else { "" }.to_string(),
1522 EvalValue::Null => String::new(),
1523 EvalValue::Derivation { out_path, .. } => out_path.clone(),
1524 EvalValue::List(items) => {
1525 let mut parts: Vec<String> = Vec::with_capacity(items.len());
1526 for it in items {
1527 parts.push(coerce_to_env_value(it, key)?);
1528 }
1529 parts.join(" ")
1530 }
1531 other => {
1532 return Err(EvalError::TypeMismatch {
1533 context: leak_msg(format!("derivation env coercion for `{key}`")),
1534 expected: "string-coercible (str/path/int/float/bool/null/list/derivation)",
1535 got: value_kind(other),
1536 });
1537 }
1538 })
1539}
1540
1541fn eval_derivation(arg: EvalValue) -> Result<EvalValue, EvalError> {
1546 let attrs = expect_attrset(arg, "derivation arg")?;
1547
1548 let name = attrs
1549 .get("name")
1550 .ok_or(EvalError::TypeMismatch {
1551 context: "derivation",
1552 expected: "attrset with `name`",
1553 got: "missing `name`",
1554 })
1555 .and_then(|v| expect_str(v.clone(), "derivation.name"))?;
1556
1557 let system = attrs
1558 .get("system")
1559 .ok_or(EvalError::TypeMismatch {
1560 context: "derivation",
1561 expected: "attrset with `system`",
1562 got: "missing `system`",
1563 })
1564 .and_then(|v| expect_str(v.clone(), "derivation.system"))?;
1565
1566 let builder_value = attrs.get("builder").ok_or(EvalError::TypeMismatch {
1567 context: "derivation",
1568 expected: "attrset with `builder`",
1569 got: "missing `builder`",
1570 })?;
1571 let builder = expect_str_or_path(builder_value.clone(), "derivation.builder")?;
1572
1573 let args_list: Vec<String> = match attrs.get("args") {
1574 Some(v) => expect_list_of_str(v.clone(), "derivation.args")?,
1575 None => Vec::new(),
1576 };
1577
1578 let outputs: Vec<String> = match attrs.get("outputs") {
1579 Some(v) => expect_list_of_str(v.clone(), "derivation.outputs")?,
1580 None => vec!["out".to_string()],
1581 };
1582
1583 let mut drv = sui_compat::derivation::Derivation {
1585 outputs: std::collections::BTreeMap::new(),
1586 input_derivations: std::collections::BTreeMap::new(),
1587 input_sources: Vec::new(),
1588 system: system.clone(),
1589 builder,
1590 args: args_list,
1591 env: std::collections::BTreeMap::new(),
1592 };
1593
1594 let structural: std::collections::HashSet<&str> = [
1599 "name", "system", "builder", "args", "outputs",
1600 ].into_iter().collect();
1605
1606 for (k, v) in &attrs {
1607 if structural.contains(k.as_str()) {
1608 continue;
1609 }
1610 let s = coerce_to_env_value(v, k)?;
1611 drv.env.insert(k.clone(), s);
1612 }
1613 drv.env.insert("name".to_string(), name.clone());
1615 drv.env.insert("system".to_string(), system.clone());
1616 drv.env.insert("builder".to_string(), drv.builder.clone());
1617
1618 for (_, v) in &attrs {
1624 if let EvalValue::Derivation { drv_path, .. } = v {
1625 drv.input_derivations
1626 .entry(drv_path.clone())
1627 .or_default()
1628 .push("out".to_string());
1629 }
1630 }
1631
1632 let algo = crate::derivation::load_canonical().map_err(|e| EvalError::TypeMismatch {
1633 context: "derivation algorithm load",
1634 expected: "canonical input-addressed algorithm",
1635 got: leak_msg(e.to_string()),
1636 })?;
1637
1638 let (drv_path, out_paths, _final_drv) =
1639 crate::derivation::apply(&algo, drv, outputs.clone(), &name).map_err(|e| {
1640 EvalError::TypeMismatch {
1641 context: "derivation algorithm apply",
1642 expected: "successful input-addressed pipeline",
1643 got: leak_msg(e.to_string()),
1644 }
1645 })?;
1646
1647 let primary_out = outputs.first().map(String::as_str).unwrap_or("out");
1649 let out_path = out_paths
1650 .get(primary_out)
1651 .cloned()
1652 .ok_or(EvalError::TypeMismatch {
1653 context: "derivation outputs",
1654 expected: "first output materialized",
1655 got: "no path computed",
1656 })?;
1657
1658 let mut result_attrs = attrs.clone();
1662 for (name, path) in &out_paths {
1663 result_attrs.insert(name.clone(), EvalValue::Str(path.clone()));
1664 }
1665 result_attrs.insert("outPath".to_string(), EvalValue::Str(out_path.clone()));
1666 result_attrs.insert("drvPath".to_string(), EvalValue::Str(drv_path.clone()));
1667 result_attrs.insert("type".to_string(), EvalValue::Str("derivation".to_string()));
1668
1669 Ok(EvalValue::Derivation {
1670 name,
1671 system,
1672 drv_path,
1673 out_path,
1674 attrs: result_attrs,
1675 })
1676}
1677
1678struct InProcessFetcherEnv {
1691 store_root: std::path::PathBuf,
1694 transport: crate::fetcher::SchemeRouter<UreqTransport>,
1695}
1696
1697impl InProcessFetcherEnv {
1698 fn new(store_root: std::path::PathBuf) -> Self {
1699 Self {
1700 store_root,
1701 transport: crate::fetcher::SchemeRouter::new(UreqTransport),
1702 }
1703 }
1704}
1705
1706impl crate::fetcher::FetcherEnvironment for InProcessFetcherEnv {
1707 fn fetch_bytes(&self, url: &str) -> Result<Vec<u8>, String> {
1708 use crate::fetcher::HttpTransport;
1709 self.transport.get(url).map_err(|e| e.to_string())
1710 }
1711
1712 fn hash_bytes(&self, bytes: &[u8]) -> String {
1713 use sha2::{Digest, Sha256};
1714 let d = Sha256::digest(bytes);
1715 let hex: String = d.iter().map(|b| format!("{b:02x}")).collect();
1716 format!("sha256:{hex}")
1717 }
1718
1719 fn write_to_store(&self, name: &str, bytes: &[u8]) -> Result<String, String> {
1720 use sha2::{Digest, Sha256};
1723 let inner = Sha256::digest(bytes);
1724 let hex: String = inner.iter().map(|b| format!("{b:02x}")).collect();
1725 let path = sui_compat::store_path::compute_fixed_output_hash(
1726 "sha256",
1727 &hex,
1728 false, name,
1730 );
1731 let basename = std::path::Path::new(&path)
1734 .file_name()
1735 .ok_or_else(|| format!("bad store path `{path}`"))?;
1736 let dst = self.store_root.join(basename);
1737 std::fs::write(&dst, bytes).map_err(|e| e.to_string())?;
1738 Ok(path)
1739 }
1740}
1741
1742struct UreqTransport;
1746
1747impl crate::fetcher::HttpTransport for UreqTransport {
1748 fn get(&self, url: &str) -> Result<Vec<u8>, crate::fetcher::HttpError> {
1749 let resp = ureq::get(url)
1750 .call()
1751 .map_err(|e| crate::fetcher::HttpError::NetworkFailure(e.to_string()))?;
1752 let mut body = Vec::new();
1753 use std::io::Read;
1754 resp.into_body()
1755 .into_reader()
1756 .read_to_end(&mut body)
1757 .map_err(|e| crate::fetcher::HttpError::BodyReadFailure(e.to_string()))?;
1758 Ok(body)
1759 }
1760}
1761
1762fn default_store_root() -> std::path::PathBuf {
1767 if let Ok(p) = std::env::var("SUI_TEST_STORE_DIR") {
1768 return std::path::PathBuf::from(p);
1769 }
1770 std::path::PathBuf::from("/nix/store")
1771}
1772
1773fn eval_fetchurl(arg: EvalValue) -> Result<EvalValue, EvalError> {
1776 let attrs = match arg {
1777 EvalValue::Str(s) => {
1779 let mut m = BTreeMap::new();
1780 m.insert("url".to_string(), EvalValue::Str(s));
1781 m
1782 }
1783 EvalValue::AttrSet(m) => m,
1784 other => {
1785 return Err(EvalError::TypeMismatch {
1786 context: "fetchurl arg",
1787 expected: "string or attrset",
1788 got: value_kind(&other),
1789 });
1790 }
1791 };
1792
1793 let url = attrs
1794 .get("url")
1795 .ok_or(EvalError::TypeMismatch {
1796 context: "fetchurl",
1797 expected: "url",
1798 got: "missing url",
1799 })
1800 .and_then(|v| expect_str_or_path(v.clone(), "fetchurl.url"))?;
1801
1802 let declared_hash = match attrs.get("sha256").or_else(|| attrs.get("hash")) {
1803 Some(EvalValue::Str(s)) => Some(format!("sha256:{}", s.trim_start_matches("sha256:"))),
1804 _ => None,
1805 };
1806
1807 let name = match attrs.get("name") {
1808 Some(EvalValue::Str(s)) => s.clone(),
1809 _ => url.rsplit('/').next().unwrap_or("download").to_string(),
1810 };
1811
1812 let env = InProcessFetcherEnv::new(default_store_root());
1813 let spec = crate::fetcher::load_named("fetchurl").map_err(|e| EvalError::TypeMismatch {
1814 context: "fetchurl spec load",
1815 expected: "fetchurl in canonical fetchers",
1816 got: leak_msg(e.to_string()),
1817 })?;
1818 let outcome = crate::fetcher::apply(
1819 &spec,
1820 &crate::fetcher::FetchArgs {
1821 url: url.clone(),
1822 declared_hash,
1823 name_hint: Some(name.clone()),
1824 },
1825 &env,
1826 )
1827 .map_err(|e| EvalError::TypeMismatch {
1828 context: "fetchurl apply",
1829 expected: "successful fetch",
1830 got: leak_msg(e.to_string()),
1831 })?;
1832
1833 Ok(EvalValue::Str(outcome.store_path))
1834}
1835
1836fn eval_fetch_tarball(arg: EvalValue) -> Result<EvalValue, EvalError> {
1840 let attrs = match arg {
1841 EvalValue::Str(s) => {
1842 let mut m = BTreeMap::new();
1843 m.insert("url".to_string(), EvalValue::Str(s));
1844 m
1845 }
1846 EvalValue::AttrSet(m) => m,
1847 other => {
1848 return Err(EvalError::TypeMismatch {
1849 context: "fetchTarball arg",
1850 expected: "string or attrset",
1851 got: value_kind(&other),
1852 });
1853 }
1854 };
1855
1856 let url = attrs
1857 .get("url")
1858 .ok_or(EvalError::TypeMismatch {
1859 context: "fetchTarball",
1860 expected: "url",
1861 got: "missing url",
1862 })
1863 .and_then(|v| expect_str_or_path(v.clone(), "fetchTarball.url"))?;
1864 let name = match attrs.get("name") {
1865 Some(EvalValue::Str(s)) => s.clone(),
1866 _ => "source".to_string(),
1867 };
1868
1869 let transport = crate::fetcher::SchemeRouter::new(UreqTransport);
1871 use crate::fetcher::HttpTransport;
1872 let bytes = transport.get(&url).map_err(|e| EvalError::TypeMismatch {
1873 context: "fetchTarball http",
1874 expected: "200 OK with body",
1875 got: leak_msg(e.to_string()),
1876 })?;
1877
1878 let tmp = tempfile_dir(&name)?;
1882 extract_tarball(&bytes, &tmp, &url)?;
1883
1884 use sha2::{Digest, Sha256};
1889 let mut h = Sha256::new();
1890 h.update(&bytes);
1891 let inner = h.finalize();
1892 let hex: String = inner.iter().map(|b| format!("{b:02x}")).collect();
1893 let path = sui_compat::store_path::compute_fixed_output_hash(
1894 "sha256", &hex, true, &name,
1896 );
1897
1898 Ok(EvalValue::Str(path))
1899}
1900
1901fn eval_fetch_git(arg: EvalValue) -> Result<EvalValue, EvalError> {
1905 let attrs = match arg {
1906 EvalValue::Str(s) => {
1907 let mut m = BTreeMap::new();
1908 m.insert("url".to_string(), EvalValue::Str(s));
1909 m
1910 }
1911 EvalValue::AttrSet(m) => m,
1912 other => {
1913 return Err(EvalError::TypeMismatch {
1914 context: "fetchGit arg",
1915 expected: "string or attrset",
1916 got: value_kind(&other),
1917 });
1918 }
1919 };
1920 let url = attrs
1921 .get("url")
1922 .ok_or(EvalError::TypeMismatch {
1923 context: "fetchGit",
1924 expected: "url",
1925 got: "missing url",
1926 })
1927 .and_then(|v| expect_str_or_path(v.clone(), "fetchGit.url"))?;
1928 let name = match attrs.get("name") {
1929 Some(EvalValue::Str(s)) => s.clone(),
1930 _ => "source".to_string(),
1931 };
1932
1933 let rev = match attrs.get("rev") {
1939 Some(EvalValue::Str(s)) => Some(s.clone()),
1940 _ => None,
1941 };
1942 let reff = match attrs.get("ref") {
1943 Some(EvalValue::Str(s)) => Some(s.clone()),
1944 _ => None,
1945 };
1946
1947 use sha2::{Digest, Sha256};
1948 let mut h = Sha256::new();
1949 h.update(url.as_bytes());
1950 if let Some(r) = &rev { h.update(r.as_bytes()); }
1951 if let Some(r) = &reff { h.update(r.as_bytes()); }
1952 let inner = h.finalize();
1953 let hex: String = inner.iter().map(|b| format!("{b:02x}")).collect();
1954 let path = sui_compat::store_path::compute_fixed_output_hash(
1955 "sha256", &hex, true, &name,
1957 );
1958 Ok(EvalValue::Str(path))
1959}
1960
1961fn eval_fetch_tree(arg: EvalValue) -> Result<EvalValue, EvalError> {
1964 let attrs = expect_attrset(arg, "fetchTree arg")?;
1965 let ty = attrs
1966 .get("type")
1967 .ok_or(EvalError::TypeMismatch {
1968 context: "fetchTree",
1969 expected: "attrset with `type`",
1970 got: "missing `type`",
1971 })
1972 .and_then(|v| expect_str(v.clone(), "fetchTree.type"))?;
1973 match ty.as_str() {
1974 "tarball" | "file" => eval_fetch_tarball(EvalValue::AttrSet(attrs)),
1975 "git" | "github" | "gitlab" | "sourcehut" => eval_fetch_git(EvalValue::AttrSet(attrs)),
1976 other => Err(EvalError::TypeMismatch {
1977 context: "fetchTree.type dispatch",
1978 expected: "tarball/file/git/github/gitlab/sourcehut",
1979 got: leak_msg(format!("unsupported `{other}`")),
1980 }),
1981 }
1982}
1983
1984fn tempfile_dir(name: &str) -> Result<std::path::PathBuf, EvalError> {
1988 let base = std::env::temp_dir();
1989 let nanos = std::time::SystemTime::now()
1990 .duration_since(std::time::UNIX_EPOCH)
1991 .map(|d| d.subsec_nanos())
1992 .unwrap_or(0);
1993 let path = base.join(format!("sui-fetch-{name}-{nanos}"));
1994 std::fs::create_dir_all(&path).map_err(|e| EvalError::TypeMismatch {
1995 context: "fetcher tmpdir",
1996 expected: "writable tmpdir",
1997 got: leak_msg(e.to_string()),
1998 })?;
1999 Ok(path)
2000}
2001
2002fn extract_tarball(
2006 bytes: &[u8],
2007 dst: &std::path::Path,
2008 url_for_err: &str,
2009) -> Result<(), EvalError> {
2010 use std::io::Cursor;
2011 if bytes.len() >= 2 && bytes[0] == 0x1f && bytes[1] == 0x8b {
2012 let gz = flate2::read::GzDecoder::new(Cursor::new(bytes));
2014 let mut ar = tar::Archive::new(gz);
2015 ar.unpack(dst).map_err(|e| EvalError::TypeMismatch {
2016 context: "tarball gunzip+untar",
2017 expected: "valid .tar.gz",
2018 got: leak_msg(format!("{url_for_err}: {e}")),
2019 })?;
2020 } else {
2021 let mut ar = tar::Archive::new(Cursor::new(bytes));
2022 ar.unpack(dst).map_err(|e| EvalError::TypeMismatch {
2023 context: "tarball untar",
2024 expected: "valid .tar",
2025 got: leak_msg(format!("{url_for_err}: {e}")),
2026 })?;
2027 }
2028 Ok(())
2029}
2030
2031fn eval_string_segments(
2032 ast: &AstGraph,
2033 segments: &[StrSegment],
2034 env: &EvalEnv,
2035 depth: u32,
2036 fallback_id: NodeId,
2037) -> Result<EvalValue, EvalError> {
2038 let mut out = String::new();
2039 for s in segments {
2040 match s {
2041 StrSegment::Literal(t) => out.push_str(t),
2042 StrSegment::Interpolation(child) => {
2043 let v = eval_at(ast, *child, env, depth)?;
2044 match v {
2045 EvalValue::Str(s) => out.push_str(&s),
2046 EvalValue::Int(n) => out.push_str(&n.to_string()),
2047 EvalValue::Float(f) => out.push_str(&f.to_string()),
2048 EvalValue::Bool(b) => out.push_str(if b { "1" } else { "" }),
2049 EvalValue::Path(p) => out.push_str(&p),
2050 EvalValue::Derivation { out_path, .. } => out.push_str(&out_path),
2054 _ => {
2059 return Ok(EvalValue::Opaque {
2060 kind: "Str-with-complex-interp".to_string(),
2061 node_id: fallback_id,
2062 });
2063 }
2064 }
2065 }
2066 }
2067 }
2068 Ok(EvalValue::Str(out))
2069}
2070
2071fn eval_select(
2072 ast: &AstGraph,
2073 target: NodeId,
2074 path: &[String],
2075 fallback: Option<NodeId>,
2076 env: &EvalEnv,
2077 depth: u32,
2078) -> Result<EvalValue, EvalError> {
2079 let base = eval_at(ast, target, env, depth)?;
2080 let result = follow_path(&base, path);
2081 match result {
2082 Some(v) => Ok(v),
2083 None => {
2084 if let Some(fb) = fallback {
2085 eval_at(ast, fb, env, depth)
2086 } else {
2087 Err(EvalError::ConfigMiss { path: path.to_vec() })
2088 }
2089 }
2090 }
2091}
2092
2093fn eval_has_attr(
2094 ast: &AstGraph,
2095 target: NodeId,
2096 path: &[String],
2097 env: &EvalEnv,
2098 depth: u32,
2099) -> Result<EvalValue, EvalError> {
2100 let base = eval_at(ast, target, env, depth)?;
2101 Ok(EvalValue::Bool(follow_path(&base, path).is_some()))
2102}
2103
2104fn follow_path(value: &EvalValue, path: &[String]) -> Option<EvalValue> {
2105 let mut cursor = value.clone();
2106 for step in path {
2107 match cursor {
2108 EvalValue::AttrSet(map) => match map.get(step) {
2109 Some(v) => cursor = v.clone(),
2110 None => return None,
2111 },
2112 EvalValue::Derivation { name, system, drv_path, out_path, attrs } => {
2118 match step.as_str() {
2119 "name" => cursor = EvalValue::Str(name.clone()),
2120 "system" => cursor = EvalValue::Str(system.clone()),
2121 "drvPath" => cursor = EvalValue::Str(drv_path.clone()),
2122 "outPath" | "out" => cursor = EvalValue::Str(out_path.clone()),
2123 "type" => cursor = EvalValue::Str("derivation".to_string()),
2124 other => match attrs.get(other) {
2125 Some(v) => cursor = v.clone(),
2126 None => return None,
2127 },
2128 }
2129 }
2130 _ => return None,
2131 }
2132 }
2133 Some(cursor)
2134}
2135
2136fn insert_at_path(
2137 out: &mut BTreeMap<String, EvalValue>,
2138 path: &[String],
2139 value: EvalValue,
2140) {
2141 if path.is_empty() {
2142 return;
2143 }
2144 if path.len() == 1 {
2145 out.insert(path[0].clone(), value);
2146 return;
2147 }
2148 let head = &path[0];
2149 let tail = &path[1..];
2150 let entry = out
2151 .entry(head.clone())
2152 .or_insert_with(|| EvalValue::AttrSet(BTreeMap::new()));
2153 if let EvalValue::AttrSet(inner) = entry {
2154 insert_at_path(inner, tail, value);
2155 }
2156}
2157
2158fn eval_binop(op: BinaryOp, l: EvalValue, r: EvalValue) -> Result<EvalValue, EvalError> {
2159 use EvalValue::*;
2160 match (op, &l, &r) {
2161 (BinaryOp::Add, Int(a), Int(b)) => Ok(Int(a + b)),
2163 (BinaryOp::Sub, Int(a), Int(b)) => Ok(Int(a - b)),
2164 (BinaryOp::Mul, Int(a), Int(b)) => Ok(Int(a * b)),
2165 (BinaryOp::Div, Int(_), Int(0)) => Err(EvalError::DivisionByZero),
2166 (BinaryOp::Div, Int(a), Int(b)) => Ok(Int(a / b)),
2167 (BinaryOp::Add, Float(a), Float(b)) => Ok(Float(a + b)),
2169 (BinaryOp::Sub, Float(a), Float(b)) => Ok(Float(a - b)),
2170 (BinaryOp::Mul, Float(a), Float(b)) => Ok(Float(a * b)),
2171 (BinaryOp::Div, Float(a), Float(b)) => Ok(Float(a / b)),
2172 (BinaryOp::Add, Str(a), Str(b)) => Ok(Str(format!("{a}{b}"))),
2174 (BinaryOp::Eq, _, _) => Ok(Bool(l == r)),
2176 (BinaryOp::NotEq, _, _) => Ok(Bool(l != r)),
2177 (BinaryOp::Lt, Int(a), Int(b)) => Ok(Bool(a < b)),
2179 (BinaryOp::Le, Int(a), Int(b)) => Ok(Bool(a <= b)),
2180 (BinaryOp::Gt, Int(a), Int(b)) => Ok(Bool(a > b)),
2181 (BinaryOp::Ge, Int(a), Int(b)) => Ok(Bool(a >= b)),
2182 (BinaryOp::Lt, Float(a), Float(b)) => Ok(Bool(a < b)),
2183 (BinaryOp::Le, Float(a), Float(b)) => Ok(Bool(a <= b)),
2184 (BinaryOp::Gt, Float(a), Float(b)) => Ok(Bool(a > b)),
2185 (BinaryOp::Ge, Float(a), Float(b)) => Ok(Bool(a >= b)),
2186 (BinaryOp::And, Bool(a), Bool(b)) => Ok(Bool(*a && *b)),
2190 (BinaryOp::Or, Bool(a), Bool(b)) => Ok(Bool(*a || *b)),
2191 (BinaryOp::Implies, Bool(a), Bool(b)) => Ok(Bool(!a || *b)),
2192 (BinaryOp::Concat, List(a), List(b)) => {
2194 let mut out = a.clone();
2195 out.extend_from_slice(b);
2196 Ok(List(out))
2197 }
2198 (BinaryOp::Update, AttrSet(a), AttrSet(b)) => {
2200 let mut merged = a.clone();
2201 for (k, v) in b {
2202 merged.insert(k.clone(), v.clone());
2203 }
2204 Ok(AttrSet(merged))
2205 }
2206 _ => Err(EvalError::TypeMismatch {
2209 context: "binop",
2210 expected: "numeric / string / list / attrset / bool match",
2211 got: value_kind(&l),
2212 }),
2213 }
2214}
2215
2216fn eval_unaryop(op: UnaryOp, v: EvalValue) -> Result<EvalValue, EvalError> {
2217 match (op, &v) {
2218 (UnaryOp::Neg, EvalValue::Int(n)) => Ok(EvalValue::Int(-n)),
2219 (UnaryOp::Neg, EvalValue::Float(f)) => Ok(EvalValue::Float(-f)),
2220 (UnaryOp::Not, EvalValue::Bool(b)) => Ok(EvalValue::Bool(!b)),
2221 _ => Err(EvalError::TypeMismatch {
2222 context: "unary op",
2223 expected: "numeric (for -) or bool (for !)",
2224 got: value_kind(&v),
2225 }),
2226 }
2227}
2228
2229fn value_kind(v: &EvalValue) -> &'static str {
2230 match v {
2231 EvalValue::Int(_) => "Int",
2232 EvalValue::Float(_) => "Float",
2233 EvalValue::Bool(_) => "Bool",
2234 EvalValue::Null => "Null",
2235 EvalValue::Str(_) => "Str",
2236 EvalValue::Path(_) => "Path",
2237 EvalValue::List(_) => "List",
2238 EvalValue::AttrSet(_) => "AttrSet",
2239 EvalValue::Closure { .. } => "Closure",
2240 EvalValue::PatternClosure { .. } => "PatternClosure",
2241 EvalValue::Builtin { .. } => "Builtin",
2242 EvalValue::Derivation { .. } => "Derivation",
2243 EvalValue::Opaque { .. } => "Opaque",
2244 }
2245}
2246
2247#[cfg(test)]
2248mod tests {
2249 use super::*;
2250 use crate::ast_graph::AstGraph;
2251 use pretty_assertions::assert_eq;
2252
2253 fn eval(source: &str) -> EvalValue {
2254 let g = AstGraph::from_source(source).expect("parse");
2255 eval_node(&g, g.root_id, &EvalEnv::new()).expect("eval")
2256 }
2257
2258 fn try_eval(source: &str) -> Result<EvalValue, EvalError> {
2259 let g = AstGraph::from_source(source).expect("parse");
2260 eval_node(&g, g.root_id, &EvalEnv::new())
2261 }
2262
2263 #[test]
2264 fn int_and_float_literals() {
2265 assert_eq!(eval("42"), EvalValue::Int(42));
2266 assert_eq!(eval("3.14"), EvalValue::Float(3.14));
2267 }
2268
2269 #[test]
2279 fn special_idents_resolve_to_typed_literals() {
2280 let g = AstGraph::from_source("true").expect("parse");
2283 assert_eq!(
2284 eval_node(&g, g.root_id, &EvalEnv::new()).unwrap(),
2285 EvalValue::Bool(true)
2286 );
2287 let g = AstGraph::from_source("false").expect("parse");
2288 assert_eq!(
2289 eval_node(&g, g.root_id, &EvalEnv::new()).unwrap(),
2290 EvalValue::Bool(false)
2291 );
2292 let g = AstGraph::from_source("null").expect("parse");
2293 assert_eq!(
2294 eval_node(&g, g.root_id, &EvalEnv::new()).unwrap(),
2295 EvalValue::Null
2296 );
2297 }
2298
2299 #[test]
2300 fn undefined_ident_errors() {
2301 let g = AstGraph::from_source("notDefined").expect("parse");
2302 let err = eval_node(&g, g.root_id, &EvalEnv::new()).unwrap_err();
2303 assert!(matches!(err, EvalError::UndefinedIdent(ref n) if n == "notDefined"));
2304 }
2305
2306 #[test]
2307 fn env_binding_resolves_ident() {
2308 let g = AstGraph::from_source("x").expect("parse");
2309 let env = EvalEnv::new().with_binding("x", EvalValue::Int(7));
2310 assert_eq!(
2311 eval_node(&g, g.root_id, &env).unwrap(),
2312 EvalValue::Int(7)
2313 );
2314 }
2315
2316 #[test]
2317 fn arithmetic() {
2318 assert_eq!(eval("1 + 2"), EvalValue::Int(3));
2319 assert_eq!(eval("5 - 2"), EvalValue::Int(3));
2320 assert_eq!(eval("4 * 3"), EvalValue::Int(12));
2321 assert_eq!(eval("12 / 4"), EvalValue::Int(3));
2322 }
2323
2324 #[test]
2325 fn division_by_zero_is_typed_error() {
2326 let err = try_eval("1 / 0").unwrap_err();
2327 assert!(matches!(err, EvalError::DivisionByZero));
2328 }
2329
2330 #[test]
2331 fn comparison_and_equality() {
2332 assert_eq!(eval("1 == 1"), EvalValue::Bool(true));
2333 assert_eq!(eval("1 == 2"), EvalValue::Bool(false));
2334 assert_eq!(eval("1 != 2"), EvalValue::Bool(true));
2335 assert_eq!(eval("1 < 2"), EvalValue::Bool(true));
2336 assert_eq!(eval("2 <= 2"), EvalValue::Bool(true));
2337 assert_eq!(eval("3 > 2"), EvalValue::Bool(true));
2338 assert_eq!(eval("3 >= 3"), EvalValue::Bool(true));
2339 }
2340
2341 #[test]
2342 fn string_literal_and_concat() {
2343 assert_eq!(eval("\"hello\""), EvalValue::Str("hello".into()));
2344 assert_eq!(
2345 eval("\"hello \" + \"world\""),
2346 EvalValue::Str("hello world".into())
2347 );
2348 }
2349
2350 #[test]
2351 fn list_construction_and_concat() {
2352 assert_eq!(
2353 eval("[1 2 3]"),
2354 EvalValue::List(vec![EvalValue::Int(1), EvalValue::Int(2), EvalValue::Int(3)])
2355 );
2356 assert_eq!(
2357 eval("[1] ++ [2 3]"),
2358 EvalValue::List(vec![EvalValue::Int(1), EvalValue::Int(2), EvalValue::Int(3)])
2359 );
2360 }
2361
2362 #[test]
2363 fn attrset_construction_via_dotted_paths() {
2364 let v = eval("{ a.b = 1; a.c = 2; }");
2365 match v {
2366 EvalValue::AttrSet(map) => {
2367 if let Some(EvalValue::AttrSet(inner)) = map.get("a") {
2368 assert_eq!(inner.get("b"), Some(&EvalValue::Int(1)));
2369 assert_eq!(inner.get("c"), Some(&EvalValue::Int(2)));
2370 } else {
2371 panic!("expected nested attrset under 'a'");
2372 }
2373 }
2374 _ => panic!("expected attrset"),
2375 }
2376 }
2377
2378 #[test]
2379 fn attrset_select_with_dotted_path() {
2380 let g = AstGraph::from_source("x.a.b").expect("parse");
2381 let env = EvalEnv::new().with_binding(
2382 "x",
2383 EvalValue::AttrSet(BTreeMap::from([(
2384 "a".to_string(),
2385 EvalValue::AttrSet(BTreeMap::from([(
2386 "b".to_string(),
2387 EvalValue::Int(42),
2388 )])),
2389 )])),
2390 );
2391 assert_eq!(
2392 eval_node(&g, g.root_id, &env).unwrap(),
2393 EvalValue::Int(42)
2394 );
2395 }
2396
2397 #[test]
2398 fn select_with_fallback_when_missing() {
2399 let g = AstGraph::from_source("x.missing or 99").expect("parse");
2400 let env = EvalEnv::new().with_binding(
2401 "x",
2402 EvalValue::AttrSet(BTreeMap::from([("present".to_string(), EvalValue::Int(1))])),
2403 );
2404 assert_eq!(
2405 eval_node(&g, g.root_id, &env).unwrap(),
2406 EvalValue::Int(99)
2407 );
2408 }
2409
2410 #[test]
2411 fn has_attr() {
2412 let g = AstGraph::from_source("x ? a").expect("parse");
2413 let env = EvalEnv::new().with_binding(
2414 "x",
2415 EvalValue::AttrSet(BTreeMap::from([("a".to_string(), EvalValue::Int(1))])),
2416 );
2417 assert_eq!(
2418 eval_node(&g, g.root_id, &env).unwrap(),
2419 EvalValue::Bool(true)
2420 );
2421
2422 let env = EvalEnv::new().with_binding(
2423 "x",
2424 EvalValue::AttrSet(BTreeMap::from([("b".to_string(), EvalValue::Int(1))])),
2425 );
2426 assert_eq!(
2427 eval_node(&g, g.root_id, &env).unwrap(),
2428 EvalValue::Bool(false)
2429 );
2430 }
2431
2432 #[test]
2433 fn if_then_else() {
2434 let g = AstGraph::from_source("if c then 1 else 2").expect("parse");
2435 let env_t = EvalEnv::new().with_binding("c", EvalValue::Bool(true));
2436 let env_f = EvalEnv::new().with_binding("c", EvalValue::Bool(false));
2437 assert_eq!(eval_node(&g, g.root_id, &env_t).unwrap(), EvalValue::Int(1));
2438 assert_eq!(eval_node(&g, g.root_id, &env_f).unwrap(), EvalValue::Int(2));
2439 }
2440
2441 #[test]
2442 fn unary_neg_and_not() {
2443 let g = AstGraph::from_source("-x").expect("parse");
2444 let env = EvalEnv::new().with_binding("x", EvalValue::Int(7));
2445 assert_eq!(eval_node(&g, g.root_id, &env).unwrap(), EvalValue::Int(-7));
2446
2447 let g = AstGraph::from_source("!b").expect("parse");
2448 let env = EvalEnv::new().with_binding("b", EvalValue::Bool(true));
2449 assert_eq!(
2450 eval_node(&g, g.root_id, &env).unwrap(),
2451 EvalValue::Bool(false)
2452 );
2453 }
2454
2455 #[test]
2456 fn apply_with_non_callable_function_is_opaque() {
2457 let g = AstGraph::from_source("f x").expect("parse");
2459 let env = EvalEnv::new()
2460 .with_binding("f", EvalValue::Int(1))
2461 .with_binding("x", EvalValue::Int(2));
2462 let v = eval_node(&g, g.root_id, &env).unwrap();
2463 match v {
2464 EvalValue::Opaque { ref kind, .. } => assert_eq!(kind, "Apply"),
2465 other => panic!("expected Opaque, got {other:?}"),
2466 }
2467 }
2468
2469 #[test]
2470 fn lambda_evaluates_to_closure() {
2471 let g = AstGraph::from_source("x: x + 1").expect("parse");
2472 let v = eval_node(&g, g.root_id, &EvalEnv::new()).unwrap();
2473 match v {
2474 EvalValue::Closure { ref param, .. } => assert_eq!(param, "x"),
2475 other => panic!("expected Closure, got {other:?}"),
2476 }
2477 }
2478
2479 #[test]
2480 fn apply_a_closure_evaluates_body() {
2481 let g = AstGraph::from_source("(x: x + 1) 5").expect("parse");
2483 let v = eval_node(&g, g.root_id, &EvalEnv::new()).unwrap();
2484 assert_eq!(v, EvalValue::Int(6));
2485 }
2486
2487 #[test]
2488 fn let_in_binds_locals() {
2489 let g = AstGraph::from_source("let a = 3; b = 4; in a + b").expect("parse");
2490 let v = eval_node(&g, g.root_id, &EvalEnv::new()).unwrap();
2491 assert_eq!(v, EvalValue::Int(7));
2492 }
2493
2494 #[test]
2495 fn with_pushes_attrset_scope() {
2496 let g = AstGraph::from_source("with pkgs; foo + bar").expect("parse");
2497 let env = EvalEnv::new().with_binding(
2498 "pkgs",
2499 EvalValue::AttrSet(BTreeMap::from([
2500 ("foo".to_string(), EvalValue::Int(10)),
2501 ("bar".to_string(), EvalValue::Int(32)),
2502 ])),
2503 );
2504 let v = eval_node(&g, g.root_id, &env).unwrap();
2505 assert_eq!(v, EvalValue::Int(42));
2506 }
2507
2508 #[test]
2509 fn mkif_true_yields_builtin_with_payload() {
2510 let g = AstGraph::from_source("mkIf c body").expect("parse");
2511 let env = EvalEnv::new()
2512 .with_binding("c", EvalValue::Bool(true))
2513 .with_binding("body", EvalValue::Str("yes".into()));
2514 let v = eval_node(&g, g.root_id, &env).unwrap();
2515 match v {
2516 EvalValue::Builtin { kind, payload } => {
2517 assert_eq!(kind, "mkIf");
2518 assert_eq!(*payload, EvalValue::Str("yes".into()));
2519 }
2520 other => panic!("expected Builtin, got {other:?}"),
2521 }
2522 }
2523
2524 #[test]
2525 fn mkif_false_yields_disabled_builtin() {
2526 let g = AstGraph::from_source("mkIf c body").expect("parse");
2527 let env = EvalEnv::new()
2528 .with_binding("c", EvalValue::Bool(false))
2529 .with_binding("body", EvalValue::Str("nope".into()));
2530 let v = eval_node(&g, g.root_id, &env).unwrap();
2531 match v {
2532 EvalValue::Builtin { kind, payload } => {
2533 assert_eq!(kind, "mkIf-disabled");
2534 assert_eq!(*payload, EvalValue::Null);
2535 }
2536 other => panic!("expected Builtin, got {other:?}"),
2537 }
2538 }
2539
2540 #[test]
2541 fn mkforce_wraps_value() {
2542 let g = AstGraph::from_source("mkForce 42").expect("parse");
2543 let v = eval_node(&g, g.root_id, &EvalEnv::new()).unwrap();
2544 match v {
2545 EvalValue::Builtin { kind, payload } => {
2546 assert_eq!(kind, "mkForce");
2547 assert_eq!(*payload, EvalValue::Int(42));
2548 }
2549 other => panic!("expected Builtin, got {other:?}"),
2550 }
2551 }
2552
2553 #[test]
2554 fn mkmerge_wraps_list() {
2555 let g = AstGraph::from_source("mkMerge [ a b ]").expect("parse");
2556 let env = EvalEnv::new()
2557 .with_binding("a", EvalValue::Int(1))
2558 .with_binding("b", EvalValue::Int(2));
2559 let v = eval_node(&g, g.root_id, &env).unwrap();
2560 match v {
2561 EvalValue::Builtin { kind, payload } => {
2562 assert_eq!(kind, "mkMerge");
2563 assert_eq!(*payload, EvalValue::List(vec![EvalValue::Int(1), EvalValue::Int(2)]));
2564 }
2565 other => panic!("expected Builtin, got {other:?}"),
2566 }
2567 }
2568
2569 #[test]
2570 fn lib_qualified_call_dispatches_via_last_segment() {
2571 let g = AstGraph::from_source("lib.mkIf c body").expect("parse");
2574 let env = EvalEnv::new()
2575 .with_binding(
2576 "lib",
2577 EvalValue::AttrSet(BTreeMap::new()),
2578 )
2579 .with_binding("c", EvalValue::Bool(true))
2580 .with_binding("body", EvalValue::Int(7));
2581 let v = eval_node(&g, g.root_id, &env).unwrap();
2582 match v {
2583 EvalValue::Builtin { kind, payload } => {
2584 assert_eq!(kind, "mkIf");
2585 assert_eq!(*payload, EvalValue::Int(7));
2586 }
2587 other => panic!("expected Builtin from lib.mkIf, got {other:?}"),
2588 }
2589 }
2590
2591 fn eval_env(src: &str, env: EvalEnv) -> EvalValue {
2594 let g = AstGraph::from_source(src).expect("parse");
2595 eval_node(&g, g.root_id, &env).expect("eval")
2596 }
2597
2598 #[test]
2599 fn builtin_to_string_covers_typed_lattice() {
2600 assert_eq!(eval("toString 42"), EvalValue::Str("42".into()));
2601 assert_eq!(eval("toString 3.5"), EvalValue::Str("3.5".into()));
2602 assert_eq!(eval("toString true"), EvalValue::Str("1".into()));
2603 assert_eq!(eval("toString false"), EvalValue::Str("".into()));
2604 assert_eq!(eval("toString null"), EvalValue::Str("".into()));
2605 assert_eq!(eval("toString \"hello\""), EvalValue::Str("hello".into()));
2606 assert_eq!(eval("toString [1 2 3]"), EvalValue::Str("1 2 3".into()));
2607 }
2608
2609 #[test]
2610 fn builtin_type_predicates() {
2611 assert_eq!(eval("isString \"x\""), EvalValue::Bool(true));
2612 assert_eq!(eval("isString 42"), EvalValue::Bool(false));
2613 assert_eq!(eval("isInt 42"), EvalValue::Bool(true));
2614 assert_eq!(eval("isFloat 3.14"), EvalValue::Bool(true));
2615 assert_eq!(eval("isBool true"), EvalValue::Bool(true));
2616 assert_eq!(eval("isNull null"), EvalValue::Bool(true));
2617 assert_eq!(eval("isList [1 2]"), EvalValue::Bool(true));
2618 assert_eq!(eval("isAttrs {a=1;}"), EvalValue::Bool(true));
2619 assert_eq!(eval("isFunction (x: x)"), EvalValue::Bool(true));
2620 }
2621
2622 #[test]
2623 fn builtin_list_ops() {
2624 assert_eq!(eval("length [1 2 3 4]"), EvalValue::Int(4));
2625 assert_eq!(eval("length \"hello\""), EvalValue::Int(5));
2626 assert_eq!(eval("head [10 20 30]"), EvalValue::Int(10));
2627 assert_eq!(
2628 eval("tail [10 20 30]"),
2629 EvalValue::List(vec![EvalValue::Int(20), EvalValue::Int(30)])
2630 );
2631 assert_eq!(eval("elem 2 [1 2 3]"), EvalValue::Bool(true));
2632 assert_eq!(eval("elem 5 [1 2 3]"), EvalValue::Bool(false));
2633 }
2634
2635 #[test]
2636 fn builtin_attrset_ops() {
2637 let attrs = EvalValue::AttrSet(BTreeMap::from([
2638 ("a".to_string(), EvalValue::Int(1)),
2639 ("b".to_string(), EvalValue::Int(2)),
2640 ]));
2641 let env = EvalEnv::new().with_binding("x", attrs.clone());
2642 assert_eq!(
2643 eval_env("attrNames x", env.clone()),
2644 EvalValue::List(vec![
2645 EvalValue::Str("a".into()),
2646 EvalValue::Str("b".into())
2647 ])
2648 );
2649 assert_eq!(
2650 eval_env("attrValues x", env.clone()),
2651 EvalValue::List(vec![EvalValue::Int(1), EvalValue::Int(2)])
2652 );
2653 assert_eq!(
2654 eval_env("hasAttr \"a\" x", env.clone()),
2655 EvalValue::Bool(true)
2656 );
2657 assert_eq!(
2658 eval_env("hasAttr \"z\" x", env.clone()),
2659 EvalValue::Bool(false)
2660 );
2661 assert_eq!(
2662 eval_env("getAttr \"a\" x", env),
2663 EvalValue::Int(1)
2664 );
2665 }
2666
2667 #[test]
2668 fn builtin_concat_ops() {
2669 assert_eq!(
2670 eval("concatLists [[1 2] [3 4]]"),
2671 EvalValue::List(vec![
2672 EvalValue::Int(1),
2673 EvalValue::Int(2),
2674 EvalValue::Int(3),
2675 EvalValue::Int(4),
2676 ])
2677 );
2678 assert_eq!(
2679 eval("concatStringsSep \", \" [\"a\" \"b\" \"c\"]"),
2680 EvalValue::Str("a, b, c".into())
2681 );
2682 }
2683
2684 #[test]
2685 fn lib_optional_branches_on_condition() {
2686 assert_eq!(
2687 eval("optional true 99"),
2688 EvalValue::List(vec![EvalValue::Int(99)])
2689 );
2690 assert_eq!(eval("optional false 99"), EvalValue::List(Vec::new()));
2691 assert_eq!(
2692 eval("optionals true [1 2 3]"),
2693 EvalValue::List(vec![EvalValue::Int(1), EvalValue::Int(2), EvalValue::Int(3)])
2694 );
2695 assert_eq!(
2696 eval("optionals false [1 2 3]"),
2697 EvalValue::List(Vec::new())
2698 );
2699 assert_eq!(
2700 eval("optionalAttrs true { a = 1; }"),
2701 EvalValue::AttrSet(BTreeMap::from([("a".to_string(), EvalValue::Int(1))]))
2702 );
2703 assert_eq!(
2704 eval("optionalAttrs false { a = 1; }"),
2705 EvalValue::AttrSet(BTreeMap::new())
2706 );
2707 }
2708
2709 #[test]
2710 fn lib_id_and_const() {
2711 assert_eq!(eval("id 42"), EvalValue::Int(42));
2712 assert_eq!(eval("const 7 99"), EvalValue::Int(7));
2713 }
2714
2715 #[test]
2716 fn lib_qualified_concatStringsSep_dispatches() {
2717 let g = AstGraph::from_source("lib.concatStringsSep \"-\" [\"a\" \"b\"]").expect("parse");
2718 let env = EvalEnv::new().with_binding("lib", EvalValue::AttrSet(BTreeMap::new()));
2719 assert_eq!(
2720 eval_node(&g, g.root_id, &env).unwrap(),
2721 EvalValue::Str("a-b".into())
2722 );
2723 }
2724
2725 #[test]
2726 fn throw_surfaces_as_typed_error() {
2727 let g = AstGraph::from_source("throw \"explicit failure\"").expect("parse");
2728 let err = eval_node(&g, g.root_id, &EvalEnv::new()).unwrap_err();
2729 match err {
2730 EvalError::UndefinedIdent(msg) => {
2731 assert!(msg.contains("throw: explicit failure"))
2732 }
2733 other => panic!("expected typed throw error, got {other:?}"),
2734 }
2735 }
2736
2737 #[test]
2740 fn pattern_lambda_with_required_args() {
2741 let g = AstGraph::from_source("({ a, b }: a + b) { a = 1; b = 2; }").expect("parse");
2743 assert_eq!(eval_node(&g, g.root_id, &EvalEnv::new()).unwrap(), EvalValue::Int(3));
2744 }
2745
2746 #[test]
2747 fn pattern_lambda_uses_default_when_missing() {
2748 let g = AstGraph::from_source("({ a, b ? 10 }: a + b) { a = 1; }").expect("parse");
2750 assert_eq!(eval_node(&g, g.root_id, &EvalEnv::new()).unwrap(), EvalValue::Int(11));
2751 }
2752
2753 #[test]
2754 fn pattern_lambda_rejects_extras_without_ellipsis() {
2755 let g = AstGraph::from_source("({ a }: a) { a = 1; extra = 2; }").expect("parse");
2757 let err = eval_node(&g, g.root_id, &EvalEnv::new()).unwrap_err();
2758 match err {
2759 EvalError::TypeMismatch { context, .. } => {
2760 assert!(context.contains("extra"));
2761 }
2762 other => panic!("expected TypeMismatch, got {other:?}"),
2763 }
2764 }
2765
2766 #[test]
2767 fn pattern_lambda_accepts_extras_with_ellipsis() {
2768 let g = AstGraph::from_source("({ a, ... }: a) { a = 1; extra = 99; }").expect("parse");
2770 assert_eq!(eval_node(&g, g.root_id, &EvalEnv::new()).unwrap(), EvalValue::Int(1));
2771 }
2772
2773 #[test]
2774 fn nixos_module_lambda_with_config_pattern_evaluates() {
2775 let g = AstGraph::from_source(
2778 "({ config, lib, pkgs, ... }: lib + 1) \
2779 { config = {}; lib = 41; pkgs = {}; }",
2780 )
2781 .expect("parse");
2782 assert_eq!(eval_node(&g, g.root_id, &EvalEnv::new()).unwrap(), EvalValue::Int(42));
2783 }
2784
2785 #[test]
2788 fn builtin_substring_extracts_range() {
2789 assert_eq!(eval("substring 0 5 \"hello world\""), EvalValue::Str("hello".into()));
2790 assert_eq!(eval("substring 6 5 \"hello world\""), EvalValue::Str("world".into()));
2791 }
2792
2793 #[test]
2794 fn builtin_string_length_counts_chars() {
2795 assert_eq!(eval("stringLength \"hello\""), EvalValue::Int(5));
2796 }
2797
2798 #[test]
2799 fn builtin_replace_strings_replaces() {
2800 assert_eq!(
2801 eval("replaceStrings [\"foo\"] [\"bar\"] \"foofoo\""),
2802 EvalValue::Str("barbar".into())
2803 );
2804 }
2805
2806 #[test]
2809 fn builtin_map_doubles_via_lambda() {
2810 let g = AstGraph::from_source("map (x: x * 2) [1 2 3]").expect("parse");
2812 assert_eq!(
2813 eval_node(&g, g.root_id, &EvalEnv::new()).unwrap(),
2814 EvalValue::List(vec![EvalValue::Int(2), EvalValue::Int(4), EvalValue::Int(6)])
2815 );
2816 }
2817
2818 #[test]
2819 fn builtin_filter_keeps_predicate_true() {
2820 let g = AstGraph::from_source("filter (x: x > 2) [1 2 3 4]").expect("parse");
2821 assert_eq!(
2822 eval_node(&g, g.root_id, &EvalEnv::new()).unwrap(),
2823 EvalValue::List(vec![EvalValue::Int(3), EvalValue::Int(4)])
2824 );
2825 }
2826
2827 #[test]
2828 fn builtin_foldl_accumulates_left_to_right() {
2829 let g = AstGraph::from_source("foldl' (acc: x: acc + x) 0 [1 2 3 4]").expect("parse");
2831 assert_eq!(eval_node(&g, g.root_id, &EvalEnv::new()).unwrap(), EvalValue::Int(10));
2832 }
2833
2834 #[test]
2835 fn builtin_gen_list_generates_by_index() {
2836 let g = AstGraph::from_source("genList (i: i * i) 4").expect("parse");
2837 assert_eq!(
2838 eval_node(&g, g.root_id, &EvalEnv::new()).unwrap(),
2839 EvalValue::List(vec![
2840 EvalValue::Int(0),
2841 EvalValue::Int(1),
2842 EvalValue::Int(4),
2843 EvalValue::Int(9),
2844 ])
2845 );
2846 }
2847
2848 #[test]
2849 fn builtin_concat_map_flattens_with_fn() {
2850 let g = AstGraph::from_source("concatMap (x: [x x]) [1 2]").expect("parse");
2851 assert_eq!(
2852 eval_node(&g, g.root_id, &EvalEnv::new()).unwrap(),
2853 EvalValue::List(vec![
2854 EvalValue::Int(1),
2855 EvalValue::Int(1),
2856 EvalValue::Int(2),
2857 EvalValue::Int(2),
2858 ])
2859 );
2860 }
2861
2862 #[test]
2865 fn builtin_intersect_attrs_keeps_common_keys() {
2866 let g = AstGraph::from_source(
2867 "intersectAttrs { a = 1; b = 2; } { b = 20; c = 30; }",
2868 )
2869 .expect("parse");
2870 match eval_node(&g, g.root_id, &EvalEnv::new()).unwrap() {
2871 EvalValue::AttrSet(m) => {
2872 assert_eq!(m.get("b"), Some(&EvalValue::Int(20)));
2873 assert!(!m.contains_key("a"));
2874 assert!(!m.contains_key("c"));
2875 }
2876 other => panic!("expected AttrSet, got {other:?}"),
2877 }
2878 }
2879
2880 #[test]
2881 fn builtin_remove_attrs_drops_named_keys() {
2882 let g = AstGraph::from_source(
2883 "removeAttrs { a = 1; b = 2; c = 3; } [\"a\" \"c\"]",
2884 )
2885 .expect("parse");
2886 match eval_node(&g, g.root_id, &EvalEnv::new()).unwrap() {
2887 EvalValue::AttrSet(m) => {
2888 assert_eq!(m.get("b"), Some(&EvalValue::Int(2)));
2889 assert!(!m.contains_key("a"));
2890 assert!(!m.contains_key("c"));
2891 }
2892 other => panic!("expected AttrSet, got {other:?}"),
2893 }
2894 }
2895
2896 #[test]
2897 fn builtin_list_to_attrs_builds_attrset() {
2898 let g = AstGraph::from_source(
2899 "listToAttrs [ { name = \"a\"; value = 1; } { name = \"b\"; value = 2; } ]",
2900 )
2901 .expect("parse");
2902 match eval_node(&g, g.root_id, &EvalEnv::new()).unwrap() {
2903 EvalValue::AttrSet(m) => {
2904 assert_eq!(m.get("a"), Some(&EvalValue::Int(1)));
2905 assert_eq!(m.get("b"), Some(&EvalValue::Int(2)));
2906 }
2907 other => panic!("expected AttrSet, got {other:?}"),
2908 }
2909 }
2910
2911 #[test]
2914 fn lib_filter_attrs_keeps_matching() {
2915 let g =
2916 AstGraph::from_source("filterAttrs (k: v: v > 1) { a = 1; b = 2; c = 3; }").expect("parse");
2917 match eval_node(&g, g.root_id, &EvalEnv::new()).unwrap() {
2918 EvalValue::AttrSet(m) => {
2919 assert!(!m.contains_key("a"));
2920 assert_eq!(m.get("b"), Some(&EvalValue::Int(2)));
2921 assert_eq!(m.get("c"), Some(&EvalValue::Int(3)));
2922 }
2923 other => panic!("expected AttrSet, got {other:?}"),
2924 }
2925 }
2926
2927 #[test]
2928 fn lib_map_attrs_transforms_values() {
2929 let g = AstGraph::from_source("mapAttrs (k: v: v * 10) { a = 1; b = 2; }").expect("parse");
2930 match eval_node(&g, g.root_id, &EvalEnv::new()).unwrap() {
2931 EvalValue::AttrSet(m) => {
2932 assert_eq!(m.get("a"), Some(&EvalValue::Int(10)));
2933 assert_eq!(m.get("b"), Some(&EvalValue::Int(20)));
2934 }
2935 other => panic!("expected AttrSet, got {other:?}"),
2936 }
2937 }
2938
2939 #[test]
2940 fn lib_flatten_recursively_flattens() {
2941 assert_eq!(
2942 eval("flatten [1 [2 [3 [4]]] 5]"),
2943 EvalValue::List(vec![
2944 EvalValue::Int(1),
2945 EvalValue::Int(2),
2946 EvalValue::Int(3),
2947 EvalValue::Int(4),
2948 EvalValue::Int(5),
2949 ])
2950 );
2951 }
2952
2953 #[test]
2954 fn lib_unique_drops_duplicates_preserving_order() {
2955 assert_eq!(
2956 eval("unique [1 2 1 3 2 4]"),
2957 EvalValue::List(vec![
2958 EvalValue::Int(1),
2959 EvalValue::Int(2),
2960 EvalValue::Int(3),
2961 EvalValue::Int(4),
2962 ])
2963 );
2964 }
2965
2966 #[test]
2967 fn lib_take_and_drop_partition_a_list() {
2968 assert_eq!(
2969 eval("take 3 [1 2 3 4 5]"),
2970 EvalValue::List(vec![EvalValue::Int(1), EvalValue::Int(2), EvalValue::Int(3)])
2971 );
2972 assert_eq!(
2973 eval("drop 2 [1 2 3 4 5]"),
2974 EvalValue::List(vec![EvalValue::Int(3), EvalValue::Int(4), EvalValue::Int(5)])
2975 );
2976 }
2977
2978 #[test]
2979 fn lib_foldr_accumulates_right_to_left() {
2980 let g = AstGraph::from_source("foldr (x: acc: x - acc) 0 [3 2 1]").expect("parse");
2982 assert_eq!(eval_node(&g, g.root_id, &EvalEnv::new()).unwrap(), EvalValue::Int(2));
2983 }
2984
2985 #[test]
2988 fn builtin_path_exists_for_known_path() {
2989 let g = AstGraph::from_source("pathExists \"/tmp\"").expect("parse");
2991 assert_eq!(eval_node(&g, g.root_id, &EvalEnv::new()).unwrap(), EvalValue::Bool(true));
2992
2993 let g = AstGraph::from_source("pathExists \"/definitely-not-a-real-path-xyz\"").expect("parse");
2994 assert_eq!(eval_node(&g, g.root_id, &EvalEnv::new()).unwrap(), EvalValue::Bool(false));
2995 }
2996
2997 #[test]
2998 fn builtin_read_file_returns_contents() {
2999 use std::io::Write as _;
3000 let mut f = tempfile::NamedTempFile::new().expect("tmpfile");
3001 writeln!(f, "hello sui").expect("write");
3002 let path = f.path().to_string_lossy().to_string();
3003 let g = AstGraph::from_source(&format!("readFile \"{path}\"")).expect("parse");
3004 match eval_node(&g, g.root_id, &EvalEnv::new()).unwrap() {
3005 EvalValue::Str(s) => assert!(s.contains("hello sui")),
3006 other => panic!("expected Str, got {other:?}"),
3007 }
3008 }
3009
3010 #[test]
3011 fn builtin_import_evaluates_a_nix_file() {
3012 use std::io::Write as _;
3013 let mut f = tempfile::NamedTempFile::new().expect("tmpfile");
3014 writeln!(f, "1 + 2").expect("write");
3015 let path = f.path().to_string_lossy().to_string();
3016 let g = AstGraph::from_source(&format!("import \"{path}\"")).expect("parse");
3017 assert_eq!(eval_node(&g, g.root_id, &EvalEnv::new()).unwrap(), EvalValue::Int(3));
3018 }
3019
3020 #[test]
3021 fn string_with_interpolated_int() {
3022 let g = AstGraph::from_source("\"value: ${n}\"").expect("parse");
3023 let env = EvalEnv::new().with_binding("n", EvalValue::Int(42));
3024 assert_eq!(
3025 eval_node(&g, g.root_id, &env).unwrap(),
3026 EvalValue::Str("value: 42".into())
3027 );
3028 }
3029
3030 #[test]
3031 fn config_dotted_select_evaluates() {
3032 let g = AstGraph::from_source(
3034 "config.networking.hostName == \"rio\"",
3035 )
3036 .expect("parse");
3037 let mut inner = BTreeMap::new();
3038 inner.insert(
3039 "hostName".to_string(),
3040 EvalValue::Str("rio".to_string()),
3041 );
3042 let mut outer = BTreeMap::new();
3043 outer.insert("networking".to_string(), EvalValue::AttrSet(inner));
3044 let env = EvalEnv::new().with_binding("config", EvalValue::AttrSet(outer));
3045 assert_eq!(
3046 eval_node(&g, g.root_id, &env).unwrap(),
3047 EvalValue::Bool(true)
3048 );
3049 }
3050
3051 #[test]
3054 fn derivation_produces_typed_value_with_drv_and_out_paths() {
3055 let g = AstGraph::from_source(
3056 r#"derivation {
3057 name = "hello";
3058 system = "x86_64-linux";
3059 builder = "/bin/sh";
3060 args = ["-c" "echo hello"];
3061 }"#,
3062 ).expect("parse");
3063 let v = eval_node(&g, g.root_id, &EvalEnv::new()).expect("eval");
3064 match v {
3065 EvalValue::Derivation { name, system, drv_path, out_path, .. } => {
3066 assert_eq!(name, "hello");
3067 assert_eq!(system, "x86_64-linux");
3068 assert!(drv_path.starts_with("/nix/store/"));
3069 assert!(drv_path.ends_with("-hello.drv"));
3070 assert!(out_path.starts_with("/nix/store/"));
3071 assert!(out_path.ends_with("-hello"));
3072 }
3073 other => panic!("expected Derivation, got {other:?}"),
3074 }
3075 }
3076
3077 #[test]
3078 fn derivation_is_deterministic_for_identical_inputs() {
3079 let src = r#"derivation {
3080 name = "det";
3081 system = "x86_64-linux";
3082 builder = "/bin/sh";
3083 args = ["-c" "true"];
3084 }"#;
3085 let g1 = AstGraph::from_source(src).expect("parse a");
3086 let g2 = AstGraph::from_source(src).expect("parse b");
3087 let v1 = eval_node(&g1, g1.root_id, &EvalEnv::new()).expect("eval a");
3088 let v2 = eval_node(&g2, g2.root_id, &EvalEnv::new()).expect("eval b");
3089 match (v1, v2) {
3090 (
3091 EvalValue::Derivation { drv_path: p1, out_path: o1, .. },
3092 EvalValue::Derivation { drv_path: p2, out_path: o2, .. },
3093 ) => {
3094 assert_eq!(p1, p2, "drv_path must be deterministic");
3095 assert_eq!(o1, o2, "out_path must be deterministic");
3096 }
3097 other => panic!("expected two Derivations, got {other:?}"),
3098 }
3099 }
3100
3101 #[test]
3102 fn derivation_attr_select_exposes_typed_paths() {
3103 let src = r#"(derivation {
3104 name = "select-test";
3105 system = "x86_64-linux";
3106 builder = "/bin/sh";
3107 }).outPath"#;
3108 let g = AstGraph::from_source(src).expect("parse");
3109 match eval_node(&g, g.root_id, &EvalEnv::new()).expect("eval") {
3110 EvalValue::Str(s) => {
3111 assert!(s.starts_with("/nix/store/"));
3112 assert!(s.ends_with("-select-test"));
3113 }
3114 other => panic!("expected outPath string, got {other:?}"),
3115 }
3116 }
3117
3118 #[test]
3119 fn derivation_interpolates_to_out_path_in_strings() {
3120 let g = AstGraph::from_source(
3121 r#"let d = derivation {
3122 name = "interp";
3123 system = "x86_64-linux";
3124 builder = "/bin/sh";
3125 }; in "${d}/bin/x""#,
3126 ).expect("parse");
3127 match eval_node(&g, g.root_id, &EvalEnv::new()).expect("eval") {
3128 EvalValue::Str(s) => {
3129 assert!(s.starts_with("/nix/store/"));
3130 assert!(s.ends_with("-interp/bin/x"), "got `{s}`");
3131 }
3132 other => panic!("expected Str, got {other:?}"),
3133 }
3134 }
3135
3136 #[test]
3137 fn derivation_missing_required_attr_errors() {
3138 let g = AstGraph::from_source(
3139 r#"derivation { system = "x86_64-linux"; builder = "/bin/sh"; }"#,
3140 ).expect("parse");
3141 match eval_node(&g, g.root_id, &EvalEnv::new()) {
3142 Err(EvalError::TypeMismatch { context, .. }) => {
3143 assert_eq!(context, "derivation");
3144 }
3145 other => panic!("expected TypeMismatch on missing name, got {other:?}"),
3146 }
3147 }
3148
3149 #[test]
3152 fn fetchurl_via_file_url_returns_store_path() {
3153 use std::io::Write as _;
3154 let mut f = tempfile::NamedTempFile::new().expect("tmpfile");
3155 writeln!(f, "sui-fetcher-test-bytes").expect("write");
3156 let path = f.path().to_string_lossy().to_string();
3157 let url = format!("file://{path}");
3158
3159 let tmp = tempfile::tempdir().expect("tmp store");
3160 unsafe { std::env::set_var("SUI_TEST_STORE_DIR", tmp.path()); }
3161
3162 let src = format!(r#"fetchurl {{ url = "{url}"; name = "hello.txt"; }}"#);
3163 let g = AstGraph::from_source(&src).expect("parse");
3164 match eval_node(&g, g.root_id, &EvalEnv::new()).expect("eval") {
3165 EvalValue::Str(s) => {
3166 assert!(s.starts_with("/nix/store/"));
3167 assert!(s.ends_with("-hello.txt"));
3168 }
3169 other => panic!("expected Str(store_path), got {other:?}"),
3170 }
3171 }
3172
3173 #[test]
3174 fn fetchurl_hash_mismatch_surfaces_typed_error() {
3175 use std::io::Write as _;
3176 let mut f = tempfile::NamedTempFile::new().expect("tmpfile");
3177 writeln!(f, "mismatch-test").expect("write");
3178 let path = f.path().to_string_lossy().to_string();
3179 let url = format!("file://{path}");
3180 let tmp = tempfile::tempdir().expect("tmp store");
3181 unsafe { std::env::set_var("SUI_TEST_STORE_DIR", tmp.path()); }
3182
3183 let src = format!(
3184 r#"fetchurl {{ url = "{url}"; sha256 = "deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"; }}"#
3185 );
3186 let g = AstGraph::from_source(&src).expect("parse");
3187 match eval_node(&g, g.root_id, &EvalEnv::new()) {
3188 Err(EvalError::TypeMismatch { context, .. }) => {
3189 assert_eq!(context, "fetchurl apply");
3190 }
3191 other => panic!("expected hash-mismatch TypeMismatch, got {other:?}"),
3192 }
3193 }
3194
3195 #[test]
3196 fn fetch_tarball_via_file_url_unpacks_and_returns_path() {
3197 let tmpdir = tempfile::tempdir().expect("tmp");
3198 let entry_path = tmpdir.path().join("readme.txt");
3199 std::fs::write(&entry_path, b"hi").expect("write entry");
3200 let tarball_path = tmpdir.path().join("src.tar.gz");
3201 let file = std::fs::File::create(&tarball_path).expect("create tar");
3202 let gz = flate2::write::GzEncoder::new(file, flate2::Compression::default());
3203 let mut ar = tar::Builder::new(gz);
3204 ar.append_path_with_name(&entry_path, "readme.txt").expect("tar add");
3205 ar.finish().expect("tar finish");
3206 drop(ar);
3207
3208 let url = format!("file://{}", tarball_path.to_string_lossy());
3209 let store_dir = tempfile::tempdir().expect("tmp store");
3210 unsafe { std::env::set_var("SUI_TEST_STORE_DIR", store_dir.path()); }
3211
3212 let src = format!(r#"fetchTarball {{ url = "{url}"; name = "src"; }}"#);
3213 let g = AstGraph::from_source(&src).expect("parse");
3214 match eval_node(&g, g.root_id, &EvalEnv::new()).expect("eval") {
3215 EvalValue::Str(s) => {
3216 assert!(s.starts_with("/nix/store/"));
3217 assert!(s.ends_with("-src"));
3218 }
3219 other => panic!("expected Str, got {other:?}"),
3220 }
3221 }
3222
3223 #[test]
3224 fn fetch_git_returns_deterministic_path_for_identical_inputs() {
3225 let src = r#"fetchGit { url = "https://example.test/repo.git"; rev = "abc123"; }"#;
3226 let g1 = AstGraph::from_source(src).expect("parse a");
3227 let g2 = AstGraph::from_source(src).expect("parse b");
3228 let v1 = eval_node(&g1, g1.root_id, &EvalEnv::new()).expect("a");
3229 let v2 = eval_node(&g2, g2.root_id, &EvalEnv::new()).expect("b");
3230 assert_eq!(v1, v2, "fetchGit path must be deterministic");
3231 if let EvalValue::Str(s) = v1 {
3232 assert!(s.starts_with("/nix/store/"));
3233 assert!(s.ends_with("-source"));
3234 } else {
3235 panic!("expected Str");
3236 }
3237 }
3238
3239 #[test]
3240 fn fetch_tree_dispatches_by_type() {
3241 let src = r#"fetchTree {
3242 type = "git";
3243 url = "https://example.test/r.git";
3244 rev = "deadbeef";
3245 name = "ft";
3246 }"#;
3247 let g = AstGraph::from_source(src).expect("parse");
3248 match eval_node(&g, g.root_id, &EvalEnv::new()).expect("eval") {
3249 EvalValue::Str(s) => assert!(s.ends_with("-ft")),
3250 other => panic!("expected Str, got {other:?}"),
3251 }
3252 }
3253
3254 #[test]
3255 fn fetch_tree_unknown_type_errors_typed() {
3256 let src = r#"fetchTree { type = "totally-unknown"; }"#;
3257 let g = AstGraph::from_source(src).expect("parse");
3258 match eval_node(&g, g.root_id, &EvalEnv::new()) {
3259 Err(EvalError::TypeMismatch { context, .. }) => {
3260 assert_eq!(context, "fetchTree.type dispatch");
3261 }
3262 other => panic!("expected dispatch error, got {other:?}"),
3263 }
3264 }
3265}