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 extensions: Vec<(u64, Vec<u8>)>,
935}
936
937impl PartialEq for InfinityModule {
938 fn eq(&self, other: &Self) -> bool {
948 self.structurally_eq(other)
949 }
950}
951
952fn node_eq(left: &Inode, right: &Inode) -> bool {
954 match (left, right) {
955 (Inode::Secret(_), Inode::Secret(_)) => true,
956 (Inode::Secret(_), _) | (_, Inode::Secret(_)) => false,
957 (Inode::Float(a), Inode::Float(b)) => a.to_bits() == b.to_bits(),
960 _ => left == right,
961 }
962}
963
964impl InfinityModule {
965 pub fn new() -> Self {
966 Self::default()
967 }
968
969 pub fn push(&mut self, node: Inode) -> NodeId {
971 let id = NodeId(u32::try_from(self.nodes.len()).expect("arena exceeds u32 ids"));
972 self.nodes.push(node);
973 id
974 }
975
976 pub fn add_node(&mut self, node: Inode) -> NodeId {
978 self.push(node)
979 }
980
981 pub fn get(&self, id: NodeId) -> Option<&Inode> {
982 self.nodes.get(id.index())
983 }
984
985 pub fn get_mut(&mut self, id: NodeId) -> Option<&mut Inode> {
986 self.nodes.get_mut(id.index())
987 }
988
989 pub fn node_mut(&mut self, id: NodeId) -> Result<&mut Inode, super::Error> {
990 self.nodes
991 .get_mut(id.index())
992 .ok_or(super::Error::NodeRef(id.0))
993 }
994
995 pub fn nodes(&self) -> &[Inode] {
996 &self.nodes
997 }
998
999 pub fn node_count(&self) -> usize {
1000 self.nodes.len()
1001 }
1002
1003 pub fn is_empty(&self) -> bool {
1004 self.nodes.is_empty()
1005 }
1006
1007 pub fn root(&self) -> Option<NodeId> {
1008 self.root
1009 }
1010
1011 pub fn set_root(&mut self, root: Option<NodeId>) {
1012 self.root = root;
1013 }
1014
1015 pub fn bindings(&self) -> &[(String, NodeId)] {
1016 &self.bindings
1017 }
1018
1019 pub fn bind(&mut self, name: impl Into<String>, id: NodeId) {
1021 let name = name.into();
1022 if let Some(slot) = self.bindings.iter_mut().find(|(n, _)| *n == name) {
1023 slot.1 = id;
1024 } else {
1025 self.bindings.push((name, id));
1026 }
1027 }
1028
1029 pub fn annotations(&self) -> &[Annotation] {
1030 &self.annotations
1031 }
1032
1033 pub fn annotate(&mut self, node: NodeId, kind: AnnotationKind) {
1034 self.annotations.push(Annotation { node, kind });
1035 }
1036
1037 pub fn clear_annotations(&mut self) {
1039 self.annotations.clear();
1040 }
1041
1042 pub fn remove_binding(&mut self, name: &str) -> Option<NodeId> {
1044 let position = self.bindings.iter().position(|(key, _)| key == name)?;
1045 Some(self.bindings.remove(position).1)
1046 }
1047
1048 pub fn provenance(&self) -> Option<&Provenance> {
1049 self.provenance.as_ref()
1050 }
1051
1052 pub fn has_secret(&self) -> bool {
1054 self.nodes
1055 .iter()
1056 .any(|node| matches!(node, Inode::Secret(_)))
1057 }
1058
1059 pub fn structurally_eq(&self, other: &Self) -> bool {
1064 let (Ok(left), Ok(right)) = (
1065 crate::canonical::prepare(self),
1066 crate::canonical::prepare(other),
1067 ) else {
1068 return false;
1069 };
1070 left.root == right.root
1071 && left.bindings == right.bindings
1072 && left.annotations == right.annotations
1073 && left.provenance == right.provenance
1074 && left.strings == right.strings
1075 && left.units == right.units
1076 && left.blobs == right.blobs
1077 && left.hashes == right.hashes
1078 && left.numbers == right.numbers
1079 && left.secrets.len() == right.secrets.len()
1080 && left.types.len() == right.types.len()
1081 && left
1082 .types
1083 .iter()
1084 .zip(right.types.iter())
1085 .all(|(a, b)| a.spec == b.spec && a.aux == b.aux)
1086 && left.nodes.len() == right.nodes.len()
1087 && left
1088 .nodes
1089 .iter()
1090 .zip(right.nodes.iter())
1091 .all(|(a, b)| node_eq(a, b))
1092 }
1093
1094 pub fn add_extension(&mut self, tag: u64, payload: impl Into<Vec<u8>>) {
1100 self.extensions.push((tag, payload.into()));
1101 }
1102
1103 pub fn extensions(&self) -> &[(u64, Vec<u8>)] {
1105 &self.extensions
1106 }
1107
1108 pub fn set_extensions(&mut self, extensions: Vec<(u64, Vec<u8>)>) {
1110 self.extensions = extensions;
1111 }
1112
1113 pub fn set_provenance(&mut self, provenance: Option<Provenance>) {
1114 self.provenance = provenance.map(|prov| {
1118 let mut by_key: std::collections::BTreeMap<String, ProvValue> =
1119 std::collections::BTreeMap::new();
1120 for entry in prov.entries {
1121 by_key.insert(entry.key, entry.value);
1122 }
1123 let entries: Vec<ProvEntry> = by_key
1124 .into_iter()
1125 .map(|(key, value)| ProvEntry { key, value })
1126 .collect();
1127 Provenance { entries, ..prov }
1128 });
1129 }
1130
1131 pub fn roots(&self) -> Vec<NodeId> {
1134 let mut roots = Vec::new();
1135 if let Some(root) = self.root {
1136 roots.push(root);
1137 }
1138 let mut bindings: Vec<&(String, NodeId)> = self.bindings.iter().collect();
1139 bindings.sort_by(|a, b| a.0.cmp(&b.0));
1140 roots.extend(bindings.into_iter().map(|(_, id)| *id));
1141 for annotation in &self.annotations {
1142 roots.push(annotation.node);
1143 if let AnnotationKind::Constraint { expr, .. } = &annotation.kind {
1144 roots.push(*expr);
1145 }
1146 }
1147 roots
1148 }
1149
1150 pub fn edges(&self) -> Vec<(NodeId, NodeId)> {
1154 let mut edges = Vec::new();
1155 for (index, node) in self.nodes.iter().enumerate() {
1156 let dependent = NodeId(index as u32);
1157 for child in node.children() {
1158 edges.push((child, dependent));
1159 }
1160 }
1161 edges.sort_unstable();
1162 edges.dedup();
1163 edges
1164 }
1165
1166 pub fn dependency_map(&self) -> HashMap<NodeId, Vec<NodeId>> {
1168 let mut map: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
1169 for (index, node) in self.nodes.iter().enumerate() {
1170 map.insert(NodeId(index as u32), node.children());
1171 }
1172 map
1173 }
1174
1175 pub fn validate_structure(&self, max_nodes: u64, max_depth: u32) -> Result<(), super::Error> {
1180 use super::Error;
1181 if self.nodes.len() as u64 > max_nodes {
1182 return Err(Error::Limit {
1183 kind: "nodes",
1184 limit: max_nodes,
1185 });
1186 }
1187 let count = self.nodes.len() as u32;
1188 let check = |id: NodeId| -> Result<(), Error> {
1189 if id.0 >= count {
1190 return Err(Error::NodeRef(id.0));
1191 }
1192 Ok(())
1193 };
1194 for (index, node) in self.nodes.iter().enumerate() {
1195 let id = NodeId(index as u32);
1196 for child in node.children() {
1197 check(child)?;
1198 }
1199 match node {
1200 Inode::Table { fields } | Inode::SchemaTable { fields, .. } => {
1201 let mut seen = std::collections::HashSet::with_capacity(fields.len());
1204 for (name, _) in fields {
1205 if !seen.insert(name.as_str()) {
1206 return Err(Error::DuplicateField { node: id.0 });
1207 }
1208 }
1209 }
1210 Inode::Float(value)
1211 if value.is_nan() && !crate::canonical::is_canonical_nan(*value) =>
1212 {
1213 return Err(Error::Node {
1214 id: id.0,
1215 reason: "non-canonical NaN",
1216 });
1217 }
1218 Inode::Constrained { constraint, .. } => match constraint {
1219 Constraint::MinMax {
1220 min: Some(min),
1221 max: Some(max),
1222 } if min > max => {
1223 return Err(Error::Node {
1224 id: id.0,
1225 reason: "constraint min exceeds max",
1226 });
1227 }
1228 Constraint::Enum(items) if items.is_empty() => {
1229 return Err(Error::Node {
1230 id: id.0,
1231 reason: "empty enum constraint",
1232 });
1233 }
1234 _ => {}
1235 },
1236 _ => {}
1237 }
1238 }
1239 if let Some(root) = self.root {
1240 check(root)?;
1241 }
1242 let mut seen = std::collections::HashSet::new();
1243 for (name, id) in &self.bindings {
1244 check(*id)?;
1245 if !seen.insert(name.as_str()) {
1246 return Err(Error::DuplicateBinding);
1247 }
1248 }
1249 for annotation in &self.annotations {
1250 check(annotation.node)?;
1251 if let AnnotationKind::Constraint { expr, .. } = &annotation.kind {
1252 check(*expr)?;
1253 }
1254 }
1255 self.check_acyclic(count)?;
1256 self.check_depth(max_depth)?;
1257 Ok(())
1258 }
1259
1260 pub fn check_acyclic(&self, count: u32) -> Result<(), super::Error> {
1267 use super::Error;
1268 let mut incoming = vec![0u32; count as usize];
1269 for node in &self.nodes {
1270 for child in node.children() {
1271 if child.0 >= count {
1272 return Err(Error::NodeRef(child.0));
1273 }
1274 let slot = &mut incoming[child.index()];
1275 *slot = slot.saturating_add(1);
1276 }
1277 }
1278 let mut queue: std::collections::VecDeque<u32> = incoming
1279 .iter()
1280 .enumerate()
1281 .filter(|(_, degree)| **degree == 0)
1282 .map(|(node, _)| node as u32)
1283 .collect();
1284 let mut processed = 0u32;
1285 while let Some(node) = queue.pop_front() {
1286 processed += 1;
1287 for child in self.nodes[node as usize].children() {
1288 let slot = &mut incoming[child.index()];
1289 *slot = slot.saturating_sub(1);
1290 if *slot == 0 {
1291 queue.push_back(child.0);
1292 }
1293 }
1294 }
1295 if processed != count {
1296 let node = incoming.iter().position(|degree| *degree > 0).unwrap_or(0) as u32;
1299 return Err(Error::Cycle { node });
1300 }
1301 Ok(())
1302 }
1303
1304 pub fn check_depth(&self, max_depth: u32) -> Result<(), super::Error> {
1309 use super::Error;
1310 let count = self.nodes.len();
1311 if count == 0 {
1312 return Ok(());
1313 }
1314 if max_depth == 0 {
1315 return self.check_acyclic(count as u32);
1318 }
1319 let mut incoming = vec![0u32; count];
1320 for node in &self.nodes {
1321 for child in node.children() {
1322 if child.index() >= count {
1323 return Err(Error::NodeRef(child.0));
1324 }
1325 incoming[child.index()] = incoming[child.index()].saturating_add(1);
1326 }
1327 }
1328 let mut depth = vec![1u32; count];
1329 let mut queue: std::collections::VecDeque<u32> = incoming
1330 .iter()
1331 .enumerate()
1332 .filter(|(_, degree)| **degree == 0)
1333 .map(|(node, _)| node as u32)
1334 .collect();
1335 if queue.is_empty() {
1336 return Err(Error::Cycle { node: 0 });
1338 }
1339 let mut processed = 0u32;
1340 while let Some(node) = queue.pop_front() {
1341 processed += 1;
1342 let next = depth[node as usize] + 1;
1343 for child in self.nodes[node as usize].children() {
1344 if next > max_depth {
1345 return Err(Error::Depth { limit: max_depth });
1346 }
1347 if next > depth[child.index()] {
1348 depth[child.index()] = next;
1349 }
1350 let slot = &mut incoming[child.index()];
1351 *slot = slot.saturating_sub(1);
1352 if *slot == 0 {
1353 queue.push_back(child.0);
1354 }
1355 }
1356 }
1357 if processed as usize != count {
1358 let node = incoming.iter().position(|degree| *degree > 0).unwrap_or(0) as u32;
1361 return Err(Error::Cycle { node });
1362 }
1363 Ok(())
1364 }
1365
1366 pub fn string_bytes(&self) -> u64 {
1368 let mut strings = Vec::new();
1369 for node in &self.nodes {
1370 node.strings(&mut strings);
1371 }
1372 strings.iter().map(|s| s.len() as u64).sum()
1373 }
1374
1375 pub fn from_parts(
1378 nodes: Vec<Inode>,
1379 root: Option<NodeId>,
1380 bindings: Vec<(String, NodeId)>,
1381 mut annotations: Vec<Annotation>,
1382 provenance: Option<Provenance>,
1383 ) -> Self {
1384 annotations.sort_by(|a, b| {
1385 (a.node.0, annotation_order(&a.kind)).cmp(&(b.node.0, annotation_order(&b.kind)))
1386 });
1387 InfinityModule {
1388 nodes,
1389 root,
1390 bindings,
1391 annotations,
1392 provenance,
1393 extensions: Vec::new(),
1394 }
1395 }
1396}
1397
1398fn annotation_order(kind: &AnnotationKind) -> u32 {
1399 match kind {
1400 AnnotationKind::Intent(_) => 0,
1401 AnnotationKind::Constraint { .. } => 1,
1402 AnnotationKind::AiImmutable(_) => 2,
1403 }
1404}
1405
1406#[derive(Debug, Default)]
1409pub struct ModuleBuilder {
1410 module: InfinityModule,
1411}
1412
1413impl ModuleBuilder {
1414 pub fn new() -> Self {
1415 Self::default()
1416 }
1417
1418 pub fn push(&mut self, node: Inode) -> NodeId {
1419 self.module.push(node)
1420 }
1421
1422 pub fn root(&mut self, id: NodeId) -> &mut Self {
1423 self.module.set_root(Some(id));
1424 self
1425 }
1426
1427 pub fn bind(&mut self, name: impl Into<String>, id: NodeId) -> &mut Self {
1428 self.module.bind(name, id);
1429 self
1430 }
1431
1432 pub fn annotate(&mut self, node: NodeId, kind: AnnotationKind) -> &mut Self {
1433 self.module.annotate(node, kind);
1434 self
1435 }
1436
1437 pub fn provenance(&mut self, provenance: Provenance) -> &mut Self {
1438 self.module.set_provenance(Some(provenance));
1439 self
1440 }
1441
1442 pub fn build(self) -> InfinityModule {
1443 self.module
1444 }
1445}