1#![allow(missing_docs)]
24
25use std::collections::HashMap;
26use std::fmt;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
30pub struct NodeId(pub u32);
31
32impl NodeId {
33 pub const NONE: NodeId = NodeId(u32::MAX);
35
36 pub fn index(self) -> usize {
37 self.0 as usize
38 }
39
40 pub fn is_none(self) -> bool {
41 self == NodeId::NONE
42 }
43}
44
45impl fmt::Display for NodeId {
46 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47 write!(f, "n{}", self.0)
48 }
49}
50
51#[derive(Clone)]
60pub struct SecretBytes(Vec<u8>);
61
62impl SecretBytes {
63 pub fn new(bytes: impl Into<Vec<u8>>) -> Self {
65 SecretBytes(bytes.into())
66 }
67
68 pub fn len(&self) -> usize {
71 self.0.len()
72 }
73
74 pub fn is_empty(&self) -> bool {
75 self.0.is_empty()
76 }
77
78 pub(crate) fn expose(&self) -> &[u8] {
81 &self.0
82 }
83}
84
85impl fmt::Debug for SecretBytes {
86 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
87 f.write_str("Secret(***)")
88 }
89}
90
91impl fmt::Display for SecretBytes {
92 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
93 f.write_str("***SECRET***")
94 }
95}
96
97impl PartialEq for SecretBytes {
98 fn eq(&self, _other: &Self) -> bool {
100 false
101 }
102}
103
104impl From<&str> for SecretBytes {
105 fn from(value: &str) -> Self {
106 SecretBytes(value.as_bytes().to_vec())
107 }
108}
109
110impl From<String> for SecretBytes {
111 fn from(value: String) -> Self {
112 SecretBytes(value.into_bytes())
113 }
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
118pub enum BinOp {
119 Add,
120 Sub,
121 Mul,
122 Div,
123 Eq,
124 Ne,
125 Lt,
126 Le,
127 Gt,
128 Ge,
129 And,
130 Or,
131}
132
133impl BinOp {
134 pub const fn code(self) -> u32 {
135 match self {
136 BinOp::Add => 0,
137 BinOp::Sub => 1,
138 BinOp::Mul => 2,
139 BinOp::Div => 3,
140 BinOp::Eq => 4,
141 BinOp::Ne => 5,
142 BinOp::Lt => 6,
143 BinOp::Le => 7,
144 BinOp::Gt => 8,
145 BinOp::Ge => 9,
146 BinOp::And => 10,
147 BinOp::Or => 11,
148 }
149 }
150
151 pub const fn from_code(code: u32) -> Option<Self> {
152 let op = match code {
153 0 => BinOp::Add,
154 1 => BinOp::Sub,
155 2 => BinOp::Mul,
156 3 => BinOp::Div,
157 4 => BinOp::Eq,
158 5 => BinOp::Ne,
159 6 => BinOp::Lt,
160 7 => BinOp::Le,
161 8 => BinOp::Gt,
162 9 => BinOp::Ge,
163 10 => BinOp::And,
164 11 => BinOp::Or,
165 _ => return None,
166 };
167 Some(op)
168 }
169
170 pub const fn as_str(self) -> &'static str {
171 match self {
172 BinOp::Add => "+",
173 BinOp::Sub => "-",
174 BinOp::Mul => "*",
175 BinOp::Div => "/",
176 BinOp::Eq => "==",
177 BinOp::Ne => "!=",
178 BinOp::Lt => "<",
179 BinOp::Le => "<=",
180 BinOp::Gt => ">",
181 BinOp::Ge => ">=",
182 BinOp::And => "and",
183 BinOp::Or => "or",
184 }
185 }
186}
187
188#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
190pub enum UnOp {
191 Neg,
192 Not,
193}
194
195impl UnOp {
196 pub const fn code(self) -> u32 {
197 match self {
198 UnOp::Neg => 0,
199 UnOp::Not => 1,
200 }
201 }
202
203 pub const fn from_code(code: u32) -> Option<Self> {
204 match code {
205 0 => Some(UnOp::Neg),
206 1 => Some(UnOp::Not),
207 _ => None,
208 }
209 }
210
211 pub const fn as_str(self) -> &'static str {
212 match self {
213 UnOp::Neg => "-",
214 UnOp::Not => "!",
215 }
216 }
217}
218
219#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
221pub enum SemanticTag {
222 Prompt,
223 Policy,
224 Template,
225}
226
227impl SemanticTag {
228 pub const fn code(self) -> u32 {
229 match self {
230 SemanticTag::Prompt => 0,
231 SemanticTag::Policy => 1,
232 SemanticTag::Template => 2,
233 }
234 }
235
236 pub const fn from_code(code: u32) -> Option<Self> {
237 match code {
238 0 => Some(SemanticTag::Prompt),
239 1 => Some(SemanticTag::Policy),
240 2 => Some(SemanticTag::Template),
241 _ => None,
242 }
243 }
244
245 pub const fn as_str(self) -> &'static str {
246 match self {
247 SemanticTag::Prompt => "prompt",
248 SemanticTag::Policy => "policy",
249 SemanticTag::Template => "template",
250 }
251 }
252}
253
254#[derive(Debug, Clone, PartialEq)]
256pub enum InterpPart {
257 Literal(String),
258 Expr(NodeId),
259}
260
261#[derive(Debug, Clone, PartialEq)]
264pub enum Constraint {
265 MinMax { min: Option<i64>, max: Option<i64> },
266 Enum(Vec<String>),
267 Pattern(String),
268 Odd,
269 Custom(String),
270}
271
272#[derive(Debug, Clone, PartialEq)]
274pub enum TypeSpec {
275 Null,
276 Bool,
277 Int,
278 Float,
279 String,
280 Duration,
281 Bytes,
282 Table { closed: bool },
283 Enum(Vec<String>),
284 Secret,
285 Any,
286 Blob,
287}
288
289impl TypeSpec {
290 pub const fn code(&self) -> u32 {
291 match self {
292 TypeSpec::Null => 0,
293 TypeSpec::Bool => 1,
294 TypeSpec::Int => 2,
295 TypeSpec::Float => 3,
296 TypeSpec::String => 4,
297 TypeSpec::Duration => 5,
298 TypeSpec::Bytes => 6,
299 TypeSpec::Table { .. } => 7,
300 TypeSpec::Enum(_) => 8,
301 TypeSpec::Secret => 9,
302 TypeSpec::Any => 10,
303 TypeSpec::Blob => 11,
304 }
305 }
306
307 pub fn name(&self) -> String {
309 match self {
310 TypeSpec::Null => "Null".into(),
311 TypeSpec::Bool => "Bool".into(),
312 TypeSpec::Int => "Int".into(),
313 TypeSpec::Float => "Float".into(),
314 TypeSpec::String => "String".into(),
315 TypeSpec::Duration => "Duration".into(),
316 TypeSpec::Bytes => "Bytes".into(),
317 TypeSpec::Table { closed } => {
318 if *closed {
319 "Table(closed)".into()
320 } else {
321 "Table".into()
322 }
323 }
324 TypeSpec::Enum(values) => format!("Enum({})", values.join(", ")),
325 TypeSpec::Secret => "Secret".into(),
326 TypeSpec::Any => "Any".into(),
327 TypeSpec::Blob => "Blob".into(),
328 }
329 }
330}
331
332#[derive(Debug, Clone, PartialEq)]
334pub enum PatchOp {
335 Set { path: String, value: NodeId },
336 Remove { path: String },
337 Rename { from: String, to: String },
338}
339
340#[derive(Debug, Clone, PartialEq)]
342pub struct SchemaFieldSpec {
343 pub typ: TypeSpec,
344 pub min: Option<NodeId>,
345 pub max: Option<NodeId>,
346 pub min_len: Option<NodeId>,
347 pub max_len: Option<NodeId>,
348 pub pattern: Option<NodeId>,
349 pub default: Option<NodeId>,
350 pub deprecated: Option<String>,
351}
352
353impl SchemaFieldSpec {
354 pub fn bare(typ: TypeSpec) -> Self {
355 Self {
356 typ,
357 min: None,
358 max: None,
359 min_len: None,
360 max_len: None,
361 pattern: None,
362 default: None,
363 deprecated: None,
364 }
365 }
366}
367
368#[derive(Clone, PartialEq)]
379pub enum Inode {
380 Null,
381 Bool(bool),
382 Int(i64),
383 Float(f64),
384 Str(String),
385 DurationMs(u64),
386 Quantity {
387 value: NodeId,
388 unit: super::UnitTag,
389 },
390 Array {
391 elements: Vec<NodeId>,
392 },
393 Table {
394 fields: Vec<(String, NodeId)>,
395 },
396 Secret(SecretBytes),
398 SecretRef {
401 locator: NodeId,
402 },
403 Ref(NodeId),
404 Env {
405 name: String,
406 default: Option<NodeId>,
407 },
408 Ident(String),
409 Interpolate {
410 parts: Vec<InterpPart>,
411 },
412 Binary {
413 op: BinOp,
414 lhs: NodeId,
415 rhs: NodeId,
416 },
417 Unary {
418 op: UnOp,
419 expr: NodeId,
420 },
421 Constrained {
422 target: NodeId,
423 constraint: Constraint,
424 },
425 Patch(PatchOp),
426 SchemaField(SchemaFieldSpec),
427 SchemaTable {
428 fields: Vec<(String, NodeId)>,
429 closed: bool,
430 },
431 SchemaArray {
432 element: NodeId,
433 },
434 Semantic {
435 tag: SemanticTag,
436 inner: NodeId,
437 },
438 ContentAddressed {
440 media_type: NodeId,
441 data: NodeId,
442 },
443 Blob {
449 hash: [u8; 32],
450 size: u64,
451 tainted: bool,
452 media_type: String,
453 payload: Option<Vec<u8>>,
454 },
455}
456
457impl fmt::Debug for Inode {
458 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
459 match self {
460 Inode::Null => f.write_str("Null"),
461 Inode::Bool(value) => write!(f, "Bool({value})"),
462 Inode::Int(value) => write!(f, "Int({value})"),
463 Inode::Float(value) => write!(f, "Float({value})"),
464 Inode::Str(value) => write!(f, "Str({value:?})"),
465 Inode::DurationMs(ms) => write!(f, "DurationMs({ms})"),
466 Inode::Quantity { value, unit } => write!(f, "Quantity({value}, {unit})"),
467 Inode::Array { elements } => write!(f, "Array({elements:?})"),
468 Inode::Table { fields } => write!(f, "Table({fields:?})"),
469 Inode::Secret(_) => f.write_str("Secret(***)"),
470 Inode::SecretRef { locator } => write!(f, "SecretRef({locator})"),
471 Inode::Ref(id) => write!(f, "Ref({id})"),
472 Inode::Env { name, default } => write!(f, "Env({name:?}, {default:?})"),
473 Inode::Ident(name) => write!(f, "Ident({name:?})"),
474 Inode::Interpolate { parts } => write!(f, "Interpolate({parts:?})"),
475 Inode::Binary { op, lhs, rhs } => write!(f, "Binary({}, {lhs}, {rhs})", op.as_str()),
476 Inode::Unary { op, expr } => write!(f, "Unary({}, {expr})", op.as_str()),
477 Inode::Constrained { target, constraint } => {
478 write!(f, "Constrained({target}, {constraint:?})")
479 }
480 Inode::Patch(op) => write!(f, "Patch({op:?})"),
481 Inode::SchemaField(spec) => write!(f, "SchemaField({spec:?})"),
482 Inode::SchemaTable { fields, closed } => {
483 write!(f, "SchemaTable({fields:?}, closed={closed})")
484 }
485 Inode::SchemaArray { element } => write!(f, "SchemaArray({element})"),
486 Inode::Semantic { tag, inner } => write!(f, "Semantic({}, {inner})", tag.as_str()),
487 Inode::ContentAddressed { media_type, data } => {
488 write!(f, "ContentAddressed({media_type}, {data})")
489 }
490 Inode::Blob {
491 hash,
492 size,
493 tainted,
494 media_type,
495 payload,
496 } => write!(
497 f,
498 "Blob(blake3:{}, size={size}, media={media_type:?}, tainted={tainted}, payload={})",
499 hash_hex(hash),
500 match payload {
501 Some(bytes) => format!("{} bytes", bytes.len()),
502 None => "absent".to_string(),
503 }
504 ),
505 }
506 }
507}
508
509pub fn hash_hex(hash: &[u8; 32]) -> String {
511 let mut out = String::with_capacity(64);
512 for byte in hash {
513 out.push_str(&format!("{byte:02x}"));
514 }
515 out
516}
517
518pub fn parse_hash_hex(text: &str) -> Option<[u8; 32]> {
520 if text.len() != 64 {
521 return None;
522 }
523 let mut out = [0u8; 32];
524 for (index, pair) in text.as_bytes().chunks_exact(2).enumerate() {
525 let hi = (pair[0] as char).to_digit(16)?;
526 let lo = (pair[1] as char).to_digit(16)?;
527 out[index] = (hi * 16 + lo) as u8;
528 }
529 Some(out)
530}
531
532#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
537#[allow(missing_docs)]
538pub enum SerializedKind {
539 Null,
540 Bool,
541 Int,
542 Float,
543 Str,
544 DurationMs,
545 Quantity,
546 Array,
547 Table,
548 Secret,
549 SecretRef,
550 Ref,
551 Env,
552 Ident,
553 Interpolate,
554 Binary,
555 Unary,
556 Constrained,
557 Patch,
558 SchemaField,
559 SchemaTable,
560 SchemaArray,
561 Semantic,
562 Blob,
563 ContentAddressed,
564}
565
566impl SerializedKind {
567 pub const fn code(self) -> u8 {
568 match self {
569 SerializedKind::Null => 0,
570 SerializedKind::Bool => 1,
571 SerializedKind::Int => 2,
572 SerializedKind::Float => 3,
573 SerializedKind::Str => 4,
574 SerializedKind::DurationMs => 5,
575 SerializedKind::Quantity => 6,
576 SerializedKind::Array => 7,
577 SerializedKind::Table => 8,
578 SerializedKind::Secret => 9,
579 SerializedKind::SecretRef => 10,
580 SerializedKind::Ref => 11,
581 SerializedKind::Env => 12,
582 SerializedKind::Ident => 13,
583 SerializedKind::Interpolate => 14,
584 SerializedKind::Binary => 15,
585 SerializedKind::Unary => 16,
586 SerializedKind::Constrained => 17,
587 SerializedKind::Patch => 18,
588 SerializedKind::SchemaField => 19,
589 SerializedKind::SchemaTable => 20,
590 SerializedKind::SchemaArray => 21,
591 SerializedKind::Semantic => 22,
592 SerializedKind::Blob => 23,
593 SerializedKind::ContentAddressed => 24,
594 }
595 }
596
597 pub const fn from_code(code: u8) -> Option<Self> {
598 let kind = match code {
599 0 => SerializedKind::Null,
600 1 => SerializedKind::Bool,
601 2 => SerializedKind::Int,
602 3 => SerializedKind::Float,
603 4 => SerializedKind::Str,
604 5 => SerializedKind::DurationMs,
605 6 => SerializedKind::Quantity,
606 7 => SerializedKind::Array,
607 8 => SerializedKind::Table,
608 9 => SerializedKind::Secret,
609 10 => SerializedKind::SecretRef,
610 11 => SerializedKind::Ref,
611 12 => SerializedKind::Env,
612 13 => SerializedKind::Ident,
613 14 => SerializedKind::Interpolate,
614 15 => SerializedKind::Binary,
615 16 => SerializedKind::Unary,
616 17 => SerializedKind::Constrained,
617 18 => SerializedKind::Patch,
618 19 => SerializedKind::SchemaField,
619 20 => SerializedKind::SchemaTable,
620 21 => SerializedKind::SchemaArray,
621 22 => SerializedKind::Semantic,
622 23 => SerializedKind::Blob,
623 24 => SerializedKind::ContentAddressed,
624 _ => return None,
625 };
626 Some(kind)
627 }
628
629 pub const fn name(self) -> &'static str {
630 crate::view::node_kind_name(self.code())
631 }
632}
633
634impl Inode {
635 pub const fn kind(&self) -> SerializedKind {
637 match self {
638 Inode::Null => SerializedKind::Null,
639 Inode::Bool(_) => SerializedKind::Bool,
640 Inode::Int(_) => SerializedKind::Int,
641 Inode::Float(_) => SerializedKind::Float,
642 Inode::Str(_) => SerializedKind::Str,
643 Inode::DurationMs(_) => SerializedKind::DurationMs,
644 Inode::Quantity { .. } => SerializedKind::Quantity,
645 Inode::Array { .. } => SerializedKind::Array,
646 Inode::Table { .. } => SerializedKind::Table,
647 Inode::Secret(_) => SerializedKind::Secret,
648 Inode::SecretRef { .. } => SerializedKind::SecretRef,
649 Inode::Ref(_) => SerializedKind::Ref,
650 Inode::Env { .. } => SerializedKind::Env,
651 Inode::Ident(_) => SerializedKind::Ident,
652 Inode::Interpolate { .. } => SerializedKind::Interpolate,
653 Inode::Binary { .. } => SerializedKind::Binary,
654 Inode::Unary { .. } => SerializedKind::Unary,
655 Inode::Constrained { .. } => SerializedKind::Constrained,
656 Inode::Patch(_) => SerializedKind::Patch,
657 Inode::SchemaField(_) => SerializedKind::SchemaField,
658 Inode::SchemaTable { .. } => SerializedKind::SchemaTable,
659 Inode::SchemaArray { .. } => SerializedKind::SchemaArray,
660 Inode::Semantic { .. } => SerializedKind::Semantic,
661 Inode::Blob { .. } => SerializedKind::Blob,
662 Inode::ContentAddressed { .. } => SerializedKind::ContentAddressed,
663 }
664 }
665}
666
667impl Inode {
668 pub const fn kind_code(&self) -> u8 {
670 match self {
671 Inode::Null => 0,
672 Inode::Bool(_) => 1,
673 Inode::Int(_) => 2,
674 Inode::Float(_) => 3,
675 Inode::Str(_) => 4,
676 Inode::DurationMs(_) => 5,
677 Inode::Quantity { .. } => 6,
678 Inode::Array { .. } => 7,
679 Inode::Table { .. } => 8,
680 Inode::Secret(_) => 9,
681 Inode::SecretRef { .. } => 10,
682 Inode::Ref(_) => 11,
683 Inode::Env { .. } => 12,
684 Inode::Ident(_) => 13,
685 Inode::Interpolate { .. } => 14,
686 Inode::Binary { .. } => 15,
687 Inode::Unary { .. } => 16,
688 Inode::Constrained { .. } => 17,
689 Inode::Patch(_) => 18,
690 Inode::SchemaField(_) => 19,
691 Inode::SchemaTable { .. } => 20,
692 Inode::SchemaArray { .. } => 21,
693 Inode::Semantic { .. } => 22,
694 Inode::Blob { .. } => 23,
695 Inode::ContentAddressed { .. } => 24,
696 }
697 }
698
699 pub const fn is_secret(&self) -> bool {
701 matches!(self, Inode::Secret(_) | Inode::SecretRef { .. })
702 }
703
704 pub fn children(&self) -> Vec<NodeId> {
710 match self {
711 Inode::Quantity { value, .. } => vec![*value],
712 Inode::Array { elements } => elements.clone(),
713 Inode::Table { fields } => fields.iter().map(|(_, id)| *id).collect(),
714 Inode::SecretRef { locator } => vec![*locator],
715 Inode::Ref(id) => vec![*id],
716 Inode::Env { default, .. } => default.iter().copied().collect(),
717 Inode::Interpolate { parts } => parts
718 .iter()
719 .filter_map(|part| match part {
720 InterpPart::Expr(id) => Some(*id),
721 InterpPart::Literal(_) => None,
722 })
723 .collect(),
724 Inode::Binary { lhs, rhs, .. } => vec![*lhs, *rhs],
725 Inode::Unary { expr, .. } => vec![*expr],
726 Inode::Constrained { target, .. } => vec![*target],
727 Inode::Patch(op) => match op {
728 PatchOp::Set { value, .. } => vec![*value],
729 PatchOp::Remove { .. } | PatchOp::Rename { .. } => Vec::new(),
730 },
731 Inode::SchemaField(spec) => {
732 let mut out = Vec::new();
733 for id in [
734 spec.min,
735 spec.max,
736 spec.min_len,
737 spec.max_len,
738 spec.pattern,
739 spec.default,
740 ]
741 .into_iter()
742 .flatten()
743 {
744 out.push(id);
745 }
746 out
747 }
748 Inode::SchemaTable { fields, .. } => fields.iter().map(|(_, id)| *id).collect(),
749 Inode::SchemaArray { element } => vec![*element],
750 Inode::Semantic { inner, .. } => vec![*inner],
751 Inode::ContentAddressed { media_type, data } => vec![*media_type, *data],
752 Inode::Null
753 | Inode::Bool(_)
754 | Inode::Int(_)
755 | Inode::Float(_)
756 | Inode::Str(_)
757 | Inode::DurationMs(_)
758 | Inode::Secret(_)
759 | Inode::Ident(_)
760 | Inode::Blob { .. } => Vec::new(),
761 }
762 }
763
764 pub fn strings(&self, out: &mut Vec<String>) {
766 match self {
767 Inode::Str(s) | Inode::Ident(s) => out.push(s.clone()),
768 Inode::Table { fields } => {
769 for (name, _) in fields {
770 out.push(name.clone());
771 }
772 }
773 Inode::Env { name, .. } => out.push(name.clone()),
774 Inode::Interpolate { parts } => {
775 for part in parts {
776 if let InterpPart::Literal(s) = part {
777 out.push(s.clone());
778 }
779 }
780 }
781 Inode::Constrained { constraint, .. } => match constraint {
782 Constraint::Enum(items) => out.extend(items.iter().cloned()),
783 Constraint::Pattern(s) | Constraint::Custom(s) => out.push(s.clone()),
784 Constraint::MinMax { .. } | Constraint::Odd => {}
785 },
786 Inode::Patch(op) => match op {
787 PatchOp::Set { path, .. } | PatchOp::Remove { path } => out.push(path.clone()),
788 PatchOp::Rename { from, to } => {
789 out.push(from.clone());
790 out.push(to.clone());
791 }
792 },
793 Inode::SchemaField(spec) => {
794 if let TypeSpec::Enum(items) = &spec.typ {
795 out.extend(items.iter().cloned());
796 }
797 if let Some(deprecated) = &spec.deprecated {
798 out.push(deprecated.clone());
799 }
800 }
801 Inode::SchemaTable { fields, .. } => {
802 for (name, _) in fields {
803 out.push(name.clone());
804 }
805 }
806 Inode::Blob { media_type, .. } => out.push(media_type.clone()),
807 Inode::Null
808 | Inode::Bool(_)
809 | Inode::Int(_)
810 | Inode::Float(_)
811 | Inode::DurationMs(_)
812 | Inode::Quantity { .. }
813 | Inode::Array { .. }
814 | Inode::Secret(_)
815 | Inode::SecretRef { .. }
816 | Inode::Ref(_)
817 | Inode::Unary { .. }
818 | Inode::Binary { .. }
819 | Inode::SchemaArray { .. }
820 | Inode::Semantic { .. }
821 | Inode::ContentAddressed { .. } => {}
822 }
823 }
824}
825
826#[derive(Debug, Clone, PartialEq)]
828pub enum AnnotationKind {
829 Intent(String),
831 Constraint { source: String, expr: NodeId },
833 AiImmutable(bool),
835}
836
837#[derive(Debug, Clone, PartialEq)]
839pub struct Annotation {
840 pub node: NodeId,
841 pub kind: AnnotationKind,
842}
843
844#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
846pub enum ProvKind {
847 Text,
848 Int,
849 Flag,
850 Hash,
851 Input,
853}
854
855impl ProvKind {
856 pub const fn code(self) -> u32 {
857 match self {
858 ProvKind::Text => 0,
859 ProvKind::Int => 1,
860 ProvKind::Flag => 2,
861 ProvKind::Hash => 3,
862 ProvKind::Input => 4,
863 }
864 }
865
866 pub const fn from_code(code: u32) -> Option<Self> {
867 match code {
868 0 => Some(ProvKind::Text),
869 1 => Some(ProvKind::Int),
870 2 => Some(ProvKind::Flag),
871 3 => Some(ProvKind::Hash),
872 4 => Some(ProvKind::Input),
873 _ => None,
874 }
875 }
876}
877
878#[derive(Debug, Clone, PartialEq)]
880pub enum ProvValue {
881 Text(String),
882 Int(i64),
883 Flag(bool),
884 Hash([u8; 32]),
885}
886
887#[derive(Debug, Clone, PartialEq)]
889pub struct ProvEntry {
890 pub key: String,
891 pub value: ProvValue,
892}
893
894impl ProvEntry {
895 pub fn kind(&self) -> ProvKind {
896 match self.value {
897 ProvValue::Text(_) => ProvKind::Text,
898 ProvValue::Int(_) => ProvKind::Int,
899 ProvValue::Flag(_) => ProvKind::Flag,
900 ProvValue::Hash(_) => ProvKind::Hash,
901 }
902 }
903}
904
905#[derive(Debug, Clone, PartialEq, Default)]
907pub struct Provenance {
908 pub producer: Option<String>,
910 pub timestamp_ms: u64,
912 pub model: Option<String>,
914 pub tool: Option<String>,
916 pub parent: Option<[u8; 32]>,
918 pub entries: Vec<ProvEntry>,
920}
921
922#[derive(Debug, Clone, Default)]
928pub struct InfinityModule {
929 nodes: Vec<Inode>,
930 root: Option<NodeId>,
931 bindings: Vec<(String, NodeId)>,
932 annotations: Vec<Annotation>,
933 provenance: Option<Provenance>,
934}
935
936impl PartialEq for InfinityModule {
937 fn eq(&self, other: &Self) -> bool {
947 self.structurally_eq(other)
948 }
949}
950
951fn prov_rank(value: &ProvValue) -> u32 {
952 match value {
953 ProvValue::Text(_) => 0,
954 ProvValue::Int(_) => 1,
955 ProvValue::Flag(_) => 2,
956 ProvValue::Hash(_) => 3,
957 }
958}
959
960fn node_eq(left: &Inode, right: &Inode) -> bool {
962 match (left, right) {
963 (Inode::Secret(_), Inode::Secret(_)) => true,
964 (Inode::Secret(_), _) | (_, Inode::Secret(_)) => false,
965 (Inode::Float(a), Inode::Float(b)) => a.to_bits() == b.to_bits(),
968 _ => left == right,
969 }
970}
971
972impl InfinityModule {
973 pub fn new() -> Self {
974 Self::default()
975 }
976
977 pub fn push(&mut self, node: Inode) -> NodeId {
979 let id = NodeId(u32::try_from(self.nodes.len()).expect("arena exceeds u32 ids"));
980 self.nodes.push(node);
981 id
982 }
983
984 pub fn add_node(&mut self, node: Inode) -> NodeId {
986 self.push(node)
987 }
988
989 pub fn get(&self, id: NodeId) -> Option<&Inode> {
990 self.nodes.get(id.index())
991 }
992
993 pub fn get_mut(&mut self, id: NodeId) -> Option<&mut Inode> {
994 self.nodes.get_mut(id.index())
995 }
996
997 pub fn node_mut(&mut self, id: NodeId) -> Result<&mut Inode, super::Error> {
998 self.nodes
999 .get_mut(id.index())
1000 .ok_or(super::Error::NodeRef(id.0))
1001 }
1002
1003 pub fn nodes(&self) -> &[Inode] {
1004 &self.nodes
1005 }
1006
1007 pub fn node_count(&self) -> usize {
1008 self.nodes.len()
1009 }
1010
1011 pub fn is_empty(&self) -> bool {
1012 self.nodes.is_empty()
1013 }
1014
1015 pub fn root(&self) -> Option<NodeId> {
1016 self.root
1017 }
1018
1019 pub fn set_root(&mut self, root: Option<NodeId>) {
1020 self.root = root;
1021 }
1022
1023 pub fn bindings(&self) -> &[(String, NodeId)] {
1024 &self.bindings
1025 }
1026
1027 pub fn bind(&mut self, name: impl Into<String>, id: NodeId) {
1029 let name = name.into();
1030 if let Some(slot) = self.bindings.iter_mut().find(|(n, _)| *n == name) {
1031 slot.1 = id;
1032 } else {
1033 self.bindings.push((name, id));
1034 }
1035 }
1036
1037 pub fn annotations(&self) -> &[Annotation] {
1038 &self.annotations
1039 }
1040
1041 pub fn annotate(&mut self, node: NodeId, kind: AnnotationKind) {
1042 self.annotations.push(Annotation { node, kind });
1043 }
1044
1045 pub fn clear_annotations(&mut self) {
1047 self.annotations.clear();
1048 }
1049
1050 pub fn remove_binding(&mut self, name: &str) -> Option<NodeId> {
1052 let position = self.bindings.iter().position(|(key, _)| key == name)?;
1053 Some(self.bindings.remove(position).1)
1054 }
1055
1056 pub fn provenance(&self) -> Option<&Provenance> {
1057 self.provenance.as_ref()
1058 }
1059
1060 pub fn has_secret(&self) -> bool {
1062 self.nodes
1063 .iter()
1064 .any(|node| matches!(node, Inode::Secret(_)))
1065 }
1066
1067 pub fn structurally_eq(&self, other: &Self) -> bool {
1072 let (Ok(left), Ok(right)) = (
1073 crate::canonical::prepare(self),
1074 crate::canonical::prepare(other),
1075 ) else {
1076 return false;
1077 };
1078 left.root == right.root
1079 && left.bindings == right.bindings
1080 && left.annotations == right.annotations
1081 && left.provenance == right.provenance
1082 && left.strings == right.strings
1083 && left.units == right.units
1084 && left.blobs == right.blobs
1085 && left.hashes == right.hashes
1086 && left.numbers == right.numbers
1087 && left.secrets.len() == right.secrets.len()
1088 && left.types.len() == right.types.len()
1089 && left
1090 .types
1091 .iter()
1092 .zip(right.types.iter())
1093 .all(|(a, b)| a.spec == b.spec && a.aux == b.aux)
1094 && left.nodes.len() == right.nodes.len()
1095 && left
1096 .nodes
1097 .iter()
1098 .zip(right.nodes.iter())
1099 .all(|(a, b)| node_eq(a, b))
1100 }
1101
1102 pub fn set_provenance(&mut self, provenance: Option<Provenance>) {
1103 self.provenance = provenance.map(|mut prov| {
1106 prov.entries.sort_by(|a, b| {
1107 (a.key.as_str(), prov_rank(&a.value)).cmp(&(b.key.as_str(), prov_rank(&b.value)))
1108 });
1109 prov.entries.dedup();
1110 prov
1111 });
1112 }
1113
1114 pub fn roots(&self) -> Vec<NodeId> {
1117 let mut roots = Vec::new();
1118 if let Some(root) = self.root {
1119 roots.push(root);
1120 }
1121 let mut bindings: Vec<&(String, NodeId)> = self.bindings.iter().collect();
1122 bindings.sort_by(|a, b| a.0.cmp(&b.0));
1123 roots.extend(bindings.into_iter().map(|(_, id)| *id));
1124 for annotation in &self.annotations {
1125 roots.push(annotation.node);
1126 if let AnnotationKind::Constraint { expr, .. } = &annotation.kind {
1127 roots.push(*expr);
1128 }
1129 }
1130 roots
1131 }
1132
1133 pub fn edges(&self) -> Vec<(NodeId, NodeId)> {
1137 let mut edges = Vec::new();
1138 for (index, node) in self.nodes.iter().enumerate() {
1139 let dependent = NodeId(index as u32);
1140 for child in node.children() {
1141 edges.push((child, dependent));
1142 }
1143 }
1144 edges.sort_unstable();
1145 edges.dedup();
1146 edges
1147 }
1148
1149 pub fn dependency_map(&self) -> HashMap<NodeId, Vec<NodeId>> {
1151 let mut map: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
1152 for (index, node) in self.nodes.iter().enumerate() {
1153 map.insert(NodeId(index as u32), node.children());
1154 }
1155 map
1156 }
1157
1158 pub fn validate_structure(&self, max_nodes: u64, max_depth: u32) -> Result<(), super::Error> {
1163 use super::Error;
1164 if self.nodes.len() as u64 > max_nodes {
1165 return Err(Error::Limit {
1166 kind: "nodes",
1167 limit: max_nodes,
1168 });
1169 }
1170 let count = self.nodes.len() as u32;
1171 let check = |id: NodeId| -> Result<(), Error> {
1172 if id.0 >= count {
1173 return Err(Error::NodeRef(id.0));
1174 }
1175 Ok(())
1176 };
1177 for (index, node) in self.nodes.iter().enumerate() {
1178 let id = NodeId(index as u32);
1179 for child in node.children() {
1180 check(child)?;
1181 }
1182 match node {
1183 Inode::Table { fields } | Inode::SchemaTable { fields, .. } => {
1184 let mut seen = std::collections::HashSet::with_capacity(fields.len());
1187 for (name, _) in fields {
1188 if !seen.insert(name.as_str()) {
1189 return Err(Error::DuplicateField { node: id.0 });
1190 }
1191 }
1192 }
1193 Inode::Float(value)
1194 if value.is_nan() && !crate::canonical::is_canonical_nan(*value) =>
1195 {
1196 return Err(Error::Node {
1197 id: id.0,
1198 reason: "non-canonical NaN",
1199 });
1200 }
1201 Inode::Constrained { constraint, .. } => match constraint {
1202 Constraint::MinMax {
1203 min: Some(min),
1204 max: Some(max),
1205 } if min > max => {
1206 return Err(Error::Node {
1207 id: id.0,
1208 reason: "constraint min exceeds max",
1209 });
1210 }
1211 Constraint::Enum(items) if items.is_empty() => {
1212 return Err(Error::Node {
1213 id: id.0,
1214 reason: "empty enum constraint",
1215 });
1216 }
1217 _ => {}
1218 },
1219 _ => {}
1220 }
1221 }
1222 if let Some(root) = self.root {
1223 check(root)?;
1224 }
1225 let mut seen = std::collections::HashSet::new();
1226 for (name, id) in &self.bindings {
1227 check(*id)?;
1228 if !seen.insert(name.as_str()) {
1229 return Err(Error::DuplicateBinding);
1230 }
1231 }
1232 for annotation in &self.annotations {
1233 check(annotation.node)?;
1234 if let AnnotationKind::Constraint { expr, .. } = &annotation.kind {
1235 check(*expr)?;
1236 }
1237 }
1238 self.check_acyclic(count)?;
1239 self.check_depth(max_depth)?;
1240 Ok(())
1241 }
1242
1243 pub fn check_acyclic(&self, count: u32) -> Result<(), super::Error> {
1249 use super::Error;
1250 let mut state = vec![0u8; count as usize];
1252 let mut stack: Vec<(u32, usize)> = Vec::new();
1253 for start in 0..count {
1254 if state[start as usize] != 0 {
1255 continue;
1256 }
1257 stack.push((start, 0));
1258 state[start as usize] = 1;
1259 while let Some((node, child_index)) = stack.pop() {
1260 let children = self.nodes[node as usize].children();
1261 if child_index < children.len() {
1262 stack.push((node, child_index + 1));
1263 let next = children[child_index].0;
1264 match state[next as usize] {
1265 0 => {
1266 state[next as usize] = 1;
1267 stack.push((next, 0));
1268 }
1269 1 => return Err(Error::Cycle { node: next }),
1270 _ => {}
1271 }
1272 } else {
1273 state[node as usize] = 2;
1274 }
1275 }
1276 }
1277 Ok(())
1278 }
1279
1280 pub fn check_depth(&self, max_depth: u32) -> Result<(), super::Error> {
1282 use super::Error;
1283 if max_depth == 0 {
1284 return Ok(());
1285 }
1286 let count = self.nodes.len();
1287 let mut depth = vec![0u32; count];
1288 let mut stack: Vec<(usize, usize)> = Vec::new();
1289 for start in 0..count {
1290 if depth[start] != 0 {
1291 continue;
1292 }
1293 stack.push((start, 0));
1294 depth[start] = 1;
1295 while let Some((node, child_index)) = stack.pop() {
1296 let children = self.nodes[node].children();
1297 if child_index < children.len() {
1298 stack.push((node, child_index + 1));
1299 let next = children[child_index].index();
1300 let candidate = depth[node].saturating_add(1);
1301 if candidate > max_depth {
1302 return Err(Error::Depth { limit: max_depth });
1303 }
1304 if candidate > depth[next] {
1305 depth[next] = candidate;
1306 stack.push((next, 0));
1307 }
1308 }
1309 }
1310 }
1311 Ok(())
1312 }
1313
1314 pub fn string_bytes(&self) -> u64 {
1316 let mut strings = Vec::new();
1317 for node in &self.nodes {
1318 node.strings(&mut strings);
1319 }
1320 strings.iter().map(|s| s.len() as u64).sum()
1321 }
1322
1323 pub fn from_parts(
1326 nodes: Vec<Inode>,
1327 root: Option<NodeId>,
1328 bindings: Vec<(String, NodeId)>,
1329 mut annotations: Vec<Annotation>,
1330 provenance: Option<Provenance>,
1331 ) -> Self {
1332 annotations.sort_by(|a, b| {
1333 (a.node.0, annotation_order(&a.kind)).cmp(&(b.node.0, annotation_order(&b.kind)))
1334 });
1335 InfinityModule {
1336 nodes,
1337 root,
1338 bindings,
1339 annotations,
1340 provenance,
1341 }
1342 }
1343}
1344
1345fn annotation_order(kind: &AnnotationKind) -> u32 {
1346 match kind {
1347 AnnotationKind::Intent(_) => 0,
1348 AnnotationKind::Constraint { .. } => 1,
1349 AnnotationKind::AiImmutable(_) => 2,
1350 }
1351}
1352
1353#[derive(Debug, Default)]
1356pub struct ModuleBuilder {
1357 module: InfinityModule,
1358}
1359
1360impl ModuleBuilder {
1361 pub fn new() -> Self {
1362 Self::default()
1363 }
1364
1365 pub fn push(&mut self, node: Inode) -> NodeId {
1366 self.module.push(node)
1367 }
1368
1369 pub fn root(&mut self, id: NodeId) -> &mut Self {
1370 self.module.set_root(Some(id));
1371 self
1372 }
1373
1374 pub fn bind(&mut self, name: impl Into<String>, id: NodeId) -> &mut Self {
1375 self.module.bind(name, id);
1376 self
1377 }
1378
1379 pub fn annotate(&mut self, node: NodeId, kind: AnnotationKind) -> &mut Self {
1380 self.module.annotate(node, kind);
1381 self
1382 }
1383
1384 pub fn provenance(&mut self, provenance: Provenance) -> &mut Self {
1385 self.module.set_provenance(Some(provenance));
1386 self
1387 }
1388
1389 pub fn build(self) -> InfinityModule {
1390 self.module
1391 }
1392}