Skip to main content

icl_core/
verifier.rs

1//! Contract verifier — checks types, invariants, determinism, and coherence
2//!
3//! The verifier ensures contracts are valid before execution.
4//! It checks: types, invariant consistency, determinism, and structural coherence.
5//!
6//! # Architecture
7//!
8//! The verifier operates on the AST (`ContractNode`) to preserve type information
9//! and source spans for error reporting. It accumulates all diagnostics rather
10//! than stopping at the first error, giving users a complete picture.
11//!
12//! # Verification Phases (per spec §4.1)
13//!
14//! 1. **Type Correctness** — All types well-formed, defaults match declared types
15//! 2. **Invariant Consistency** — Invariants reference valid state fields
16//! 3. **Determinism** — No non-deterministic patterns detected
17//! 4. **Coherence** — Structural validity (unique names, valid ranges, feasible limits)
18
19use std::collections::BTreeSet;
20
21use crate::parser::ast::*;
22use crate::parser::tokenizer::Span;
23
24// ── Verification Result Types ─────────────────────────────
25
26/// Result of contract verification — accumulates all diagnostics
27#[derive(Debug, Clone)]
28pub struct VerificationResult {
29    pub diagnostics: Vec<Diagnostic>,
30}
31
32impl VerificationResult {
33    pub fn new() -> Self {
34        Self {
35            diagnostics: Vec::new(),
36        }
37    }
38
39    /// Returns true if no errors were found (warnings are OK)
40    pub fn is_valid(&self) -> bool {
41        !self
42            .diagnostics
43            .iter()
44            .any(|d| d.severity == Severity::Error)
45    }
46
47    /// Returns only error-level diagnostics
48    pub fn errors(&self) -> Vec<&Diagnostic> {
49        self.diagnostics
50            .iter()
51            .filter(|d| d.severity == Severity::Error)
52            .collect()
53    }
54
55    /// Returns only warning-level diagnostics
56    pub fn warnings(&self) -> Vec<&Diagnostic> {
57        self.diagnostics
58            .iter()
59            .filter(|d| d.severity == Severity::Warning)
60            .collect()
61    }
62
63    fn add_error(&mut self, kind: DiagnosticKind, message: String, span: Option<Span>) {
64        self.diagnostics.push(Diagnostic {
65            severity: Severity::Error,
66            kind,
67            message,
68            span,
69        });
70    }
71
72    fn add_warning(&mut self, kind: DiagnosticKind, message: String, span: Option<Span>) {
73        self.diagnostics.push(Diagnostic {
74            severity: Severity::Warning,
75            kind,
76            message,
77            span,
78        });
79    }
80}
81
82impl Default for VerificationResult {
83    fn default() -> Self {
84        Self::new()
85    }
86}
87
88/// A single verification diagnostic
89#[derive(Debug, Clone)]
90pub struct Diagnostic {
91    pub severity: Severity,
92    pub kind: DiagnosticKind,
93    pub message: String,
94    pub span: Option<Span>,
95}
96
97impl std::fmt::Display for Diagnostic {
98    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
99        let prefix = match self.severity {
100            Severity::Error => "error",
101            Severity::Warning => "warning",
102        };
103        if let Some(ref span) = self.span {
104            write!(
105                f,
106                "{} [{}] at {}: {}",
107                prefix, self.kind, span, self.message
108            )
109        } else {
110            write!(f, "{} [{}]: {}", prefix, self.kind, self.message)
111        }
112    }
113}
114
115/// Severity level for diagnostics
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
117pub enum Severity {
118    Error,
119    Warning,
120}
121
122/// Category of verification issue
123#[derive(Debug, Clone, Copy, PartialEq, Eq)]
124pub enum DiagnosticKind {
125    TypeError,
126    InvariantError,
127    DeterminismViolation,
128    CoherenceError,
129}
130
131impl std::fmt::Display for DiagnosticKind {
132    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
133        match self {
134            DiagnosticKind::TypeError => write!(f, "type"),
135            DiagnosticKind::InvariantError => write!(f, "invariant"),
136            DiagnosticKind::DeterminismViolation => write!(f, "determinism"),
137            DiagnosticKind::CoherenceError => write!(f, "coherence"),
138        }
139    }
140}
141
142// ── Public API ────────────────────────────────────────────
143
144/// Verify a parsed contract AST for correctness.
145///
146/// Runs all verification phases and returns accumulated diagnostics.
147/// Does not stop at first error — reports everything found.
148pub fn verify(ast: &ContractNode) -> VerificationResult {
149    let mut result = VerificationResult::new();
150
151    // Phase 3.1 — Type Checker
152    verify_types(ast, &mut result);
153
154    // Phase 3.2 — Invariant Verifier
155    verify_invariants(ast, &mut result);
156
157    // Phase 3.3 — Determinism Checker
158    verify_determinism(ast, &mut result);
159
160    // Phase 3.4 — Coherence Verifier
161    verify_coherence(ast, &mut result);
162
163    result
164}
165
166// ── Phase 3.1: Type Checker ──────────────────────────────
167
168/// Validate all types in the contract are well-formed and defaults match declared types.
169fn verify_types(ast: &ContractNode, result: &mut VerificationResult) {
170    // Check Identity constraints
171    verify_identity_types(&ast.identity, result);
172
173    // Check PurposeStatement constraints
174    verify_purpose_types(&ast.purpose_statement, result);
175
176    // Check state field types
177    for field in &ast.data_semantics.state {
178        verify_type_expression(&field.type_expr, result);
179        if let Some(ref default) = field.default_value {
180            verify_default_matches_type(&field.name.value, &field.type_expr, default, result);
181        }
182    }
183
184    // Check operation parameter types
185    for op in &ast.behavioral_semantics.operations {
186        for param in &op.parameters {
187            verify_type_expression(&param.type_expr, result);
188            if let Some(ref default) = param.default_value {
189                verify_default_matches_type(&param.name.value, &param.type_expr, default, result);
190            }
191        }
192    }
193
194    // Check resource limits are valid
195    verify_resource_limit_types(&ast.execution_constraints.resource_limits, result);
196}
197
198/// Verify Identity field constraints (spec §1.2)
199fn verify_identity_types(identity: &IdentityNode, result: &mut VerificationResult) {
200    // Version must be non-negative
201    if identity.version.value < 0 {
202        result.add_error(
203            DiagnosticKind::TypeError,
204            format!(
205                "version must be non-negative, found {}",
206                identity.version.value
207            ),
208            Some(identity.version.span.clone()),
209        );
210    }
211
212    // stable_id must match pattern: [a-z0-9][a-z0-9\-]{0,30}[a-z0-9]
213    let sid = &identity.stable_id.value;
214    if !is_valid_stable_id(sid) {
215        result.add_error(
216            DiagnosticKind::TypeError,
217            format!(
218                "stable_id '{}' does not match required pattern [a-z0-9][a-z0-9-]{{0,30}}[a-z0-9]",
219                sid
220            ),
221            Some(identity.stable_id.span.clone()),
222        );
223    }
224
225    // semantic_hash must be valid hex
226    let hash = &identity.semantic_hash.value;
227    if !hash.chars().all(|c| c.is_ascii_hexdigit()) {
228        result.add_error(
229            DiagnosticKind::TypeError,
230            format!("semantic_hash '{}' is not valid hexadecimal", hash),
231            Some(identity.semantic_hash.span.clone()),
232        );
233    }
234}
235
236/// Check if a stable_id matches the spec pattern
237fn is_valid_stable_id(id: &str) -> bool {
238    if id.len() < 2 || id.len() > 32 {
239        return false;
240    }
241    let bytes = id.as_bytes();
242    // First and last must be [a-z0-9]
243    let valid_alnum = |b: u8| b.is_ascii_lowercase() || b.is_ascii_digit();
244    let valid_middle = |b: u8| valid_alnum(b) || b == b'-';
245    if !valid_alnum(bytes[0]) || !valid_alnum(bytes[bytes.len() - 1]) {
246        return false;
247    }
248    bytes[1..bytes.len() - 1].iter().all(|&b| valid_middle(b))
249}
250
251/// Verify PurposeStatement constraints (spec §1.3)
252fn verify_purpose_types(purpose: &PurposeStatementNode, result: &mut VerificationResult) {
253    // confidence_level must be in [0.0, 1.0]
254    let cl = purpose.confidence_level.value;
255    if !(0.0..=1.0).contains(&cl) {
256        result.add_error(
257            DiagnosticKind::TypeError,
258            format!("confidence_level must be in range [0.0, 1.0], found {}", cl),
259            Some(purpose.confidence_level.span.clone()),
260        );
261    }
262
263    // narrative should be < 500 chars (warning, not error)
264    if purpose.narrative.value.len() > 500 {
265        result.add_warning(
266            DiagnosticKind::TypeError,
267            format!(
268                "narrative exceeds recommended 500 character limit ({} chars)",
269                purpose.narrative.value.len()
270            ),
271            Some(purpose.narrative.span.clone()),
272        );
273    }
274}
275
276/// Verify a type expression is well-formed
277fn verify_type_expression(type_expr: &TypeExpression, result: &mut VerificationResult) {
278    match type_expr {
279        TypeExpression::Primitive(_, _) => {
280            // All primitive types are valid by construction
281        }
282        TypeExpression::Array(inner, _) => {
283            verify_type_expression(inner, result);
284        }
285        TypeExpression::Map(key, value, span) => {
286            // Map keys must be a hashable/comparable type
287            verify_type_expression(key, result);
288            verify_type_expression(value, result);
289            verify_map_key_type(key, span, result);
290        }
291        TypeExpression::Object(fields, _) => {
292            // Check for duplicate field names
293            let mut seen = BTreeSet::new();
294            for field in fields {
295                if !seen.insert(&field.name.value) {
296                    result.add_error(
297                        DiagnosticKind::TypeError,
298                        format!("duplicate field name '{}' in Object type", field.name.value),
299                        Some(field.name.span.clone()),
300                    );
301                }
302                verify_type_expression(&field.type_expr, result);
303                if let Some(ref default) = field.default_value {
304                    verify_default_matches_type(
305                        &field.name.value,
306                        &field.type_expr,
307                        default,
308                        result,
309                    );
310                }
311            }
312        }
313        TypeExpression::Enum(variants, span) => {
314            // Enum must have at least one variant
315            if variants.is_empty() {
316                result.add_error(
317                    DiagnosticKind::TypeError,
318                    "Enum type must have at least one variant".to_string(),
319                    Some(span.clone()),
320                );
321            }
322            // Enum variants must be unique
323            let mut seen = BTreeSet::new();
324            for variant in variants {
325                if !seen.insert(&variant.value) {
326                    result.add_error(
327                        DiagnosticKind::TypeError,
328                        format!("duplicate Enum variant '{}'", variant.value),
329                        Some(variant.span.clone()),
330                    );
331                }
332            }
333        }
334    }
335}
336
337/// Verify Map key type is a valid key type (must be hashable/comparable)
338fn verify_map_key_type(
339    key_type: &TypeExpression,
340    map_span: &Span,
341    result: &mut VerificationResult,
342) {
343    match key_type {
344        TypeExpression::Primitive(pt, _) => match pt {
345            PrimitiveType::String
346            | PrimitiveType::Integer
347            | PrimitiveType::Uuid
348            | PrimitiveType::Boolean
349            | PrimitiveType::Iso8601 => {
350                // Valid key types
351            }
352            PrimitiveType::Float => {
353                result.add_error(
354                    DiagnosticKind::TypeError,
355                    "Float cannot be used as Map key type (non-deterministic equality)".to_string(),
356                    Some(map_span.clone()),
357                );
358            }
359        },
360        TypeExpression::Enum(_, _) => {
361            // Enum is a valid key type (string-based)
362        }
363        _ => {
364            result.add_error(
365                DiagnosticKind::TypeError,
366                format!(
367                    "Map key type must be a primitive or Enum, found {}",
368                    type_expr_name(key_type)
369                ),
370                Some(map_span.clone()),
371            );
372        }
373    }
374}
375
376/// Verify a default value matches its declared type
377fn verify_default_matches_type(
378    field_name: &str,
379    type_expr: &TypeExpression,
380    default: &LiteralValue,
381    result: &mut VerificationResult,
382) {
383    let matches = default_matches_type(type_expr, default);
384    if !matches {
385        result.add_error(
386            DiagnosticKind::TypeError,
387            format!(
388                "default value for '{}' has type {}, expected {}",
389                field_name,
390                literal_type_name(default),
391                type_expr_name(type_expr),
392            ),
393            Some(literal_span(default)),
394        );
395    }
396}
397
398/// Check if a literal value is compatible with a type expression
399fn default_matches_type(type_expr: &TypeExpression, default: &LiteralValue) -> bool {
400    match (type_expr, default) {
401        (TypeExpression::Primitive(PrimitiveType::Integer, _), LiteralValue::Integer(_, _)) => true,
402        (TypeExpression::Primitive(PrimitiveType::Float, _), LiteralValue::Float(_, _)) => true,
403        // Allow integer literals as float defaults (e.g., 0 for Float)
404        (TypeExpression::Primitive(PrimitiveType::Float, _), LiteralValue::Integer(_, _)) => true,
405        (TypeExpression::Primitive(PrimitiveType::String, _), LiteralValue::String(_, _)) => true,
406        (TypeExpression::Primitive(PrimitiveType::Boolean, _), LiteralValue::Boolean(_, _)) => true,
407        // ISO8601 and UUID are typically string literals
408        (TypeExpression::Primitive(PrimitiveType::Iso8601, _), LiteralValue::String(_, _)) => true,
409        (TypeExpression::Primitive(PrimitiveType::Uuid, _), LiteralValue::String(_, _)) => true,
410        // Enum default must be a string that matches a variant
411        (TypeExpression::Enum(variants, _), LiteralValue::String(s, _)) => {
412            variants.iter().any(|v| v.value == *s)
413        }
414        // Array default must be array of matching elements
415        (TypeExpression::Array(elem_type, _), LiteralValue::Array(elems, _)) => {
416            elems.iter().all(|e| default_matches_type(elem_type, e))
417        }
418        _ => false,
419    }
420}
421
422/// Verify resource limits are valid positive values
423fn verify_resource_limit_types(limits: &ResourceLimitsNode, result: &mut VerificationResult) {
424    if limits.max_memory_bytes.value <= 0 {
425        result.add_error(
426            DiagnosticKind::TypeError,
427            format!(
428                "max_memory_bytes must be positive, found {}",
429                limits.max_memory_bytes.value
430            ),
431            Some(limits.max_memory_bytes.span.clone()),
432        );
433    }
434    if limits.computation_timeout_ms.value <= 0 {
435        result.add_error(
436            DiagnosticKind::TypeError,
437            format!(
438                "computation_timeout_ms must be positive, found {}",
439                limits.computation_timeout_ms.value
440            ),
441            Some(limits.computation_timeout_ms.span.clone()),
442        );
443    }
444    if limits.max_state_size_bytes.value <= 0 {
445        result.add_error(
446            DiagnosticKind::TypeError,
447            format!(
448                "max_state_size_bytes must be positive, found {}",
449                limits.max_state_size_bytes.value
450            ),
451            Some(limits.max_state_size_bytes.span.clone()),
452        );
453    }
454}
455
456// ── Phase 3.2: Invariant Verifier ─────────────────────────
457
458/// Verify invariants reference valid state fields and are logically consistent.
459fn verify_invariants(ast: &ContractNode, result: &mut VerificationResult) {
460    let state_field_names: BTreeSet<&str> = ast
461        .data_semantics
462        .state
463        .iter()
464        .map(|f| f.name.value.as_str())
465        .collect();
466
467    for invariant in &ast.data_semantics.invariants {
468        let inv_text = &invariant.value;
469
470        // Extract potential field references from invariant text
471        let referenced_fields = extract_identifiers(inv_text);
472
473        // Check that referenced identifiers that look like field names exist in state
474        let mut found_field_ref = false;
475        for ident in &referenced_fields {
476            if state_field_names.contains(ident.as_str()) {
477                found_field_ref = true;
478            }
479        }
480
481        // Warn if invariant doesn't reference any state fields
482        if !found_field_ref && !state_field_names.is_empty() && !inv_text.is_empty() {
483            result.add_warning(
484                DiagnosticKind::InvariantError,
485                format!(
486                    "invariant '{}' does not reference any declared state fields",
487                    inv_text,
488                ),
489                Some(invariant.span.clone()),
490            );
491        }
492    }
493
494    // Check for duplicate invariants
495    let mut seen = BTreeSet::new();
496    for invariant in &ast.data_semantics.invariants {
497        if !seen.insert(&invariant.value) {
498            result.add_warning(
499                DiagnosticKind::InvariantError,
500                format!("duplicate invariant: '{}'", invariant.value),
501                Some(invariant.span.clone()),
502            );
503        }
504    }
505}
506
507/// Extract identifiers (potential field references) from an invariant/condition string
508fn extract_identifiers(text: &str) -> Vec<String> {
509    let mut identifiers = Vec::new();
510    let mut chars = text.chars().peekable();
511
512    while let Some(&c) = chars.peek() {
513        if c.is_ascii_alphabetic() || c == '_' {
514            let mut ident = String::new();
515            while let Some(&c) = chars.peek() {
516                if c.is_ascii_alphanumeric() || c == '_' {
517                    ident.push(c);
518                    chars.next();
519                } else {
520                    break;
521                }
522            }
523            // Filter out common keywords/comparators — keep likely field names
524            if !is_keyword(&ident) {
525                identifiers.push(ident);
526            }
527        } else {
528            chars.next();
529        }
530    }
531
532    identifiers
533}
534
535/// Check if identifier is a common keyword (not a field reference)
536fn is_keyword(s: &str) -> bool {
537    matches!(
538        s,
539        "is" | "not"
540            | "and"
541            | "or"
542            | "true"
543            | "false"
544            | "null"
545            | "empty"
546            | "if"
547            | "then"
548            | "else"
549            | "for"
550            | "while"
551            | "in"
552            | "gt"
553            | "lt"
554            | "eq"
555            | "ne"
556            | "ge"
557            | "le"
558            | "the"
559            | "a"
560            | "an"
561            | "of"
562            | "to"
563            | "at"
564            | "by"
565            | "must"
566            | "should"
567            | "can"
568            | "may"
569            | "will"
570            | "exists"
571            | "unique"
572            | "valid"
573            | "always"
574            | "never"
575            | "updated"
576            | "set"
577            | "contains"
578            | "matches"
579    )
580}
581
582// ── Phase 3.3: Determinism Checker ────────────────────────
583
584/// Check for non-deterministic patterns in contract text fields.
585fn verify_determinism(ast: &ContractNode, result: &mut VerificationResult) {
586    // Patterns that suggest non-determinism
587    let nondeterministic_patterns = [
588        // Randomness
589        ("random", "randomness usage"),
590        ("rand(", "random function call"),
591        ("Math.random", "random function call"),
592        ("uuid_generate", "runtime UUID generation"),
593        ("generate_id", "runtime ID generation"),
594        // System time
595        ("now()", "system time access"),
596        ("current_time", "system time access"),
597        ("system_time", "system time access"),
598        ("Date.now", "system time access"),
599        ("time.time", "system time access"),
600        ("Instant::now", "system time access"),
601        // External I/O
602        ("fetch(", "external I/O"),
603        ("http_request", "external I/O"),
604        ("read_file", "external I/O"),
605        ("write_file", "external I/O"),
606        ("network_call", "external I/O"),
607        ("socket", "external I/O"),
608        // Hash iteration
609        ("HashMap", "non-deterministic hash iteration"),
610        ("HashSet", "non-deterministic hash iteration"),
611        ("dict_keys", "non-deterministic hash iteration"),
612    ];
613
614    // Check operation preconditions, postconditions, side_effects
615    for op in &ast.behavioral_semantics.operations {
616        check_string_for_nondeterminism(
617            &op.precondition.value,
618            &format!("operation '{}' precondition", op.name.value),
619            &op.precondition.span,
620            &nondeterministic_patterns,
621            result,
622        );
623        check_string_for_nondeterminism(
624            &op.postcondition.value,
625            &format!("operation '{}' postcondition", op.name.value),
626            &op.postcondition.span,
627            &nondeterministic_patterns,
628            result,
629        );
630        for se in &op.side_effects {
631            check_string_for_nondeterminism(
632                &se.value,
633                &format!("operation '{}' side_effect", op.name.value),
634                &se.span,
635                &nondeterministic_patterns,
636                result,
637            );
638        }
639        check_string_for_nondeterminism(
640            &op.idempotence.value,
641            &format!("operation '{}' idempotence", op.name.value),
642            &op.idempotence.span,
643            &nondeterministic_patterns,
644            result,
645        );
646    }
647
648    // Check invariants for non-deterministic patterns
649    for inv in &ast.data_semantics.invariants {
650        check_string_for_nondeterminism(
651            &inv.value,
652            "invariant",
653            &inv.span,
654            &nondeterministic_patterns,
655            result,
656        );
657    }
658}
659
660/// Check a string for non-deterministic patterns
661fn check_string_for_nondeterminism(
662    text: &str,
663    context: &str,
664    span: &Span,
665    patterns: &[(&str, &str)],
666    result: &mut VerificationResult,
667) {
668    let lower = text.to_lowercase();
669    for &(pattern, description) in patterns {
670        if lower.contains(&pattern.to_lowercase()) {
671            result.add_error(
672                DiagnosticKind::DeterminismViolation,
673                format!(
674                    "{} detected in {}: text contains '{}'",
675                    description, context, pattern,
676                ),
677                Some(span.clone()),
678            );
679        }
680    }
681}
682
683// ── Phase 3.4: Coherence Verifier ─────────────────────────
684
685/// Check structural coherence of the contract.
686fn verify_coherence(ast: &ContractNode, result: &mut VerificationResult) {
687    // Check unique operation names
688    verify_unique_operation_names(ast, result);
689
690    // Check unique state field names
691    verify_unique_state_fields(ast, result);
692
693    // Check sandbox_mode is a known value
694    verify_sandbox_mode(ast, result);
695
696    // Check trigger_types are known values
697    verify_trigger_types(ast, result);
698
699    // Check operations reference valid state fields in pre/postconditions
700    verify_operation_field_references(ast, result);
701
702    // Check extension namespace isolation
703    verify_extension_namespaces(ast, result);
704}
705
706/// Verify operation names are unique
707fn verify_unique_operation_names(ast: &ContractNode, result: &mut VerificationResult) {
708    let mut seen = BTreeSet::new();
709    for op in &ast.behavioral_semantics.operations {
710        if !seen.insert(&op.name.value) {
711            result.add_error(
712                DiagnosticKind::CoherenceError,
713                format!("duplicate operation name '{}'", op.name.value),
714                Some(op.name.span.clone()),
715            );
716        }
717    }
718}
719
720/// Verify state field names are unique
721fn verify_unique_state_fields(ast: &ContractNode, result: &mut VerificationResult) {
722    let mut seen = BTreeSet::new();
723    for field in &ast.data_semantics.state {
724        if !seen.insert(&field.name.value) {
725            result.add_error(
726                DiagnosticKind::CoherenceError,
727                format!("duplicate state field name '{}'", field.name.value),
728                Some(field.name.span.clone()),
729            );
730        }
731    }
732}
733
734/// Verify sandbox_mode is a recognized value
735fn verify_sandbox_mode(ast: &ContractNode, result: &mut VerificationResult) {
736    let valid_modes = ["full_isolation", "restricted", "none"];
737    let mode = &ast.execution_constraints.sandbox_mode.value;
738    if !valid_modes.contains(&mode.as_str()) {
739        result.add_warning(
740            DiagnosticKind::CoherenceError,
741            format!(
742                "unrecognized sandbox_mode '{}', expected one of: {}",
743                mode,
744                valid_modes.join(", ")
745            ),
746            Some(ast.execution_constraints.sandbox_mode.span.clone()),
747        );
748    }
749}
750
751/// Verify trigger_types contain recognized values
752fn verify_trigger_types(ast: &ContractNode, result: &mut VerificationResult) {
753    let valid_types = ["manual", "time_based", "event_based"];
754    for tt in &ast.execution_constraints.trigger_types {
755        if !valid_types.contains(&tt.value.as_str()) {
756            result.add_warning(
757                DiagnosticKind::CoherenceError,
758                format!(
759                    "unrecognized trigger_type '{}', expected one of: {}",
760                    tt.value,
761                    valid_types.join(", ")
762                ),
763                Some(tt.span.clone()),
764            );
765        }
766    }
767}
768
769/// Verify operation pre/postconditions reference valid state fields
770fn verify_operation_field_references(ast: &ContractNode, result: &mut VerificationResult) {
771    let state_field_names: BTreeSet<&str> = ast
772        .data_semantics
773        .state
774        .iter()
775        .map(|f| f.name.value.as_str())
776        .collect();
777
778    for op in &ast.behavioral_semantics.operations {
779        // Check precondition references
780        let pre_idents = extract_identifiers(&op.precondition.value);
781        for ident in &pre_idents {
782            if looks_like_field_ref(ident) && !state_field_names.contains(ident.as_str()) {
783                // Only warn — preconditions may reference parameters too
784                let param_names: BTreeSet<&str> = op
785                    .parameters
786                    .iter()
787                    .map(|p| p.name.value.as_str())
788                    .collect();
789                if !param_names.contains(ident.as_str()) {
790                    result.add_warning(
791                        DiagnosticKind::CoherenceError,
792                        format!(
793                            "precondition of '{}' references unknown field '{}'",
794                            op.name.value, ident,
795                        ),
796                        Some(op.precondition.span.clone()),
797                    );
798                }
799            }
800        }
801
802        // Check postcondition references
803        let post_idents = extract_identifiers(&op.postcondition.value);
804        for ident in &post_idents {
805            if looks_like_field_ref(ident) && !state_field_names.contains(ident.as_str()) {
806                let param_names: BTreeSet<&str> = op
807                    .parameters
808                    .iter()
809                    .map(|p| p.name.value.as_str())
810                    .collect();
811                if !param_names.contains(ident.as_str()) {
812                    result.add_warning(
813                        DiagnosticKind::CoherenceError,
814                        format!(
815                            "postcondition of '{}' references unknown field '{}'",
816                            op.name.value, ident,
817                        ),
818                        Some(op.postcondition.span.clone()),
819                    );
820                }
821            }
822        }
823    }
824}
825
826/// Check if an identifier looks like a field reference (snake_case, not a common word)
827fn looks_like_field_ref(ident: &str) -> bool {
828    // Must be lowercase with underscores, at least 2 chars
829    ident.len() >= 2
830        && ident
831            .chars()
832            .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
833        && ident.contains('_')
834}
835
836/// Verify extension namespaces are unique
837fn verify_extension_namespaces(ast: &ContractNode, result: &mut VerificationResult) {
838    if let Some(ref ext) = ast.extensions {
839        let mut seen = BTreeSet::new();
840        for system in &ext.systems {
841            if !seen.insert(&system.name.value) {
842                result.add_error(
843                    DiagnosticKind::CoherenceError,
844                    format!("duplicate extension namespace '{}'", system.name.value),
845                    Some(system.name.span.clone()),
846                );
847            }
848        }
849    }
850}
851
852// ── Helpers ───────────────────────────────────────────────
853
854/// Human-readable name for a type expression
855fn type_expr_name(type_expr: &TypeExpression) -> String {
856    match type_expr {
857        TypeExpression::Primitive(pt, _) => pt.to_string(),
858        TypeExpression::Array(inner, _) => format!("Array<{}>", type_expr_name(inner)),
859        TypeExpression::Map(k, v, _) => {
860            format!("Map<{}, {}>", type_expr_name(k), type_expr_name(v))
861        }
862        TypeExpression::Object(_, _) => "Object".to_string(),
863        TypeExpression::Enum(_, _) => "Enum".to_string(),
864    }
865}
866
867/// Human-readable name for a literal value type
868fn literal_type_name(lit: &LiteralValue) -> String {
869    match lit {
870        LiteralValue::String(_, _) => "String".to_string(),
871        LiteralValue::Integer(_, _) => "Integer".to_string(),
872        LiteralValue::Float(_, _) => "Float".to_string(),
873        LiteralValue::Boolean(_, _) => "Boolean".to_string(),
874        LiteralValue::Array(_, _) => "Array".to_string(),
875    }
876}
877
878/// Get the span of a literal value
879fn literal_span(lit: &LiteralValue) -> Span {
880    match lit {
881        LiteralValue::String(_, s) => s.clone(),
882        LiteralValue::Integer(_, s) => s.clone(),
883        LiteralValue::Float(_, s) => s.clone(),
884        LiteralValue::Boolean(_, s) => s.clone(),
885        LiteralValue::Array(_, s) => s.clone(),
886    }
887}
888
889// ── Tests ─────────────────────────────────────────────────
890
891#[cfg(test)]
892mod tests {
893    use super::*;
894    use crate::parser::parse;
895
896    // ── Helper: parse and verify ──────────────────────────
897
898    fn parse_and_verify(input: &str) -> VerificationResult {
899        let ast = parse(input).expect("test input should parse");
900        verify(&ast)
901    }
902
903    // ── Phase 3.1: Type Checker Tests ─────────────────────
904
905    #[test]
906    fn test_valid_minimal_contract() {
907        let result = parse_and_verify(include_str!(
908            "../../../tests/fixtures/conformance/valid/minimal-contract.icl"
909        ));
910        assert!(
911            result.is_valid(),
912            "minimal contract should verify: {:?}",
913            result.errors()
914        );
915    }
916
917    #[test]
918    fn test_valid_all_primitive_types() {
919        let result = parse_and_verify(include_str!(
920            "../../../tests/fixtures/conformance/valid/all-primitive-types.icl"
921        ));
922        assert!(
923            result.is_valid(),
924            "all-primitive-types should verify: {:?}",
925            result.errors()
926        );
927    }
928
929    #[test]
930    fn test_valid_composite_types() {
931        let result = parse_and_verify(include_str!(
932            "../../../tests/fixtures/conformance/valid/composite-types.icl"
933        ));
934        assert!(
935            result.is_valid(),
936            "composite-types should verify: {:?}",
937            result.errors()
938        );
939    }
940
941    #[test]
942    fn test_valid_multiple_operations() {
943        let result = parse_and_verify(include_str!(
944            "../../../tests/fixtures/conformance/valid/multiple-operations.icl"
945        ));
946        assert!(
947            result.is_valid(),
948            "multiple-operations should verify: {:?}",
949            result.errors()
950        );
951    }
952
953    #[test]
954    fn test_integer_type_valid_default() {
955        let input = make_contract_with_state("count: Integer = 42");
956        let result = parse_and_verify(&input);
957        assert!(
958            result.is_valid(),
959            "Integer default 42 should be valid: {:?}",
960            result.errors()
961        );
962    }
963
964    #[test]
965    fn test_float_type_valid_default() {
966        let input = make_contract_with_state("ratio: Float = 3.14");
967        let result = parse_and_verify(&input);
968        assert!(
969            result.is_valid(),
970            "Float default 3.14 should be valid: {:?}",
971            result.errors()
972        );
973    }
974
975    #[test]
976    fn test_float_type_integer_default_allowed() {
977        let input = make_contract_with_state("ratio: Float = 0");
978        let result = parse_and_verify(&input);
979        assert!(
980            result.is_valid(),
981            "Integer literal as Float default should be allowed: {:?}",
982            result.errors()
983        );
984    }
985
986    #[test]
987    fn test_string_type_valid_default() {
988        let input = make_contract_with_state("label: String = \"hello\"");
989        let result = parse_and_verify(&input);
990        assert!(
991            result.is_valid(),
992            "String default should be valid: {:?}",
993            result.errors()
994        );
995    }
996
997    #[test]
998    fn test_boolean_type_valid_default() {
999        let input = make_contract_with_state("active: Boolean = true");
1000        let result = parse_and_verify(&input);
1001        assert!(
1002            result.is_valid(),
1003            "Boolean default should be valid: {:?}",
1004            result.errors()
1005        );
1006    }
1007
1008    #[test]
1009    fn test_type_mismatch_string_for_integer() {
1010        let input = make_contract_with_state("count: Integer = \"hello\"");
1011        let result = parse_and_verify(&input);
1012        assert!(!result.is_valid(), "String default for Integer should fail");
1013        assert!(
1014            result
1015                .errors()
1016                .iter()
1017                .any(|d| d.kind == DiagnosticKind::TypeError),
1018            "Should produce TypeError"
1019        );
1020    }
1021
1022    #[test]
1023    fn test_type_mismatch_integer_for_string() {
1024        let input = make_contract_with_state("label: String = 42");
1025        let result = parse_and_verify(&input);
1026        assert!(!result.is_valid(), "Integer default for String should fail");
1027    }
1028
1029    #[test]
1030    fn test_type_mismatch_boolean_for_integer() {
1031        let input = make_contract_with_state("count: Integer = true");
1032        let result = parse_and_verify(&input);
1033        assert!(
1034            !result.is_valid(),
1035            "Boolean default for Integer should fail"
1036        );
1037    }
1038
1039    #[test]
1040    fn test_type_mismatch_string_for_boolean() {
1041        let input = make_contract_with_state("active: Boolean = \"yes\"");
1042        let result = parse_and_verify(&input);
1043        assert!(!result.is_valid(), "String default for Boolean should fail");
1044    }
1045
1046    #[test]
1047    fn test_enum_valid_default() {
1048        let input =
1049            make_contract_with_state("status: Enum [\"active\", \"inactive\"] = \"active\"");
1050        let result = parse_and_verify(&input);
1051        assert!(
1052            result.is_valid(),
1053            "Valid Enum default should pass: {:?}",
1054            result.errors()
1055        );
1056    }
1057
1058    #[test]
1059    fn test_enum_invalid_default() {
1060        let input =
1061            make_contract_with_state("status: Enum [\"active\", \"inactive\"] = \"unknown\"");
1062        let result = parse_and_verify(&input);
1063        assert!(!result.is_valid(), "Invalid Enum default should fail");
1064    }
1065
1066    #[test]
1067    fn test_enum_duplicate_variants() {
1068        let input = make_contract_with_state("status: Enum [\"active\", \"active\", \"inactive\"]");
1069        let result = parse_and_verify(&input);
1070        assert!(
1071            result
1072                .errors()
1073                .iter()
1074                .any(|d| d.message.contains("duplicate Enum variant")),
1075            "Should detect duplicate Enum variants"
1076        );
1077    }
1078
1079    #[test]
1080    fn test_object_duplicate_fields() {
1081        let input = make_contract_with_state("data: Object { name: String, name: Integer }");
1082        let result = parse_and_verify(&input);
1083        assert!(
1084            result
1085                .errors()
1086                .iter()
1087                .any(|d| d.message.contains("duplicate field name")),
1088            "Should detect duplicate Object fields"
1089        );
1090    }
1091
1092    #[test]
1093    fn test_map_float_key_rejected() {
1094        let input = make_contract_with_state("lookup: Map<Float, String>");
1095        let result = parse_and_verify(&input);
1096        assert!(
1097            result
1098                .errors()
1099                .iter()
1100                .any(|d| d.message.contains("Float cannot be used as Map key")),
1101            "Float Map keys should be rejected"
1102        );
1103    }
1104
1105    #[test]
1106    fn test_map_string_key_valid() {
1107        let input = make_contract_with_state("lookup: Map<String, Integer>");
1108        let result = parse_and_verify(&input);
1109        assert!(
1110            result.is_valid(),
1111            "String Map key should be valid: {:?}",
1112            result.errors()
1113        );
1114    }
1115
1116    #[test]
1117    fn test_array_type_valid() {
1118        let input = make_contract_with_state("items: Array<String>");
1119        let result = parse_and_verify(&input);
1120        assert!(
1121            result.is_valid(),
1122            "Array<String> should be valid: {:?}",
1123            result.errors()
1124        );
1125    }
1126
1127    #[test]
1128    fn test_nested_collection_types() {
1129        let input = make_contract_with_state("matrix: Array<Array<Integer>>");
1130        let result = parse_and_verify(&input);
1131        assert!(
1132            result.is_valid(),
1133            "Nested Array should be valid: {:?}",
1134            result.errors()
1135        );
1136    }
1137
1138    #[test]
1139    fn test_confidence_level_out_of_range_high() {
1140        // Parser already validates confidence_level in [0.0, 1.0]
1141        let input = make_contract_with_confidence("1.5");
1142        assert!(
1143            parse(&input).is_err(),
1144            "confidence_level 1.5 should fail at parse"
1145        );
1146    }
1147
1148    #[test]
1149    fn test_confidence_level_out_of_range_low() {
1150        // Verifier catches out-of-range on a constructed AST
1151        let mut ast = make_valid_ast();
1152        ast.purpose_statement.confidence_level = SpannedValue::new(-0.1, dummy_span());
1153        let result = verify(&ast);
1154        assert!(!result.is_valid(), "confidence_level -0.1 should fail");
1155        assert!(
1156            result
1157                .errors()
1158                .iter()
1159                .any(|d| d.message.contains("confidence_level")),
1160            "Should mention confidence_level: {:?}",
1161            result.errors()
1162        );
1163    }
1164
1165    #[test]
1166    fn test_confidence_level_boundary_zero() {
1167        let input = make_contract_with_confidence("0.0");
1168        let result = parse_and_verify(&input);
1169        assert!(
1170            result.is_valid(),
1171            "confidence_level 0.0 should be valid: {:?}",
1172            result.errors()
1173        );
1174    }
1175
1176    #[test]
1177    fn test_confidence_level_boundary_one() {
1178        let input = make_contract_with_confidence("1.0");
1179        let result = parse_and_verify(&input);
1180        assert!(
1181            result.is_valid(),
1182            "confidence_level 1.0 should be valid: {:?}",
1183            result.errors()
1184        );
1185    }
1186
1187    #[test]
1188    fn test_negative_version() {
1189        // Verifier catches negative version on constructed AST
1190        let mut ast = make_valid_ast();
1191        ast.identity.version = SpannedValue::new(-1, dummy_span());
1192        let result = verify(&ast);
1193        assert!(!result.is_valid(), "negative version should fail");
1194        assert!(
1195            result
1196                .errors()
1197                .iter()
1198                .any(|d| d.message.contains("version")),
1199            "Should mention version: {:?}",
1200            result.errors()
1201        );
1202    }
1203
1204    #[test]
1205    fn test_negative_resource_limits() {
1206        // Verifier catches negative resource limits on constructed AST
1207        let mut ast = make_valid_ast();
1208        ast.execution_constraints.resource_limits.max_memory_bytes =
1209            SpannedValue::new(-1, dummy_span());
1210        let result = verify(&ast);
1211        assert!(!result.is_valid(), "negative max_memory_bytes should fail");
1212        assert!(
1213            result
1214                .errors()
1215                .iter()
1216                .any(|d| d.message.contains("max_memory_bytes")),
1217            "Should mention max_memory_bytes: {:?}",
1218            result.errors()
1219        );
1220    }
1221
1222    #[test]
1223    fn test_zero_timeout() {
1224        let input = make_contract_with_resource_limits(1048576, 0, 1048576);
1225        let result = parse_and_verify(&input);
1226        assert!(
1227            !result.is_valid(),
1228            "zero computation_timeout_ms should fail"
1229        );
1230    }
1231
1232    #[test]
1233    fn test_valid_stable_id() {
1234        let input = make_contract_with_stable_id("ic-test-001");
1235        let result = parse_and_verify(&input);
1236        assert!(result.is_valid(), "valid stable_id: {:?}", result.errors());
1237    }
1238
1239    #[test]
1240    fn test_invalid_stable_id_uppercase() {
1241        let input = make_contract_with_stable_id("IC-TEST-001");
1242        let result = parse_and_verify(&input);
1243        assert!(!result.is_valid(), "uppercase stable_id should fail");
1244    }
1245
1246    #[test]
1247    fn test_invalid_stable_id_starts_with_dash() {
1248        let input = make_contract_with_stable_id("-invalid");
1249        let result = parse_and_verify(&input);
1250        assert!(!result.is_valid(), "dash-start stable_id should fail");
1251    }
1252
1253    #[test]
1254    fn test_invalid_semantic_hash() {
1255        let input = make_contract_with_hash("not-hex-at-all!");
1256        let result = parse_and_verify(&input);
1257        assert!(!result.is_valid(), "non-hex hash should fail");
1258    }
1259
1260    // ── Phase 3.2: Invariant Verifier Tests ───────────────
1261
1262    #[test]
1263    fn test_invariant_references_valid_field() {
1264        let input = make_contract_with_state_and_invariants("count: Integer = 0", &["count >= 0"]);
1265        let result = parse_and_verify(&input);
1266        // Should not warn about unreferenced fields
1267        assert!(
1268            !result
1269                .warnings()
1270                .iter()
1271                .any(|d| d.kind == DiagnosticKind::InvariantError),
1272            "Valid field reference should not warn: {:?}",
1273            result.warnings()
1274        );
1275    }
1276
1277    #[test]
1278    fn test_duplicate_invariant_warning() {
1279        let input = make_contract_with_state_and_invariants(
1280            "count: Integer = 0",
1281            &["count >= 0", "count >= 0"],
1282        );
1283        let result = parse_and_verify(&input);
1284        assert!(
1285            result
1286                .warnings()
1287                .iter()
1288                .any(|d| d.message.contains("duplicate invariant")),
1289            "Should warn about duplicate invariants"
1290        );
1291    }
1292
1293    // ── Phase 3.3: Determinism Checker Tests ──────────────
1294
1295    #[test]
1296    fn test_detect_randomness_in_precondition() {
1297        let input = make_contract_with_operation("random_op", "random() > 0.5", "result set");
1298        let result = parse_and_verify(&input);
1299        assert!(
1300            result
1301                .errors()
1302                .iter()
1303                .any(|d| d.kind == DiagnosticKind::DeterminismViolation),
1304            "Should detect randomness: {:?}",
1305            result.diagnostics
1306        );
1307    }
1308
1309    #[test]
1310    fn test_detect_system_time_in_postcondition() {
1311        let input = make_contract_with_operation("time_op", "true", "timestamp = now()");
1312        let result = parse_and_verify(&input);
1313        assert!(
1314            result
1315                .errors()
1316                .iter()
1317                .any(|d| d.kind == DiagnosticKind::DeterminismViolation),
1318            "Should detect system time: {:?}",
1319            result.diagnostics
1320        );
1321    }
1322
1323    #[test]
1324    fn test_detect_external_io() {
1325        let input = make_contract_with_operation("io_op", "true", "data = fetch(url)");
1326        let result = parse_and_verify(&input);
1327        assert!(
1328            result
1329                .errors()
1330                .iter()
1331                .any(|d| d.kind == DiagnosticKind::DeterminismViolation),
1332            "Should detect external I/O: {:?}",
1333            result.diagnostics
1334        );
1335    }
1336
1337    #[test]
1338    fn test_detect_hashmap_usage() {
1339        let input = make_contract_with_operation("hash_op", "true", "HashMap iteration order");
1340        let result = parse_and_verify(&input);
1341        assert!(
1342            result
1343                .errors()
1344                .iter()
1345                .any(|d| d.kind == DiagnosticKind::DeterminismViolation),
1346            "Should detect HashMap: {:?}",
1347            result.diagnostics
1348        );
1349    }
1350
1351    #[test]
1352    fn test_clean_operation_no_determinism_violation() {
1353        let input = make_contract_with_operation("clean_op", "count >= 0", "count updated");
1354        let result = parse_and_verify(&input);
1355        assert!(
1356            !result
1357                .errors()
1358                .iter()
1359                .any(|d| d.kind == DiagnosticKind::DeterminismViolation),
1360            "Clean operation should have no determinism violations: {:?}",
1361            result.errors()
1362        );
1363    }
1364
1365    // ── Phase 3.4: Coherence Verifier Tests ───────────────
1366
1367    #[test]
1368    fn test_duplicate_operation_names() {
1369        let input = make_contract_with_two_ops("update_count", "update_count");
1370        let result = parse_and_verify(&input);
1371        assert!(
1372            result
1373                .errors()
1374                .iter()
1375                .any(|d| d.message.contains("duplicate operation name")),
1376            "Should detect duplicate operation names: {:?}",
1377            result.diagnostics
1378        );
1379    }
1380
1381    #[test]
1382    fn test_unique_operation_names() {
1383        let input = make_contract_with_two_ops("create_item", "delete_item");
1384        let result = parse_and_verify(&input);
1385        assert!(
1386            !result
1387                .errors()
1388                .iter()
1389                .any(|d| d.message.contains("duplicate operation name")),
1390            "Unique operation names should pass: {:?}",
1391            result.errors()
1392        );
1393    }
1394
1395    #[test]
1396    fn test_duplicate_state_fields() {
1397        let input = make_contract_with_state("count: Integer, count: String");
1398        let result = parse_and_verify(&input);
1399        assert!(
1400            result
1401                .errors()
1402                .iter()
1403                .any(|d| d.message.contains("duplicate state field")),
1404            "Should detect duplicate state fields: {:?}",
1405            result.diagnostics
1406        );
1407    }
1408
1409    #[test]
1410    fn test_unknown_sandbox_mode_warning() {
1411        let input = make_contract_with_sandbox_mode("super_isolated");
1412        let result = parse_and_verify(&input);
1413        assert!(
1414            result
1415                .warnings()
1416                .iter()
1417                .any(|d| d.message.contains("sandbox_mode")),
1418            "Unknown sandbox_mode should warn: {:?}",
1419            result.diagnostics
1420        );
1421    }
1422
1423    #[test]
1424    fn test_valid_sandbox_modes() {
1425        for mode in &["full_isolation", "restricted", "none"] {
1426            let input = make_contract_with_sandbox_mode(mode);
1427            let result = parse_and_verify(&input);
1428            assert!(
1429                !result
1430                    .warnings()
1431                    .iter()
1432                    .any(|d| d.message.contains("sandbox_mode")),
1433                "sandbox_mode '{}' should not warn: {:?}",
1434                mode,
1435                result.warnings()
1436            );
1437        }
1438    }
1439
1440    #[test]
1441    fn test_unknown_trigger_type_warning() {
1442        let input = make_contract_with_trigger_types(&["cron_job"]);
1443        let result = parse_and_verify(&input);
1444        assert!(
1445            result
1446                .warnings()
1447                .iter()
1448                .any(|d| d.message.contains("trigger_type")),
1449            "Unknown trigger_type should warn: {:?}",
1450            result.diagnostics
1451        );
1452    }
1453
1454    #[test]
1455    fn test_valid_trigger_types() {
1456        let input = make_contract_with_trigger_types(&["manual", "time_based", "event_based"]);
1457        let result = parse_and_verify(&input);
1458        assert!(
1459            !result
1460                .warnings()
1461                .iter()
1462                .any(|d| d.message.contains("trigger_type")),
1463            "Known trigger_types should not warn: {:?}",
1464            result.warnings()
1465        );
1466    }
1467
1468    // ── Conformance Suite ─────────────────────────────────
1469
1470    #[test]
1471    fn test_conformance_valid_all_pass_verification() {
1472        let fixtures = [
1473            include_str!("../../../tests/fixtures/conformance/valid/minimal-contract.icl"),
1474            include_str!("../../../tests/fixtures/conformance/valid/all-primitive-types.icl"),
1475            include_str!("../../../tests/fixtures/conformance/valid/composite-types.icl"),
1476            include_str!("../../../tests/fixtures/conformance/valid/multiple-operations.icl"),
1477        ];
1478        for (i, fixture) in fixtures.iter().enumerate() {
1479            let result = parse_and_verify(fixture);
1480            assert!(
1481                result.is_valid(),
1482                "conformance fixture {} should verify: {:?}",
1483                i,
1484                result.errors()
1485            );
1486        }
1487    }
1488
1489    // ── Determinism Tests ─────────────────────────────────
1490
1491    #[test]
1492    fn test_verification_determinism_100_iterations() {
1493        let input =
1494            include_str!("../../../tests/fixtures/conformance/valid/all-primitive-types.icl");
1495        let ast = parse(input).expect("should parse");
1496
1497        let first = verify(&ast);
1498        let first_count = first.diagnostics.len();
1499        let first_valid = first.is_valid();
1500
1501        for i in 0..100 {
1502            let result = verify(&ast);
1503            assert_eq!(
1504                result.diagnostics.len(),
1505                first_count,
1506                "Determinism failure at iteration {}: diagnostic count differs",
1507                i
1508            );
1509            assert_eq!(
1510                result.is_valid(),
1511                first_valid,
1512                "Determinism failure at iteration {}: validity differs",
1513                i
1514            );
1515            // Compare each diagnostic message
1516            for (j, (a, b)) in first
1517                .diagnostics
1518                .iter()
1519                .zip(result.diagnostics.iter())
1520                .enumerate()
1521            {
1522                assert_eq!(
1523                    a.message, b.message,
1524                    "Determinism failure at iteration {}, diagnostic {}: messages differ",
1525                    i, j
1526                );
1527                assert_eq!(
1528                    a.severity, b.severity,
1529                    "Determinism failure at iteration {}, diagnostic {}: severities differ",
1530                    i, j
1531                );
1532            }
1533        }
1534    }
1535
1536    #[test]
1537    fn test_verification_determinism_complex_contract() {
1538        let input =
1539            include_str!("../../../tests/fixtures/conformance/valid/multiple-operations.icl");
1540        let ast = parse(input).expect("should parse");
1541
1542        let first = verify(&ast);
1543        for i in 0..100 {
1544            let result = verify(&ast);
1545            assert_eq!(
1546                result.diagnostics.len(),
1547                first.diagnostics.len(),
1548                "Determinism failure at iteration {} on complex contract",
1549                i
1550            );
1551        }
1552    }
1553
1554    // ── Test Helpers ──────────────────────────────────────
1555
1556    fn make_contract_with_state(state_fields: &str) -> String {
1557        format!(
1558            r#"Contract {{
1559  Identity {{
1560    stable_id: "ic-test-001",
1561    version: 1,
1562    created_timestamp: 2026-02-01T00:00:00Z,
1563    owner: "test",
1564    semantic_hash: "0000000000000000"
1565  }}
1566  PurposeStatement {{
1567    narrative: "Test contract",
1568    intent_source: "test",
1569    confidence_level: 1.0
1570  }}
1571  DataSemantics {{
1572    state: {{
1573      {}
1574    }},
1575    invariants: []
1576  }}
1577  BehavioralSemantics {{
1578    operations: []
1579  }}
1580  ExecutionConstraints {{
1581    trigger_types: ["manual"],
1582    resource_limits: {{
1583      max_memory_bytes: 1048576,
1584      computation_timeout_ms: 100,
1585      max_state_size_bytes: 1048576
1586    }},
1587    external_permissions: [],
1588    sandbox_mode: "full_isolation"
1589  }}
1590  HumanMachineContract {{
1591    system_commitments: [],
1592    system_refusals: [],
1593    user_obligations: []
1594  }}
1595}}"#,
1596            state_fields
1597        )
1598    }
1599
1600    fn make_contract_with_state_and_invariants(state_fields: &str, invariants: &[&str]) -> String {
1601        let inv_str = invariants
1602            .iter()
1603            .map(|i| format!("\"{}\"", i))
1604            .collect::<Vec<_>>()
1605            .join(", ");
1606        format!(
1607            r#"Contract {{
1608  Identity {{
1609    stable_id: "ic-test-001",
1610    version: 1,
1611    created_timestamp: 2026-02-01T00:00:00Z,
1612    owner: "test",
1613    semantic_hash: "0000000000000000"
1614  }}
1615  PurposeStatement {{
1616    narrative: "Test contract",
1617    intent_source: "test",
1618    confidence_level: 1.0
1619  }}
1620  DataSemantics {{
1621    state: {{
1622      {}
1623    }},
1624    invariants: [{}]
1625  }}
1626  BehavioralSemantics {{
1627    operations: []
1628  }}
1629  ExecutionConstraints {{
1630    trigger_types: ["manual"],
1631    resource_limits: {{
1632      max_memory_bytes: 1048576,
1633      computation_timeout_ms: 100,
1634      max_state_size_bytes: 1048576
1635    }},
1636    external_permissions: [],
1637    sandbox_mode: "full_isolation"
1638  }}
1639  HumanMachineContract {{
1640    system_commitments: [],
1641    system_refusals: [],
1642    user_obligations: []
1643  }}
1644}}"#,
1645            state_fields, inv_str
1646        )
1647    }
1648
1649    fn make_contract_with_confidence(level: &str) -> String {
1650        format!(
1651            r#"Contract {{
1652  Identity {{
1653    stable_id: "ic-test-001",
1654    version: 1,
1655    created_timestamp: 2026-02-01T00:00:00Z,
1656    owner: "test",
1657    semantic_hash: "0000000000000000"
1658  }}
1659  PurposeStatement {{
1660    narrative: "Test contract",
1661    intent_source: "test",
1662    confidence_level: {}
1663  }}
1664  DataSemantics {{
1665    state: {{
1666      value: String
1667    }},
1668    invariants: []
1669  }}
1670  BehavioralSemantics {{
1671    operations: []
1672  }}
1673  ExecutionConstraints {{
1674    trigger_types: ["manual"],
1675    resource_limits: {{
1676      max_memory_bytes: 1048576,
1677      computation_timeout_ms: 100,
1678      max_state_size_bytes: 1048576
1679    }},
1680    external_permissions: [],
1681    sandbox_mode: "full_isolation"
1682  }}
1683  HumanMachineContract {{
1684    system_commitments: [],
1685    system_refusals: [],
1686    user_obligations: []
1687  }}
1688}}"#,
1689            level
1690        )
1691    }
1692
1693    fn make_contract_with_resource_limits(mem: i64, timeout: i64, state: i64) -> String {
1694        format!(
1695            r#"Contract {{
1696  Identity {{
1697    stable_id: "ic-test-001",
1698    version: 1,
1699    created_timestamp: 2026-02-01T00:00:00Z,
1700    owner: "test",
1701    semantic_hash: "0000000000000000"
1702  }}
1703  PurposeStatement {{
1704    narrative: "Test contract",
1705    intent_source: "test",
1706    confidence_level: 1.0
1707  }}
1708  DataSemantics {{
1709    state: {{
1710      value: String
1711    }},
1712    invariants: []
1713  }}
1714  BehavioralSemantics {{
1715    operations: []
1716  }}
1717  ExecutionConstraints {{
1718    trigger_types: ["manual"],
1719    resource_limits: {{
1720      max_memory_bytes: {},
1721      computation_timeout_ms: {},
1722      max_state_size_bytes: {}
1723    }},
1724    external_permissions: [],
1725    sandbox_mode: "full_isolation"
1726  }}
1727  HumanMachineContract {{
1728    system_commitments: [],
1729    system_refusals: [],
1730    user_obligations: []
1731  }}
1732}}"#,
1733            mem, timeout, state
1734        )
1735    }
1736
1737    fn make_contract_with_stable_id(id: &str) -> String {
1738        format!(
1739            r#"Contract {{
1740  Identity {{
1741    stable_id: "{}",
1742    version: 1,
1743    created_timestamp: 2026-02-01T00:00:00Z,
1744    owner: "test",
1745    semantic_hash: "0000000000000000"
1746  }}
1747  PurposeStatement {{
1748    narrative: "Test contract",
1749    intent_source: "test",
1750    confidence_level: 1.0
1751  }}
1752  DataSemantics {{
1753    state: {{
1754      value: String
1755    }},
1756    invariants: []
1757  }}
1758  BehavioralSemantics {{
1759    operations: []
1760  }}
1761  ExecutionConstraints {{
1762    trigger_types: ["manual"],
1763    resource_limits: {{
1764      max_memory_bytes: 1048576,
1765      computation_timeout_ms: 100,
1766      max_state_size_bytes: 1048576
1767    }},
1768    external_permissions: [],
1769    sandbox_mode: "full_isolation"
1770  }}
1771  HumanMachineContract {{
1772    system_commitments: [],
1773    system_refusals: [],
1774    user_obligations: []
1775  }}
1776}}"#,
1777            id
1778        )
1779    }
1780
1781    fn make_contract_with_hash(hash: &str) -> String {
1782        format!(
1783            r#"Contract {{
1784  Identity {{
1785    stable_id: "ic-test-001",
1786    version: 1,
1787    created_timestamp: 2026-02-01T00:00:00Z,
1788    owner: "test",
1789    semantic_hash: "{}"
1790  }}
1791  PurposeStatement {{
1792    narrative: "Test contract",
1793    intent_source: "test",
1794    confidence_level: 1.0
1795  }}
1796  DataSemantics {{
1797    state: {{
1798      value: String
1799    }},
1800    invariants: []
1801  }}
1802  BehavioralSemantics {{
1803    operations: []
1804  }}
1805  ExecutionConstraints {{
1806    trigger_types: ["manual"],
1807    resource_limits: {{
1808      max_memory_bytes: 1048576,
1809      computation_timeout_ms: 100,
1810      max_state_size_bytes: 1048576
1811    }},
1812    external_permissions: [],
1813    sandbox_mode: "full_isolation"
1814  }}
1815  HumanMachineContract {{
1816    system_commitments: [],
1817    system_refusals: [],
1818    user_obligations: []
1819  }}
1820}}"#,
1821            hash
1822        )
1823    }
1824
1825    fn make_contract_with_operation(name: &str, precondition: &str, postcondition: &str) -> String {
1826        format!(
1827            r#"Contract {{
1828  Identity {{
1829    stable_id: "ic-test-001",
1830    version: 1,
1831    created_timestamp: 2026-02-01T00:00:00Z,
1832    owner: "test",
1833    semantic_hash: "0000000000000000"
1834  }}
1835  PurposeStatement {{
1836    narrative: "Test contract",
1837    intent_source: "test",
1838    confidence_level: 1.0
1839  }}
1840  DataSemantics {{
1841    state: {{
1842      count: Integer = 0,
1843      result: String
1844    }},
1845    invariants: []
1846  }}
1847  BehavioralSemantics {{
1848    operations: [
1849      {{
1850        name: "{}",
1851        precondition: "{}",
1852        parameters: {{}},
1853        postcondition: "{}",
1854        side_effects: [],
1855        idempotence: "idempotent"
1856      }}
1857    ]
1858  }}
1859  ExecutionConstraints {{
1860    trigger_types: ["manual"],
1861    resource_limits: {{
1862      max_memory_bytes: 1048576,
1863      computation_timeout_ms: 100,
1864      max_state_size_bytes: 1048576
1865    }},
1866    external_permissions: [],
1867    sandbox_mode: "full_isolation"
1868  }}
1869  HumanMachineContract {{
1870    system_commitments: [],
1871    system_refusals: [],
1872    user_obligations: []
1873  }}
1874}}"#,
1875            name, precondition, postcondition
1876        )
1877    }
1878
1879    fn make_contract_with_two_ops(name1: &str, name2: &str) -> String {
1880        format!(
1881            r#"Contract {{
1882  Identity {{
1883    stable_id: "ic-test-001",
1884    version: 1,
1885    created_timestamp: 2026-02-01T00:00:00Z,
1886    owner: "test",
1887    semantic_hash: "0000000000000000"
1888  }}
1889  PurposeStatement {{
1890    narrative: "Test contract",
1891    intent_source: "test",
1892    confidence_level: 1.0
1893  }}
1894  DataSemantics {{
1895    state: {{
1896      count: Integer = 0
1897    }},
1898    invariants: []
1899  }}
1900  BehavioralSemantics {{
1901    operations: [
1902      {{
1903        name: "{}",
1904        precondition: "true",
1905        parameters: {{}},
1906        postcondition: "done",
1907        side_effects: [],
1908        idempotence: "idempotent"
1909      }},
1910      {{
1911        name: "{}",
1912        precondition: "true",
1913        parameters: {{}},
1914        postcondition: "done",
1915        side_effects: [],
1916        idempotence: "idempotent"
1917      }}
1918    ]
1919  }}
1920  ExecutionConstraints {{
1921    trigger_types: ["manual"],
1922    resource_limits: {{
1923      max_memory_bytes: 1048576,
1924      computation_timeout_ms: 100,
1925      max_state_size_bytes: 1048576
1926    }},
1927    external_permissions: [],
1928    sandbox_mode: "full_isolation"
1929  }}
1930  HumanMachineContract {{
1931    system_commitments: [],
1932    system_refusals: [],
1933    user_obligations: []
1934  }}
1935}}"#,
1936            name1, name2
1937        )
1938    }
1939
1940    fn make_contract_with_sandbox_mode(mode: &str) -> String {
1941        format!(
1942            r#"Contract {{
1943  Identity {{
1944    stable_id: "ic-test-001",
1945    version: 1,
1946    created_timestamp: 2026-02-01T00:00:00Z,
1947    owner: "test",
1948    semantic_hash: "0000000000000000"
1949  }}
1950  PurposeStatement {{
1951    narrative: "Test contract",
1952    intent_source: "test",
1953    confidence_level: 1.0
1954  }}
1955  DataSemantics {{
1956    state: {{
1957      value: String
1958    }},
1959    invariants: []
1960  }}
1961  BehavioralSemantics {{
1962    operations: []
1963  }}
1964  ExecutionConstraints {{
1965    trigger_types: ["manual"],
1966    resource_limits: {{
1967      max_memory_bytes: 1048576,
1968      computation_timeout_ms: 100,
1969      max_state_size_bytes: 1048576
1970    }},
1971    external_permissions: [],
1972    sandbox_mode: "{}"
1973  }}
1974  HumanMachineContract {{
1975    system_commitments: [],
1976    system_refusals: [],
1977    user_obligations: []
1978  }}
1979}}"#,
1980            mode
1981        )
1982    }
1983
1984    fn make_contract_with_trigger_types(types: &[&str]) -> String {
1985        let types_str = types
1986            .iter()
1987            .map(|t| format!("\"{}\"", t))
1988            .collect::<Vec<_>>()
1989            .join(", ");
1990        format!(
1991            r#"Contract {{
1992  Identity {{
1993    stable_id: "ic-test-001",
1994    version: 1,
1995    created_timestamp: 2026-02-01T00:00:00Z,
1996    owner: "test",
1997    semantic_hash: "0000000000000000"
1998  }}
1999  PurposeStatement {{
2000    narrative: "Test contract",
2001    intent_source: "test",
2002    confidence_level: 1.0
2003  }}
2004  DataSemantics {{
2005    state: {{
2006      value: String
2007    }},
2008    invariants: []
2009  }}
2010  BehavioralSemantics {{
2011    operations: []
2012  }}
2013  ExecutionConstraints {{
2014    trigger_types: [{}],
2015    resource_limits: {{
2016      max_memory_bytes: 1048576,
2017      computation_timeout_ms: 100,
2018      max_state_size_bytes: 1048576
2019    }},
2020    external_permissions: [],
2021    sandbox_mode: "full_isolation"
2022  }}
2023  HumanMachineContract {{
2024    system_commitments: [],
2025    system_refusals: [],
2026    user_obligations: []
2027  }}
2028}}"#,
2029            types_str
2030        )
2031    }
2032
2033    /// Create a dummy span for AST construction in tests
2034    fn dummy_span() -> Span {
2035        Span {
2036            line: 0,
2037            column: 0,
2038            offset: 0,
2039        }
2040    }
2041
2042    /// Create a minimal valid AST for direct manipulation in tests
2043    fn make_valid_ast() -> ContractNode {
2044        ContractNode {
2045            identity: IdentityNode {
2046                stable_id: SpannedValue::new("ic-test-001".to_string(), dummy_span()),
2047                version: SpannedValue::new(1, dummy_span()),
2048                created_timestamp: SpannedValue::new(
2049                    "2026-02-01T00:00:00Z".to_string(),
2050                    dummy_span(),
2051                ),
2052                owner: SpannedValue::new("test".to_string(), dummy_span()),
2053                semantic_hash: SpannedValue::new("0000000000000000".to_string(), dummy_span()),
2054                span: dummy_span(),
2055            },
2056            purpose_statement: PurposeStatementNode {
2057                narrative: SpannedValue::new("Test contract".to_string(), dummy_span()),
2058                intent_source: SpannedValue::new("test".to_string(), dummy_span()),
2059                confidence_level: SpannedValue::new(1.0, dummy_span()),
2060                span: dummy_span(),
2061            },
2062            data_semantics: DataSemanticsNode {
2063                state: vec![StateFieldNode {
2064                    name: SpannedValue::new("value".to_string(), dummy_span()),
2065                    type_expr: TypeExpression::Primitive(PrimitiveType::String, dummy_span()),
2066                    default_value: None,
2067                    span: dummy_span(),
2068                }],
2069                invariants: vec![],
2070                span: dummy_span(),
2071            },
2072            behavioral_semantics: BehavioralSemanticsNode {
2073                operations: vec![],
2074                span: dummy_span(),
2075            },
2076            execution_constraints: ExecutionConstraintsNode {
2077                trigger_types: vec![SpannedValue::new("manual".to_string(), dummy_span())],
2078                resource_limits: ResourceLimitsNode {
2079                    max_memory_bytes: SpannedValue::new(1048576, dummy_span()),
2080                    computation_timeout_ms: SpannedValue::new(100, dummy_span()),
2081                    max_state_size_bytes: SpannedValue::new(1048576, dummy_span()),
2082                    span: dummy_span(),
2083                },
2084                external_permissions: vec![],
2085                sandbox_mode: SpannedValue::new("full_isolation".to_string(), dummy_span()),
2086                span: dummy_span(),
2087            },
2088            human_machine_contract: HumanMachineContractNode {
2089                system_commitments: vec![],
2090                system_refusals: vec![],
2091                user_obligations: vec![],
2092                span: dummy_span(),
2093            },
2094            extensions: None,
2095            span: dummy_span(),
2096        }
2097    }
2098}