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 node_eq(left: &Inode, right: &Inode) -> bool {
953 match (left, right) {
954 (Inode::Secret(_), Inode::Secret(_)) => true,
955 (Inode::Secret(_), _) | (_, Inode::Secret(_)) => false,
956 (Inode::Float(a), Inode::Float(b)) => a.to_bits() == b.to_bits(),
959 _ => left == right,
960 }
961}
962
963impl InfinityModule {
964 pub fn new() -> Self {
965 Self::default()
966 }
967
968 pub fn push(&mut self, node: Inode) -> NodeId {
970 let id = NodeId(u32::try_from(self.nodes.len()).expect("arena exceeds u32 ids"));
971 self.nodes.push(node);
972 id
973 }
974
975 pub fn add_node(&mut self, node: Inode) -> NodeId {
977 self.push(node)
978 }
979
980 pub fn get(&self, id: NodeId) -> Option<&Inode> {
981 self.nodes.get(id.index())
982 }
983
984 pub fn get_mut(&mut self, id: NodeId) -> Option<&mut Inode> {
985 self.nodes.get_mut(id.index())
986 }
987
988 pub fn node_mut(&mut self, id: NodeId) -> Result<&mut Inode, super::Error> {
989 self.nodes
990 .get_mut(id.index())
991 .ok_or(super::Error::NodeRef(id.0))
992 }
993
994 pub fn nodes(&self) -> &[Inode] {
995 &self.nodes
996 }
997
998 pub fn node_count(&self) -> usize {
999 self.nodes.len()
1000 }
1001
1002 pub fn is_empty(&self) -> bool {
1003 self.nodes.is_empty()
1004 }
1005
1006 pub fn root(&self) -> Option<NodeId> {
1007 self.root
1008 }
1009
1010 pub fn set_root(&mut self, root: Option<NodeId>) {
1011 self.root = root;
1012 }
1013
1014 pub fn bindings(&self) -> &[(String, NodeId)] {
1015 &self.bindings
1016 }
1017
1018 pub fn bind(&mut self, name: impl Into<String>, id: NodeId) {
1020 let name = name.into();
1021 if let Some(slot) = self.bindings.iter_mut().find(|(n, _)| *n == name) {
1022 slot.1 = id;
1023 } else {
1024 self.bindings.push((name, id));
1025 }
1026 }
1027
1028 pub fn annotations(&self) -> &[Annotation] {
1029 &self.annotations
1030 }
1031
1032 pub fn annotate(&mut self, node: NodeId, kind: AnnotationKind) {
1033 self.annotations.push(Annotation { node, kind });
1034 }
1035
1036 pub fn clear_annotations(&mut self) {
1038 self.annotations.clear();
1039 }
1040
1041 pub fn remove_binding(&mut self, name: &str) -> Option<NodeId> {
1043 let position = self.bindings.iter().position(|(key, _)| key == name)?;
1044 Some(self.bindings.remove(position).1)
1045 }
1046
1047 pub fn provenance(&self) -> Option<&Provenance> {
1048 self.provenance.as_ref()
1049 }
1050
1051 pub fn has_secret(&self) -> bool {
1053 self.nodes
1054 .iter()
1055 .any(|node| matches!(node, Inode::Secret(_)))
1056 }
1057
1058 pub fn structurally_eq(&self, other: &Self) -> bool {
1063 let (Ok(left), Ok(right)) = (
1064 crate::canonical::prepare(self),
1065 crate::canonical::prepare(other),
1066 ) else {
1067 return false;
1068 };
1069 left.root == right.root
1070 && left.bindings == right.bindings
1071 && left.annotations == right.annotations
1072 && left.provenance == right.provenance
1073 && left.strings == right.strings
1074 && left.units == right.units
1075 && left.blobs == right.blobs
1076 && left.hashes == right.hashes
1077 && left.numbers == right.numbers
1078 && left.secrets.len() == right.secrets.len()
1079 && left.types.len() == right.types.len()
1080 && left
1081 .types
1082 .iter()
1083 .zip(right.types.iter())
1084 .all(|(a, b)| a.spec == b.spec && a.aux == b.aux)
1085 && left.nodes.len() == right.nodes.len()
1086 && left
1087 .nodes
1088 .iter()
1089 .zip(right.nodes.iter())
1090 .all(|(a, b)| node_eq(a, b))
1091 }
1092
1093 pub fn set_provenance(&mut self, provenance: Option<Provenance>) {
1094 self.provenance = provenance.map(|prov| {
1098 let mut by_key: std::collections::BTreeMap<String, ProvValue> =
1099 std::collections::BTreeMap::new();
1100 for entry in prov.entries {
1101 by_key.insert(entry.key, entry.value);
1102 }
1103 let entries: Vec<ProvEntry> = by_key
1104 .into_iter()
1105 .map(|(key, value)| ProvEntry { key, value })
1106 .collect();
1107 Provenance { entries, ..prov }
1108 });
1109 }
1110
1111 pub fn roots(&self) -> Vec<NodeId> {
1114 let mut roots = Vec::new();
1115 if let Some(root) = self.root {
1116 roots.push(root);
1117 }
1118 let mut bindings: Vec<&(String, NodeId)> = self.bindings.iter().collect();
1119 bindings.sort_by(|a, b| a.0.cmp(&b.0));
1120 roots.extend(bindings.into_iter().map(|(_, id)| *id));
1121 for annotation in &self.annotations {
1122 roots.push(annotation.node);
1123 if let AnnotationKind::Constraint { expr, .. } = &annotation.kind {
1124 roots.push(*expr);
1125 }
1126 }
1127 roots
1128 }
1129
1130 pub fn edges(&self) -> Vec<(NodeId, NodeId)> {
1134 let mut edges = Vec::new();
1135 for (index, node) in self.nodes.iter().enumerate() {
1136 let dependent = NodeId(index as u32);
1137 for child in node.children() {
1138 edges.push((child, dependent));
1139 }
1140 }
1141 edges.sort_unstable();
1142 edges.dedup();
1143 edges
1144 }
1145
1146 pub fn dependency_map(&self) -> HashMap<NodeId, Vec<NodeId>> {
1148 let mut map: HashMap<NodeId, Vec<NodeId>> = HashMap::new();
1149 for (index, node) in self.nodes.iter().enumerate() {
1150 map.insert(NodeId(index as u32), node.children());
1151 }
1152 map
1153 }
1154
1155 pub fn validate_structure(&self, max_nodes: u64, max_depth: u32) -> Result<(), super::Error> {
1160 use super::Error;
1161 if self.nodes.len() as u64 > max_nodes {
1162 return Err(Error::Limit {
1163 kind: "nodes",
1164 limit: max_nodes,
1165 });
1166 }
1167 let count = self.nodes.len() as u32;
1168 let check = |id: NodeId| -> Result<(), Error> {
1169 if id.0 >= count {
1170 return Err(Error::NodeRef(id.0));
1171 }
1172 Ok(())
1173 };
1174 for (index, node) in self.nodes.iter().enumerate() {
1175 let id = NodeId(index as u32);
1176 for child in node.children() {
1177 check(child)?;
1178 }
1179 match node {
1180 Inode::Table { fields } | Inode::SchemaTable { fields, .. } => {
1181 let mut seen = std::collections::HashSet::with_capacity(fields.len());
1184 for (name, _) in fields {
1185 if !seen.insert(name.as_str()) {
1186 return Err(Error::DuplicateField { node: id.0 });
1187 }
1188 }
1189 }
1190 Inode::Float(value)
1191 if value.is_nan() && !crate::canonical::is_canonical_nan(*value) =>
1192 {
1193 return Err(Error::Node {
1194 id: id.0,
1195 reason: "non-canonical NaN",
1196 });
1197 }
1198 Inode::Constrained { constraint, .. } => match constraint {
1199 Constraint::MinMax {
1200 min: Some(min),
1201 max: Some(max),
1202 } if min > max => {
1203 return Err(Error::Node {
1204 id: id.0,
1205 reason: "constraint min exceeds max",
1206 });
1207 }
1208 Constraint::Enum(items) if items.is_empty() => {
1209 return Err(Error::Node {
1210 id: id.0,
1211 reason: "empty enum constraint",
1212 });
1213 }
1214 _ => {}
1215 },
1216 _ => {}
1217 }
1218 }
1219 if let Some(root) = self.root {
1220 check(root)?;
1221 }
1222 let mut seen = std::collections::HashSet::new();
1223 for (name, id) in &self.bindings {
1224 check(*id)?;
1225 if !seen.insert(name.as_str()) {
1226 return Err(Error::DuplicateBinding);
1227 }
1228 }
1229 for annotation in &self.annotations {
1230 check(annotation.node)?;
1231 if let AnnotationKind::Constraint { expr, .. } = &annotation.kind {
1232 check(*expr)?;
1233 }
1234 }
1235 self.check_acyclic(count)?;
1236 self.check_depth(max_depth)?;
1237 Ok(())
1238 }
1239
1240 pub fn check_acyclic(&self, count: u32) -> Result<(), super::Error> {
1246 use super::Error;
1247 let mut state = vec![0u8; count as usize];
1249 let mut stack: Vec<(u32, usize)> = Vec::new();
1250 for start in 0..count {
1251 if state[start as usize] != 0 {
1252 continue;
1253 }
1254 stack.push((start, 0));
1255 state[start as usize] = 1;
1256 while let Some((node, child_index)) = stack.pop() {
1257 let children = self.nodes[node as usize].children();
1258 if child_index < children.len() {
1259 stack.push((node, child_index + 1));
1260 let next = children[child_index].0;
1261 match state[next as usize] {
1262 0 => {
1263 state[next as usize] = 1;
1264 stack.push((next, 0));
1265 }
1266 1 => return Err(Error::Cycle { node: next }),
1267 _ => {}
1268 }
1269 } else {
1270 state[node as usize] = 2;
1271 }
1272 }
1273 }
1274 Ok(())
1275 }
1276
1277 pub fn check_depth(&self, max_depth: u32) -> Result<(), super::Error> {
1279 use super::Error;
1280 if max_depth == 0 {
1281 return Ok(());
1282 }
1283 let count = self.nodes.len();
1284 let mut depth = vec![0u32; count];
1285 let mut stack: Vec<(usize, usize)> = Vec::new();
1286 for start in 0..count {
1287 if depth[start] != 0 {
1288 continue;
1289 }
1290 stack.push((start, 0));
1291 depth[start] = 1;
1292 while let Some((node, child_index)) = stack.pop() {
1293 let children = self.nodes[node].children();
1294 if child_index < children.len() {
1295 stack.push((node, child_index + 1));
1296 let next = children[child_index].index();
1297 let candidate = depth[node].saturating_add(1);
1298 if candidate > max_depth {
1299 return Err(Error::Depth { limit: max_depth });
1300 }
1301 if candidate > depth[next] {
1302 depth[next] = candidate;
1303 stack.push((next, 0));
1304 }
1305 }
1306 }
1307 }
1308 Ok(())
1309 }
1310
1311 pub fn string_bytes(&self) -> u64 {
1313 let mut strings = Vec::new();
1314 for node in &self.nodes {
1315 node.strings(&mut strings);
1316 }
1317 strings.iter().map(|s| s.len() as u64).sum()
1318 }
1319
1320 pub fn from_parts(
1323 nodes: Vec<Inode>,
1324 root: Option<NodeId>,
1325 bindings: Vec<(String, NodeId)>,
1326 mut annotations: Vec<Annotation>,
1327 provenance: Option<Provenance>,
1328 ) -> Self {
1329 annotations.sort_by(|a, b| {
1330 (a.node.0, annotation_order(&a.kind)).cmp(&(b.node.0, annotation_order(&b.kind)))
1331 });
1332 InfinityModule {
1333 nodes,
1334 root,
1335 bindings,
1336 annotations,
1337 provenance,
1338 }
1339 }
1340}
1341
1342fn annotation_order(kind: &AnnotationKind) -> u32 {
1343 match kind {
1344 AnnotationKind::Intent(_) => 0,
1345 AnnotationKind::Constraint { .. } => 1,
1346 AnnotationKind::AiImmutable(_) => 2,
1347 }
1348}
1349
1350#[derive(Debug, Default)]
1353pub struct ModuleBuilder {
1354 module: InfinityModule,
1355}
1356
1357impl ModuleBuilder {
1358 pub fn new() -> Self {
1359 Self::default()
1360 }
1361
1362 pub fn push(&mut self, node: Inode) -> NodeId {
1363 self.module.push(node)
1364 }
1365
1366 pub fn root(&mut self, id: NodeId) -> &mut Self {
1367 self.module.set_root(Some(id));
1368 self
1369 }
1370
1371 pub fn bind(&mut self, name: impl Into<String>, id: NodeId) -> &mut Self {
1372 self.module.bind(name, id);
1373 self
1374 }
1375
1376 pub fn annotate(&mut self, node: NodeId, kind: AnnotationKind) -> &mut Self {
1377 self.module.annotate(node, kind);
1378 self
1379 }
1380
1381 pub fn provenance(&mut self, provenance: Provenance) -> &mut Self {
1382 self.module.set_provenance(Some(provenance));
1383 self
1384 }
1385
1386 pub fn build(self) -> InfinityModule {
1387 self.module
1388 }
1389}