1use std::collections::BTreeMap;
43
44use crate::sexpr::SExpr;
45
46#[derive(Debug, Clone, PartialEq)]
49pub enum NixValue {
50 Int(i64),
51 Float(f64),
52 Bool(bool),
53 Str(String),
54 Null,
55 Ident(String),
58 List(Vec<NixValue>),
59 AttrSet(BTreeMap<String, NixValue>),
61}
62
63impl NixValue {
64 #[must_use]
66 pub fn to_nix_expr(&self) -> String {
67 let mut out = String::new();
68 self.write_into(&mut out, 0);
69 out
70 }
71
72 fn write_into(&self, out: &mut String, indent: usize) {
73 match self {
74 Self::Int(i) => out.push_str(&i.to_string()),
75 Self::Float(f) => {
76 let s = f.to_string();
77 if s.contains('.') || s.contains('e') || s.contains('E') {
78 out.push_str(&s);
79 } else {
80 out.push_str(&s);
81 out.push_str(".0");
82 }
83 }
84 Self::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
85 Self::Null => out.push_str("null"),
86 Self::Str(s) => {
87 out.push('"');
88 for c in s.chars() {
89 match c {
90 '"' => out.push_str("\\\""),
91 '\\' => out.push_str("\\\\"),
92 '\n' => out.push_str("\\n"),
93 '\t' => out.push_str("\\t"),
94 '$' => {
95 out.push_str("\\$");
97 }
98 c => out.push(c),
99 }
100 }
101 out.push('"');
102 }
103 Self::Ident(name) => out.push_str(name),
104 Self::List(items) => {
105 out.push('[');
106 for (i, item) in items.iter().enumerate() {
107 if i > 0 {
108 out.push(' ');
109 }
110 item.write_into(out, indent);
111 }
112 out.push(']');
113 }
114 Self::AttrSet(entries) => {
115 if entries.is_empty() {
116 out.push_str("{}");
117 return;
118 }
119 out.push_str("{ ");
120 for (i, (k, v)) in entries.iter().enumerate() {
121 if i > 0 {
122 out.push(' ');
123 }
124 if is_simple_ident(k) {
126 out.push_str(k);
127 } else {
128 out.push('"');
129 out.push_str(&k.replace('"', "\\\""));
130 out.push('"');
131 }
132 out.push_str(" = ");
133 v.write_into(out, indent);
134 out.push(';');
135 }
136 out.push_str(" }");
137 }
138 }
139 }
140}
141
142fn is_simple_ident(s: &str) -> bool {
143 let mut chars = s.chars();
144 match chars.next() {
145 Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
146 _ => return false,
147 }
148 chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
149}
150
151impl NixValue {
154 #[must_use]
161 pub fn from_sexpr(s: &SExpr) -> Self {
162 match s {
163 SExpr::Integer(i) => Self::Int(*i),
164 SExpr::Float(f) => Self::Float(*f),
165 SExpr::Bool(b) => Self::Bool(*b),
166 SExpr::Nil => Self::Null,
167 SExpr::String(s) => Self::Str(s.clone()),
168 SExpr::Symbol(name) => Self::Ident(name.clone()),
169 SExpr::List(items) => {
170 if let Some(attrset) = struct_form_to_attrset(items) {
171 attrset
172 } else {
173 Self::List(items.iter().map(Self::from_sexpr).collect())
174 }
175 }
176 }
177 }
178
179 #[must_use]
190 pub fn to_sexpr(&self) -> SExpr {
191 match self {
192 Self::Int(i) => SExpr::Integer(*i),
193 Self::Float(f) => SExpr::Float(*f),
194 Self::Bool(b) => SExpr::Bool(*b),
195 Self::Null => SExpr::Nil,
196 Self::Str(s) => SExpr::String(s.clone()),
197 Self::Ident(name) => SExpr::Symbol(name.clone()),
198 Self::List(items) => SExpr::List(items.iter().map(Self::to_sexpr).collect()),
199 Self::AttrSet(entries) => {
200 let head = entries
201 .get("head")
202 .and_then(|v| match v {
203 Self::Str(s) | Self::Ident(s) => Some(s.clone()),
204 _ => None,
205 })
206 .unwrap_or_else(|| "attrs".to_string());
207 let mut out = vec![SExpr::Symbol(head.clone())];
208 for (k, v) in entries {
209 if k == "head" {
210 continue;
211 }
212 out.push(SExpr::List(vec![
213 SExpr::Symbol(format!(":{k}")),
214 v.to_sexpr(),
215 ]));
216 }
217 SExpr::List(out)
218 }
219 }
220 }
221}
222
223fn struct_form_to_attrset(items: &[SExpr]) -> Option<NixValue> {
227 let (head, rest) = items.split_first()?;
228 let head_name = match head {
229 SExpr::Symbol(s) if !s.starts_with(':') => s.clone(),
230 _ => return None,
231 };
232 if rest.is_empty() {
233 return None;
234 }
235 let mut entries: BTreeMap<String, NixValue> = BTreeMap::new();
236 for item in rest {
237 let pair = match item {
238 SExpr::List(pair) => pair,
239 _ => return None,
240 };
241 if pair.len() != 2 {
242 return None;
243 }
244 let key = match &pair[0] {
245 SExpr::Symbol(k) if k.starts_with(':') => k[1..].to_string(),
246 _ => return None,
247 };
248 entries.insert(key, NixValue::from_sexpr(&pair[1]));
249 }
250 entries.insert("head".into(), NixValue::Str(head_name));
251 Some(NixValue::AttrSet(entries))
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257 use crate::ir::IacType;
258 use crate::sexpr::ToSExpr;
259
260 #[test]
263 fn int_roundtrip() {
264 let s = SExpr::Integer(42);
265 assert_eq!(NixValue::from_sexpr(&s).to_sexpr(), s);
266 }
267
268 #[test]
269 fn bool_roundtrip() {
270 assert_eq!(
271 NixValue::from_sexpr(&SExpr::Bool(true)).to_sexpr(),
272 SExpr::Bool(true)
273 );
274 }
275
276 #[test]
277 fn nil_roundtrip() {
278 assert_eq!(NixValue::from_sexpr(&SExpr::Nil).to_sexpr(), SExpr::Nil);
279 }
280
281 #[test]
282 fn string_roundtrip() {
283 let s = SExpr::String("hello world".into());
284 assert_eq!(NixValue::from_sexpr(&s).to_sexpr(), s);
285 }
286
287 #[test]
288 fn symbol_roundtrip() {
289 let s = SExpr::Symbol("string".into());
290 assert_eq!(NixValue::from_sexpr(&s).to_sexpr(), s);
291 }
292
293 #[test]
296 fn struct_form_becomes_attrset() {
297 let s = SExpr::parse("(object (:name \"X\") (:size 3))").unwrap();
298 let nix = NixValue::from_sexpr(&s);
299 let matches = matches!(nix, NixValue::AttrSet(_));
300 assert!(matches, "struct-form should convert to AttrSet");
301 }
302
303 #[test]
304 fn attrset_roundtrips_through_struct_form() {
305 let s = SExpr::parse("(object (:name \"X\") (:size 3))").unwrap();
306 let nix = NixValue::from_sexpr(&s);
307 let back = nix.to_sexpr();
308 assert_eq!(back, s);
310 }
311
312 #[test]
313 fn iac_type_list_roundtrips_through_nix() {
314 let ty = IacType::List(Box::new(IacType::String));
315 let s = ty.to_sexpr();
316 let nix = NixValue::from_sexpr(&s);
317 let back = nix.to_sexpr();
318 assert_eq!(back, s);
321 }
322
323 #[test]
324 fn iac_attribute_roundtrips_through_nix_semantically() {
325 use crate::ir::IacAttribute;
333 use crate::sexpr::FromSExpr;
334 use crate::testing::TestAttributeBuilder;
335
336 let attr = TestAttributeBuilder::new("x", IacType::String)
337 .required()
338 .build();
339 let s = attr.to_sexpr();
340 let nix = NixValue::from_sexpr(&s);
341 let back = nix.to_sexpr();
342
343 let parsed = IacAttribute::from_sexpr(&back).expect("parse round-tripped sexpr");
344 assert_eq!(parsed, attr, "semantic round-trip must preserve value");
345 }
346
347 #[test]
348 fn nix_roundtrip_sorts_fields_alphabetically() {
349 let s = SExpr::parse("(x (:z 1) (:a 2))").unwrap();
353 let back = NixValue::from_sexpr(&s).to_sexpr();
354 let back_text = back.emit();
355 let a_pos = back_text.find(":a").unwrap();
356 let z_pos = back_text.find(":z").unwrap();
357 assert!(a_pos < z_pos, "a should come before z: {back_text}");
358 }
359
360 #[test]
363 fn int_renders_as_decimal() {
364 assert_eq!(NixValue::Int(42).to_nix_expr(), "42");
365 }
366
367 #[test]
368 fn float_renders_with_decimal_point() {
369 assert_eq!(NixValue::Float(1.0).to_nix_expr(), "1.0");
370 assert_eq!(NixValue::Float(2.75).to_nix_expr(), "2.75");
371 }
372
373 #[test]
374 fn bool_renders_as_nix_literal() {
375 assert_eq!(NixValue::Bool(true).to_nix_expr(), "true");
376 assert_eq!(NixValue::Bool(false).to_nix_expr(), "false");
377 }
378
379 #[test]
380 fn null_renders() {
381 assert_eq!(NixValue::Null.to_nix_expr(), "null");
382 }
383
384 #[test]
385 fn string_escapes_dollar_sign() {
386 assert_eq!(NixValue::Str("$HOME".into()).to_nix_expr(), "\"\\$HOME\"",);
390 }
391
392 #[test]
393 fn string_escapes_quotes() {
394 assert_eq!(NixValue::Str("a\"b".into()).to_nix_expr(), "\"a\\\"b\"",);
395 }
396
397 #[test]
398 fn empty_list_renders() {
399 assert_eq!(NixValue::List(vec![]).to_nix_expr(), "[]");
400 }
401
402 #[test]
403 fn list_renders_space_separated() {
404 assert_eq!(
405 NixValue::List(vec![NixValue::Int(1), NixValue::Int(2), NixValue::Int(3),])
406 .to_nix_expr(),
407 "[1 2 3]",
408 );
409 }
410
411 #[test]
412 fn empty_attrset_renders() {
413 assert_eq!(NixValue::AttrSet(BTreeMap::new()).to_nix_expr(), "{}",);
414 }
415
416 #[test]
417 fn attrset_renders_semicolon_separated() {
418 let mut map = BTreeMap::new();
419 map.insert("a".to_string(), NixValue::Int(1));
420 map.insert("b".to_string(), NixValue::Int(2));
421 assert_eq!(NixValue::AttrSet(map).to_nix_expr(), "{ a = 1; b = 2; }",);
422 }
423
424 #[test]
425 fn attrset_quotes_non_ident_keys() {
426 let mut map = BTreeMap::new();
427 map.insert("weird key".to_string(), NixValue::Int(1));
428 let rendered = NixValue::AttrSet(map).to_nix_expr();
429 assert!(rendered.contains("\"weird key\""));
430 }
431
432 #[test]
435 fn attrset_iteration_is_deterministic() {
436 let mut map = BTreeMap::new();
438 map.insert("z".to_string(), NixValue::Int(1));
439 map.insert("a".to_string(), NixValue::Int(2));
440 map.insert("m".to_string(), NixValue::Int(3));
441 let rendered = NixValue::AttrSet(map).to_nix_expr();
442 let a_pos = rendered.find("a = ").unwrap();
444 let m_pos = rendered.find("m = ").unwrap();
445 let z_pos = rendered.find("z = ").unwrap();
446 assert!(a_pos < m_pos);
447 assert!(m_pos < z_pos);
448 }
449
450 #[test]
451 fn iac_type_renders_as_nix() {
452 let ty = IacType::List(Box::new(IacType::Integer));
453 let nix = NixValue::from_sexpr(&ty.to_sexpr());
454 let expr = nix.to_nix_expr();
457 assert!(expr.contains("list"));
458 assert!(expr.contains("integer"));
459 }
460
461 #[test]
462 fn iac_attribute_renders_as_attrset() {
463 use crate::testing::TestAttributeBuilder;
464 let attr = TestAttributeBuilder::new("x", IacType::String)
465 .required()
466 .build();
467 let nix = NixValue::from_sexpr(&attr.to_sexpr());
468 let expr = nix.to_nix_expr();
469 assert!(expr.starts_with("{ "));
471 assert!(expr.ends_with(" }"));
472 assert!(expr.contains("head = \"attribute\""));
473 assert!(expr.contains("required = true"));
474 }
475}