tensorlogic-adapters 0.1.0

Symbol tables, axis metadata, and domain masks for TensorLogic
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
//! Property-based tests for tensorlogic-adapters.
//!
//! These tests use proptest to verify invariants across randomly generated inputs.

use proptest::prelude::*;
use tensorlogic_adapters::{
    DependentType,
    DependentTypeContext,
    DimExpr,
    DomainHierarchy,
    DomainInfo,
    Effect,
    EffectSet,
    EvolutionAnalyzer,
    LinearContext,
    LinearKind,
    LinearType,
    MemoryDatabase,
    PredicateInfo,
    PredicateQuery,
    QueryPlanner,
    // Advanced Type System
    RefinementPredicate,
    RefinementRegistry,
    SchemaDatabase,
    SchemaValidator,
    StringInterner,
    SymbolTable,
};

// Strategy for generating valid domain names
fn domain_name_strategy() -> impl Strategy<Value = String> {
    "[A-Z][a-zA-Z0-9_]{0,20}"
        .prop_map(|s| s.to_string())
        .prop_filter("Not empty", |s| !s.is_empty())
}

// Strategy for generating domain cardinalities
fn cardinality_strategy() -> impl Strategy<Value = usize> {
    1usize..1000
}

// Strategy for generating DomainInfo
fn domain_info_strategy() -> impl Strategy<Value = DomainInfo> {
    (domain_name_strategy(), cardinality_strategy())
        .prop_map(|(name, card)| DomainInfo::new(name, card))
}

// Strategy for generating PredicateInfo
fn predicate_info_strategy(domains: Vec<String>) -> impl Strategy<Value = PredicateInfo> {
    let domains_clone = domains.clone();
    (
        "[a-z][a-zA-Z0-9_]{0,20}",
        prop::collection::vec(
            prop::sample::select(domains_clone),
            1..5, // Arity between 1 and 4
        ),
    )
        .prop_map(|(name, arg_domains)| PredicateInfo::new(name.to_string(), arg_domains))
}

// Strategy for generating a complete SymbolTable
fn symbol_table_strategy() -> impl Strategy<Value = SymbolTable> {
    prop::collection::vec(domain_info_strategy(), 1..10).prop_flat_map(|domains| {
        let domain_names: Vec<String> = domains.iter().map(|d| d.name.clone()).collect();
        let domain_names_clone = domain_names.clone();

        (
            Just(domains),
            prop::collection::vec(predicate_info_strategy(domain_names_clone), 0..20),
        )
            .prop_map(|(domains, predicates)| {
                let mut table = SymbolTable::new();
                for domain in domains {
                    table.add_domain(domain).expect("unwrap");
                }
                for predicate in predicates {
                    let _ = table.add_predicate(predicate); // May fail if references unknown domain
                }
                table
            })
    })
}

proptest! {
    /// Test that JSON serialization round-trips correctly
    #[test]
    fn test_json_roundtrip(table in symbol_table_strategy()) {
        let json = table.to_json().expect("unwrap");
        let deserialized = SymbolTable::from_json(&json).expect("unwrap");

        // Check domains match
        prop_assert_eq!(table.domains.len(), deserialized.domains.len());
        for (name, domain) in &table.domains {
            let deser_domain = deserialized.get_domain(name).expect("unwrap");
            prop_assert_eq!(&domain.name, &deser_domain.name);
            prop_assert_eq!(domain.cardinality, deser_domain.cardinality);
        }

        // Check predicates match
        prop_assert_eq!(table.predicates.len(), deserialized.predicates.len());
        for (name, pred) in &table.predicates {
            let deser_pred = deserialized.get_predicate(name).expect("unwrap");
            prop_assert_eq!(&pred.name, &deser_pred.name);
            prop_assert_eq!(&pred.arg_domains, &deser_pred.arg_domains);
        }
    }

    /// Test that YAML serialization round-trips correctly
    #[test]
    fn test_yaml_roundtrip(table in symbol_table_strategy()) {
        let yaml = table.to_yaml().expect("unwrap");
        let deserialized = SymbolTable::from_yaml(&yaml).expect("unwrap");

        // Check domains match
        prop_assert_eq!(table.domains.len(), deserialized.domains.len());

        // Check predicates match
        prop_assert_eq!(table.predicates.len(), deserialized.predicates.len());
    }

    /// Test that adding a domain always succeeds
    #[test]
    fn test_add_domain_always_succeeds(domain in domain_info_strategy()) {
        let mut table = SymbolTable::new();
        prop_assert!(table.add_domain(domain.clone()).is_ok());
        prop_assert!(table.get_domain(&domain.name).is_some());
    }

    /// Test that domain lookup is consistent
    #[test]
    fn test_domain_lookup_consistency(domains in prop::collection::vec(domain_info_strategy(), 1..20)) {
        let mut table = SymbolTable::new();

        // Use a map to track the last version of each domain (simulating replacement behavior)
        let mut domain_map = std::collections::HashMap::new();
        for domain in &domains {
            table.add_domain(domain.clone()).expect("unwrap");
            domain_map.insert(domain.name.clone(), domain.clone());
        }

        // Verify lookups match the last inserted version of each domain
        for (name, domain) in domain_map.iter() {
            let retrieved = table.get_domain(name);
            prop_assert!(retrieved.is_some());
            let retrieved = retrieved.expect("unwrap");
            prop_assert_eq!(&retrieved.name, &domain.name);
            prop_assert_eq!(retrieved.cardinality, domain.cardinality);
        }
    }

    /// Test that predicate arity is preserved
    #[test]
    fn test_predicate_arity_preserved(
        domains in prop::collection::vec(domain_info_strategy(), 1..10),
        arity in 1usize..5
    ) {
        let mut table = SymbolTable::new();
        for domain in &domains {
            table.add_domain(domain.clone()).expect("unwrap");
        }

        let domain_names: Vec<String> = domains.iter().map(|d| d.name.clone()).collect();
        let arg_domains = vec![domain_names[0].clone(); arity];
        let pred = PredicateInfo::new("test_pred", arg_domains.clone());

        table.add_predicate(pred).expect("unwrap");
        let retrieved = table.get_predicate("test_pred").expect("unwrap");
        prop_assert_eq!(retrieved.arg_domains.len(), arity);
    }

    /// Test that schema validator never panics
    #[test]
    fn test_validator_never_panics(table in symbol_table_strategy()) {
        let validator = SchemaValidator::new(&table);
        let _report = validator.validate(); // Should never panic
    }

    /// Test that string interner maintains consistency
    #[test]
    fn test_string_interner_consistency(strings in prop::collection::vec("[a-zA-Z]{1,20}", 1..100)) {
        let mut interner = StringInterner::new();
        let mut id_map = std::collections::HashMap::new();

        for s in &strings {
            let id = interner.intern(s);
            id_map.entry(s.clone()).or_insert(id);
        }

        // Check that same string always gets same ID
        for (s, expected_id) in &id_map {
            let id = interner.intern(s);
            prop_assert_eq!(id, *expected_id);
        }

        // Check that all IDs resolve correctly
        for (s, id) in &id_map {
            let resolved = interner.resolve(*id);
            prop_assert_eq!(resolved, Some(s.as_str()));
        }
    }

    /// Test that domain hierarchy is always acyclic
    #[test]
    fn test_hierarchy_acyclic(edges in prop::collection::vec(
        (domain_name_strategy(), domain_name_strategy()),
        1..20
    )) {
        let mut hierarchy = DomainHierarchy::new();

        for (subtype, supertype) in edges {
            if subtype != supertype {
                hierarchy.add_subtype(&subtype, &supertype);
            }
        }

        // Verify no cycles exist by checking that no domain is its own ancestor
        for domain in hierarchy.all_domains() {
            let ancestors = hierarchy.get_ancestors(&domain);
            prop_assert!(!ancestors.contains(&domain));
        }
    }

    /// Test that hierarchy transitivity holds
    #[test]
    fn test_hierarchy_transitivity(
        a in domain_name_strategy(),
        b in domain_name_strategy(),
        c in domain_name_strategy()
    ) {
        if a == b || b == c || a == c {
            return Ok(());
        }

        let mut hierarchy = DomainHierarchy::new();
        hierarchy.add_subtype(&a, &b);
        hierarchy.add_subtype(&b, &c);

        // a <: b and b <: c implies a <: c (transitivity)
        prop_assert!(hierarchy.is_subtype(&a, &c));
    }

    /// Test that memory stats are consistent
    #[test]
    fn test_memory_stats_consistent(strings in prop::collection::vec("[a-zA-Z]{5,20}", 10..100)) {
        let mut interner = StringInterner::new();
        for s in &strings {
            interner.intern(s);
        }

        let stats = interner.memory_usage();
        let unique_strings: std::collections::HashSet<_> = strings.into_iter().collect();

        prop_assert_eq!(stats.string_count, unique_strings.len());
        prop_assert!(stats.total_bytes() > 0);
    }

    /// Test that variable binding respects domains
    #[test]
    fn test_variable_binding_domain_check(
        domains in prop::collection::vec(domain_info_strategy(), 1..10),
        var_name in "[a-z][a-z0-9]{0,10}"
    ) {
        let mut table = SymbolTable::new();
        for domain in &domains {
            table.add_domain(domain.clone()).expect("unwrap");
        }

        // Binding to existing domain should succeed
        if let Some(domain) = domains.first() {
            prop_assert!(table.bind_variable(&var_name, &domain.name).is_ok());
            prop_assert_eq!(table.get_variable_domain(&var_name), Some(domain.name.as_str()));
        }

        // Binding to non-existent domain should fail
        prop_assert!(table.bind_variable("x", "NonExistentDomain").is_err());
    }
}

// Additional deterministic tests for edge cases
#[cfg(test)]
mod deterministic_tests {
    use super::*;

    #[test]
    fn test_empty_symbol_table_serialization() {
        let table = SymbolTable::new();
        let json = table.to_json().expect("unwrap");
        let deserialized = SymbolTable::from_json(&json).expect("unwrap");
        assert_eq!(table.domains.len(), deserialized.domains.len());
    }

    #[test]
    fn test_string_interner_empty() {
        let interner = StringInterner::new();
        assert_eq!(interner.len(), 0);
        assert!(interner.is_empty());
    }

    #[test]
    fn test_hierarchy_no_cycles() {
        let mut hierarchy = DomainHierarchy::new();
        hierarchy.add_subtype("A", "B");
        hierarchy.add_subtype("B", "C");

        // Verify validation passes for acyclic hierarchy
        assert!(hierarchy.validate_acyclic().is_ok());
    }

    #[test]
    fn test_validation_report_empty_table() {
        let table = SymbolTable::new();
        let validator = SchemaValidator::new(&table);
        let report = validator.validate().expect("unwrap");
        // Empty table should have no errors but may have warnings
        assert!(report.errors.is_empty());
    }
}

// Property tests for schema evolution
proptest! {
    /// Test that evolution analysis never panics
    #[test]
    fn test_evolution_never_panics(
        old_table in symbol_table_strategy(),
        new_table in symbol_table_strategy()
    ) {
        let analyzer = EvolutionAnalyzer::new(&old_table, &new_table);
        let _report = analyzer.analyze(); // Should never panic
    }

    /// Test that identical schemas have no breaking changes
    #[test]
    fn test_evolution_identical_schemas_no_breaking(table in symbol_table_strategy()) {
        let analyzer = EvolutionAnalyzer::new(&table, &table);
        let report = analyzer.analyze().expect("unwrap");

        prop_assert!(!report.has_breaking_changes());
        prop_assert!(report.is_backward_compatible());
    }

    /// Test that evolution is reflexive: schema compared to itself is identical
    #[test]
    fn test_evolution_reflexivity(table in symbol_table_strategy()) {
        let analyzer = EvolutionAnalyzer::new(&table, &table);
        let report = analyzer.analyze().expect("unwrap");

        prop_assert_eq!(report.breaking_changes.len(), 0);
        prop_assert!(report.is_backward_compatible());
    }

    /// Test that adding NEW domains is backward compatible
    #[test]
    fn test_evolution_adding_domains_is_backward_compatible(
        old_table in symbol_table_strategy(),
        new_domain in domain_info_strategy()
    ) {
        // Only test if the domain is truly new (doesn't exist in old table)
        if old_table.get_domain(&new_domain.name).is_some() {
            // Skip this test case - domain already exists
            return Ok(());
        }

        let mut new_table = old_table.clone();
        new_table.add_domain(new_domain).expect("unwrap");

        let analyzer = EvolutionAnalyzer::new(&old_table, &new_table);
        let report = analyzer.analyze().expect("unwrap");

        // Adding truly new domains should be backward compatible
        prop_assert!(report.is_backward_compatible());
    }

    /// Test that migration plan is always generated
    #[test]
    fn test_evolution_migration_plan_exists(
        old_table in symbol_table_strategy(),
        new_table in symbol_table_strategy()
    ) {
        let analyzer = EvolutionAnalyzer::new(&old_table, &new_table);
        let _report = analyzer.analyze().expect("unwrap");

        // Migration plan should always exist (may be empty if no changes)
        // Always true for Vec, just verify it doesn't panic
    }
}

// Property tests for query planner
proptest! {
    /// Test that query planner never panics
    #[test]
    fn test_query_planner_never_panics(table in symbol_table_strategy()) {
        let mut planner = QueryPlanner::new(&table);

        // Try various query types
        let _by_name = planner.execute(&PredicateQuery::by_name("test"));
        let _by_arity = planner.execute(&PredicateQuery::by_arity(2));
    }

    /// Test that query by name returns correct results
    #[test]
    fn test_query_by_name_correctness(table in symbol_table_strategy()) {
        let mut planner = QueryPlanner::new(&table);

        // Query for each predicate by name
        for (name, _pred) in &table.predicates {
            let query = PredicateQuery::by_name(name);
            let results = planner.execute(&query).expect("unwrap");

            prop_assert!(results.len() <= 1); // At most one result
            if let Some((result_name, _)) = results.first() {
                prop_assert_eq!(result_name, name);
            }
        }
    }

    /// Test that query by arity returns predicates with correct arity
    #[test]
    fn test_query_by_arity_correctness(
        table in symbol_table_strategy(),
        arity in 1usize..5
    ) {
        let mut planner = QueryPlanner::new(&table);
        let query = PredicateQuery::by_arity(arity);
        let results = planner.execute(&query).expect("unwrap");

        // All results should have the specified arity
        for (_name, pred) in results {
            prop_assert_eq!(pred.arg_domains.len(), arity);
        }
    }

    /// Test that query results are deterministic
    #[test]
    fn test_query_determinism(table in symbol_table_strategy()) {
        let mut planner = QueryPlanner::new(&table);
        let query = PredicateQuery::by_arity(2);

        // Execute same query multiple times
        let results1 = planner.execute(&query).expect("unwrap");
        let results2 = planner.execute(&query).expect("unwrap");

        // Results should be identical
        prop_assert_eq!(results1.len(), results2.len());
        for ((name1, pred1), (name2, pred2)) in results1.iter().zip(results2.iter()) {
            prop_assert_eq!(name1, name2);
            prop_assert_eq!(&pred1.arg_domains, &pred2.arg_domains);
        }
    }

    /// Test that query planner caches plans
    #[test]
    fn test_query_planner_has_cache(table in symbol_table_strategy()) {
        let mut planner = QueryPlanner::new(&table);
        let query = PredicateQuery::by_arity(2);

        // Execute query multiple times
        let _ = planner.execute(&query);
        let cache_size1 = planner.cache_size();

        let _ = planner.execute(&query);
        let cache_size2 = planner.cache_size();

        // Cache size should not decrease
        prop_assert!(cache_size2 >= cache_size1);
    }

    // ==========================================================================
    // Advanced Type System Property Tests
    // ==========================================================================

    /// Test that refinement predicate range checks are consistent
    #[test]
    fn test_refinement_range_consistency(
        min in -1000.0f64..1000.0,
        max in -1000.0f64..1000.0,
        value in -2000.0f64..2000.0
    ) {
        if min <= max {
            let pred = RefinementPredicate::range(min, max);
            let result = pred.check(value);

            // Value should be in range iff both bounds are satisfied
            let expected = value >= min && value <= max;
            prop_assert_eq!(result, expected);
        }
    }

    /// Test that refinement And combines predicates correctly
    #[test]
    fn test_refinement_and_composition(value in -1000.0f64..1000.0) {
        let p1 = RefinementPredicate::GreaterThan(0.0);
        let p2 = RefinementPredicate::LessThan(100.0);
        let combined = RefinementPredicate::And(vec![p1.clone(), p2.clone()]);

        let result = combined.check(value);
        let expected = p1.check(value) && p2.check(value);
        prop_assert_eq!(result, expected);
    }

    /// Test that refinement Or combines predicates correctly
    #[test]
    fn test_refinement_or_composition(value in -1000.0f64..1000.0) {
        let p1 = RefinementPredicate::LessThan(0.0);
        let p2 = RefinementPredicate::GreaterThan(100.0);
        let combined = RefinementPredicate::Or(vec![p1.clone(), p2.clone()]);

        let result = combined.check(value);
        let expected = p1.check(value) || p2.check(value);
        prop_assert_eq!(result, expected);
    }

    /// Test that refinement Not negates correctly
    #[test]
    fn test_refinement_not_negation(value in -1000.0f64..1000.0) {
        let pred = RefinementPredicate::GreaterThan(0.0);
        let negated = RefinementPredicate::not(pred.clone());

        let result = negated.check(value);
        let expected = !pred.check(value);
        prop_assert_eq!(result, expected);
    }

    /// Test that built-in refinement registry types are valid
    #[test]
    fn test_refinement_registry_builtins_valid(value in -1000.0f64..1000.0) {
        let registry = RefinementRegistry::with_builtins();

        // PositiveInt should only accept positive values
        if let Some(is_positive) = registry.check("PositiveInt", value) {
            prop_assert_eq!(is_positive, value > 0.0);
        }

        // NonNegativeInt should accept zero and positive
        if let Some(is_nonneg) = registry.check("NonNegativeInt", value) {
            prop_assert_eq!(is_nonneg, value >= 0.0);
        }
    }

    /// Test that dependent dimension expressions evaluate correctly
    #[test]
    fn test_dim_expr_arithmetic(
        a in 1usize..1000,
        b in 1usize..1000
    ) {
        let mut ctx = DependentTypeContext::new();
        ctx.set_dim("a", a);
        ctx.set_dim("b", b);

        // Test addition
        let add_expr = DimExpr::var("a").add(DimExpr::var("b"));
        prop_assert_eq!(add_expr.eval(&ctx), Some(a + b));

        // Test multiplication
        let mul_expr = DimExpr::var("a").mul(DimExpr::var("b"));
        prop_assert_eq!(mul_expr.eval(&ctx), Some(a * b));

        // Test max
        let max_expr = DimExpr::var("a").max(DimExpr::var("b"));
        prop_assert_eq!(max_expr.eval(&ctx), Some(a.max(b)));

        // Test min
        let min_expr = DimExpr::var("a").min(DimExpr::var("b"));
        prop_assert_eq!(min_expr.eval(&ctx), Some(a.min(b)));
    }

    /// Test that dependent type shapes evaluate correctly
    #[test]
    fn test_dependent_type_shape(
        batch in 1usize..100,
        height in 1usize..100,
        width in 1usize..100
    ) {
        let mut ctx = DependentTypeContext::new();
        ctx.set_dim("batch", batch);
        ctx.set_dim("height", height);
        ctx.set_dim("width", width);

        let tensor = DependentType::tensor(
            "Float",
            vec![DimExpr::var("batch"), DimExpr::var("height"), DimExpr::var("width")]
        );

        let shape = tensor.eval_shape(&ctx).expect("unwrap");
        prop_assert_eq!(shape, vec![batch, height, width]);
        prop_assert_eq!(tensor.rank(), 3);
    }

    /// Test that linear type kind properties are correct
    #[test]
    fn test_linear_kind_properties(_dummy in 0..1) {
        // Linear: no copy, no drop
        prop_assert!(!LinearKind::Linear.allows_copy());
        prop_assert!(!LinearKind::Linear.allows_drop());

        // Affine: no copy, yes drop
        prop_assert!(!LinearKind::Affine.allows_copy());
        prop_assert!(LinearKind::Affine.allows_drop());

        // Relevant: yes copy, no drop
        prop_assert!(LinearKind::Relevant.allows_copy());
        prop_assert!(!LinearKind::Relevant.allows_drop());

        // Unrestricted: yes copy, yes drop
        prop_assert!(LinearKind::Unrestricted.allows_copy());
        prop_assert!(LinearKind::Unrestricted.allows_drop());
    }

    /// Test that linear context tracks resources correctly
    #[test]
    fn test_linear_context_tracking(n_resources in 1usize..10) {
        let mut ctx = LinearContext::new();
        let ty = LinearType::affine("Test");

        // Create resources
        for i in 0..n_resources {
            let name = format!("resource_{}", i);
            ctx.create_resource(&name, ty.clone(), "test").expect("unwrap");
        }

        // Check statistics
        let stats = ctx.statistics();
        prop_assert_eq!(stats.total, n_resources);
        prop_assert_eq!(stats.unused, n_resources);
        prop_assert_eq!(stats.used, 0);

        // Use first resource
        ctx.use_resource("resource_0", "test").expect("unwrap");
        let stats = ctx.statistics();
        prop_assert_eq!(stats.used, 1);
        prop_assert_eq!(stats.unused, n_resources - 1);
    }

    /// Test that effect set operations are consistent
    #[test]
    fn test_effect_set_union_idempotent(_dummy in 0..1) {
        let set1 = EffectSet::new().with(Effect::IO).with(Effect::State);
        let union = set1.union(&set1);

        // Union with self should be the same
        prop_assert_eq!(set1.len(), union.len());
        for effect in set1.iter() {
            prop_assert!(union.has(*effect));
        }
    }

    /// Test that effect set union is commutative
    #[test]
    fn test_effect_set_union_commutative(_dummy in 0..1) {
        let set1 = EffectSet::new().with(Effect::IO).with(Effect::State);
        let set2 = EffectSet::new().with(Effect::NonDet).with(Effect::GPU);

        let union1 = set1.union(&set2);
        let union2 = set2.union(&set1);

        prop_assert_eq!(union1.len(), union2.len());
        for effect in union1.iter() {
            prop_assert!(union2.has(*effect));
        }
    }

    /// Test that pure effect set is empty
    #[test]
    fn test_effect_set_pure_empty(_dummy in 0..1) {
        let pure = EffectSet::pure();
        prop_assert!(pure.is_pure());
        prop_assert!(pure.is_empty());
        prop_assert_eq!(pure.len(), 0);
    }

    /// Test that effect set difference works correctly
    #[test]
    fn test_effect_set_difference(_dummy in 0..1) {
        let set1 = EffectSet::new().with(Effect::IO).with(Effect::State).with(Effect::GPU);
        let set2 = EffectSet::new().with(Effect::IO).with(Effect::State);

        let diff = set1.difference(&set2);

        // Difference should contain only GPU
        prop_assert_eq!(diff.len(), 1);
        prop_assert!(diff.has(Effect::GPU));
        prop_assert!(!diff.has(Effect::IO));
        prop_assert!(!diff.has(Effect::State));
    }
}

// =============================================================================
// Database Property Tests
// =============================================================================

mod database_properties {
    use super::*;

    proptest! {
        /// Test that storing and loading a schema preserves all data
        #[test]
        fn test_database_store_load_roundtrip(table in symbol_table_strategy()) {
            let mut db = MemoryDatabase::new();

            // Store the schema
            let schema_id = db.store_schema("test_schema", &table).expect("unwrap");

            // Load it back
            let loaded = db.load_schema(schema_id).expect("unwrap");

            // Verify all domains are preserved
            prop_assert_eq!(table.domains.len(), loaded.domains.len());
            for (name, domain) in &table.domains {
                prop_assert!(loaded.domains.contains_key(name));
                let loaded_domain = &loaded.domains[name];
                prop_assert_eq!(domain.cardinality, loaded_domain.cardinality);
                prop_assert_eq!(&domain.name, &loaded_domain.name);
            }

            // Verify all predicates are preserved
            prop_assert_eq!(table.predicates.len(), loaded.predicates.len());
            for (name, pred) in &table.predicates {
                prop_assert!(loaded.predicates.contains_key(name));
                let loaded_pred = &loaded.predicates[name];
                prop_assert_eq!(pred.arity, loaded_pred.arity);
                prop_assert_eq!(&pred.arg_domains, &loaded_pred.arg_domains);
            }
        }

        /// Test that schemas can be loaded by name
        #[test]
        fn test_database_load_by_name(table in symbol_table_strategy(), name in "[a-z]{3,15}") {
            let mut db = MemoryDatabase::new();

            // Store with a specific name
            db.store_schema(&name, &table).expect("unwrap");

            // Load by name
            let loaded = db.load_schema_by_name(&name).expect("unwrap");

            // Should match
            prop_assert_eq!(table.domains.len(), loaded.domains.len());
            prop_assert_eq!(table.predicates.len(), loaded.predicates.len());
        }

        /// Test that multiple versions of the same schema can coexist
        #[test]
        fn test_database_versioning(
            table1 in symbol_table_strategy(),
            table2 in symbol_table_strategy(),
        ) {
            let mut db = MemoryDatabase::new();

            // Store two versions of the same schema
            let id1 = db.store_schema("versioned_schema", &table1).expect("unwrap");
            let id2 = db.store_schema("versioned_schema", &table2).expect("unwrap");

            // IDs should be different
            prop_assert_ne!(id1, id2);

            // Both should be loadable
            let loaded1 = db.load_schema(id1).expect("unwrap");
            let loaded2 = db.load_schema(id2).expect("unwrap");

            prop_assert_eq!(table1.domains.len(), loaded1.domains.len());
            prop_assert_eq!(table2.domains.len(), loaded2.domains.len());
        }

        /// Test that schema search works correctly
        #[test]
        fn test_database_search(
            table in symbol_table_strategy(),
            prefix in "[a-z]{2,5}",
            suffix in "[a-z]{2,5}",
        ) {
            let mut db = MemoryDatabase::new();
            let name = format!("{}_test_{}", prefix, suffix);

            db.store_schema(&name, &table).expect("unwrap");

            // Search with prefix should find it
            let results = db.search_schemas(&format!("{}_", prefix)).expect("unwrap");
            prop_assert!(!results.is_empty());

            // Verify the found schema has the correct name
            prop_assert!(results.iter().any(|m| m.name.starts_with(&prefix)));
        }

        /// Test that deleting a schema removes it
        #[test]
        fn test_database_delete(table in symbol_table_strategy()) {
            let mut db = MemoryDatabase::new();

            let id = db.store_schema("to_delete", &table).expect("unwrap");

            // Verify it exists
            prop_assert!(db.load_schema(id).is_ok());

            // Delete it
            db.delete_schema(id).expect("unwrap");

            // Should no longer be loadable
            prop_assert!(db.load_schema(id).is_err());
        }

        /// Test that schema history tracks versions
        #[test]
        fn test_database_history(
            table1 in symbol_table_strategy(),
            table2 in symbol_table_strategy(),
            table3 in symbol_table_strategy(),
        ) {
            let mut db = MemoryDatabase::new();
            let name = "history_test";

            db.store_schema(name, &table1).expect("unwrap");
            db.store_schema(name, &table2).expect("unwrap");
            db.store_schema(name, &table3).expect("unwrap");

            let history = db.get_schema_history(name).expect("unwrap");

            // Should have 3 versions
            prop_assert_eq!(history.len(), 3);

            // Versions should be sequential
            prop_assert_eq!(history[0].version, 1);
            prop_assert_eq!(history[1].version, 2);
            prop_assert_eq!(history[2].version, 3);
        }

        /// Test that list_schemas returns all stored schemas
        #[test]
        fn test_database_list_schemas(
            tables in prop::collection::vec(symbol_table_strategy(), 1..10),
        ) {
            let mut db = MemoryDatabase::new();
            let mut names = Vec::new();

            // Store multiple schemas with unique names
            for (i, table) in tables.iter().enumerate() {
                let name = format!("schema_{}", i);
                names.push(name.clone());
                db.store_schema(&name, table).expect("unwrap");
            }

            let list = db.list_schemas().expect("unwrap");

            // Should have at least as many as we stored
            prop_assert!(list.len() >= tables.len());

            // All our schemas should be in the list
            for name in &names {
                prop_assert!(list.iter().any(|m| m.name == *name));
            }
        }

        /// Test that database handles empty schemas correctly
        #[test]
        fn test_database_empty_schema(_dummy in 0..1) {
            let mut db = MemoryDatabase::new();
            let empty = SymbolTable::new();

            let id = db.store_schema("empty", &empty).expect("unwrap");
            let loaded = db.load_schema(id).expect("unwrap");

            prop_assert_eq!(loaded.domains.len(), 0);
            prop_assert_eq!(loaded.predicates.len(), 0);
            prop_assert_eq!(loaded.variables.len(), 0);
        }

        /// Test database metadata accuracy
        #[test]
        fn test_database_metadata_accuracy(table in symbol_table_strategy()) {
            let mut db = MemoryDatabase::new();

            let id = db.store_schema("metadata_test", &table).expect("unwrap");
            let list = db.list_schemas().expect("unwrap");

            // Find our schema in the list
            let metadata = list.iter().find(|m| m.id == id).expect("unwrap");

            // Verify counts match
            prop_assert_eq!(metadata.num_domains, table.domains.len());
            prop_assert_eq!(metadata.num_predicates, table.predicates.len());
            prop_assert_eq!(metadata.num_variables, table.variables.len());
        }

        /// Test that concurrent schema operations don't interfere
        #[test]
        fn test_database_independence(
            table1 in symbol_table_strategy(),
            table2 in symbol_table_strategy(),
        ) {
            let mut db = MemoryDatabase::new();

            // Store two different schemas
            let id1 = db.store_schema("schema_a", &table1).expect("unwrap");
            let id2 = db.store_schema("schema_b", &table2).expect("unwrap");

            // Load them back
            let loaded1 = db.load_schema(id1).expect("unwrap");
            let loaded2 = db.load_schema(id2).expect("unwrap");

            // They should be independent (not affect each other)
            prop_assert_eq!(table1.domains.len(), loaded1.domains.len());
            prop_assert_eq!(table2.domains.len(), loaded2.domains.len());

            // Modifying one shouldn't affect the other
            if !table1.domains.is_empty() && !table2.domains.is_empty() {
                let dom1_names: Vec<_> = table1.domains.keys().collect();
                let dom2_names: Vec<_> = table2.domains.keys().collect();

                // If they have different domains, verify independence
                if dom1_names != dom2_names {
                    for name in &dom1_names {
                        if !dom2_names.contains(name) {
                            prop_assert!(!loaded2.domains.contains_key(*name));
                        }
                    }
                }
            }
        }
    }
}