use proptest::prelude::*;
use tensorlogic_adapters::{
DependentType,
DependentTypeContext,
DimExpr,
DomainHierarchy,
DomainInfo,
Effect,
EffectSet,
EvolutionAnalyzer,
LinearContext,
LinearKind,
LinearType,
MemoryDatabase,
PredicateInfo,
PredicateQuery,
QueryPlanner,
RefinementPredicate,
RefinementRegistry,
SchemaDatabase,
SchemaValidator,
StringInterner,
SymbolTable,
};
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())
}
fn cardinality_strategy() -> impl Strategy<Value = usize> {
1usize..1000
}
fn domain_info_strategy() -> impl Strategy<Value = DomainInfo> {
(domain_name_strategy(), cardinality_strategy())
.prop_map(|(name, card)| DomainInfo::new(name, card))
}
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, ),
)
.prop_map(|(name, arg_domains)| PredicateInfo::new(name.to_string(), arg_domains))
}
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); }
table
})
})
}
proptest! {
#[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");
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);
}
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]
fn test_yaml_roundtrip(table in symbol_table_strategy()) {
let yaml = table.to_yaml().expect("unwrap");
let deserialized = SymbolTable::from_yaml(&yaml).expect("unwrap");
prop_assert_eq!(table.domains.len(), deserialized.domains.len());
prop_assert_eq!(table.predicates.len(), deserialized.predicates.len());
}
#[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]
fn test_domain_lookup_consistency(domains in prop::collection::vec(domain_info_strategy(), 1..20)) {
let mut table = SymbolTable::new();
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());
}
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]
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]
fn test_validator_never_panics(table in symbol_table_strategy()) {
let validator = SchemaValidator::new(&table);
let _report = validator.validate(); }
#[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);
}
for (s, expected_id) in &id_map {
let id = interner.intern(s);
prop_assert_eq!(id, *expected_id);
}
for (s, id) in &id_map {
let resolved = interner.resolve(*id);
prop_assert_eq!(resolved, Some(s.as_str()));
}
}
#[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);
}
}
for domain in hierarchy.all_domains() {
let ancestors = hierarchy.get_ancestors(&domain);
prop_assert!(!ancestors.contains(&domain));
}
}
#[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);
prop_assert!(hierarchy.is_subtype(&a, &c));
}
#[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]
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");
}
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()));
}
prop_assert!(table.bind_variable("x", "NonExistentDomain").is_err());
}
}
#[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");
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");
assert!(report.errors.is_empty());
}
}
proptest! {
#[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(); }
#[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]
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]
fn test_evolution_adding_domains_is_backward_compatible(
old_table in symbol_table_strategy(),
new_domain in domain_info_strategy()
) {
if old_table.get_domain(&new_domain.name).is_some() {
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");
prop_assert!(report.is_backward_compatible());
}
#[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");
}
}
proptest! {
#[test]
fn test_query_planner_never_panics(table in symbol_table_strategy()) {
let mut planner = QueryPlanner::new(&table);
let _by_name = planner.execute(&PredicateQuery::by_name("test"));
let _by_arity = planner.execute(&PredicateQuery::by_arity(2));
}
#[test]
fn test_query_by_name_correctness(table in symbol_table_strategy()) {
let mut planner = QueryPlanner::new(&table);
for (name, _pred) in &table.predicates {
let query = PredicateQuery::by_name(name);
let results = planner.execute(&query).expect("unwrap");
prop_assert!(results.len() <= 1); if let Some((result_name, _)) = results.first() {
prop_assert_eq!(result_name, name);
}
}
}
#[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");
for (_name, pred) in results {
prop_assert_eq!(pred.arg_domains.len(), arity);
}
}
#[test]
fn test_query_determinism(table in symbol_table_strategy()) {
let mut planner = QueryPlanner::new(&table);
let query = PredicateQuery::by_arity(2);
let results1 = planner.execute(&query).expect("unwrap");
let results2 = planner.execute(&query).expect("unwrap");
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]
fn test_query_planner_has_cache(table in symbol_table_strategy()) {
let mut planner = QueryPlanner::new(&table);
let query = PredicateQuery::by_arity(2);
let _ = planner.execute(&query);
let cache_size1 = planner.cache_size();
let _ = planner.execute(&query);
let cache_size2 = planner.cache_size();
prop_assert!(cache_size2 >= cache_size1);
}
#[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);
let expected = value >= min && value <= max;
prop_assert_eq!(result, expected);
}
}
#[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]
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]
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]
fn test_refinement_registry_builtins_valid(value in -1000.0f64..1000.0) {
let registry = RefinementRegistry::with_builtins();
if let Some(is_positive) = registry.check("PositiveInt", value) {
prop_assert_eq!(is_positive, value > 0.0);
}
if let Some(is_nonneg) = registry.check("NonNegativeInt", value) {
prop_assert_eq!(is_nonneg, value >= 0.0);
}
}
#[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);
let add_expr = DimExpr::var("a").add(DimExpr::var("b"));
prop_assert_eq!(add_expr.eval(&ctx), Some(a + b));
let mul_expr = DimExpr::var("a").mul(DimExpr::var("b"));
prop_assert_eq!(mul_expr.eval(&ctx), Some(a * b));
let max_expr = DimExpr::var("a").max(DimExpr::var("b"));
prop_assert_eq!(max_expr.eval(&ctx), Some(a.max(b)));
let min_expr = DimExpr::var("a").min(DimExpr::var("b"));
prop_assert_eq!(min_expr.eval(&ctx), Some(a.min(b)));
}
#[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]
fn test_linear_kind_properties(_dummy in 0..1) {
prop_assert!(!LinearKind::Linear.allows_copy());
prop_assert!(!LinearKind::Linear.allows_drop());
prop_assert!(!LinearKind::Affine.allows_copy());
prop_assert!(LinearKind::Affine.allows_drop());
prop_assert!(LinearKind::Relevant.allows_copy());
prop_assert!(!LinearKind::Relevant.allows_drop());
prop_assert!(LinearKind::Unrestricted.allows_copy());
prop_assert!(LinearKind::Unrestricted.allows_drop());
}
#[test]
fn test_linear_context_tracking(n_resources in 1usize..10) {
let mut ctx = LinearContext::new();
let ty = LinearType::affine("Test");
for i in 0..n_resources {
let name = format!("resource_{}", i);
ctx.create_resource(&name, ty.clone(), "test").expect("unwrap");
}
let stats = ctx.statistics();
prop_assert_eq!(stats.total, n_resources);
prop_assert_eq!(stats.unused, n_resources);
prop_assert_eq!(stats.used, 0);
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]
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);
prop_assert_eq!(set1.len(), union.len());
for effect in set1.iter() {
prop_assert!(union.has(*effect));
}
}
#[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]
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]
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);
prop_assert_eq!(diff.len(), 1);
prop_assert!(diff.has(Effect::GPU));
prop_assert!(!diff.has(Effect::IO));
prop_assert!(!diff.has(Effect::State));
}
}
mod database_properties {
use super::*;
proptest! {
#[test]
fn test_database_store_load_roundtrip(table in symbol_table_strategy()) {
let mut db = MemoryDatabase::new();
let schema_id = db.store_schema("test_schema", &table).expect("unwrap");
let loaded = db.load_schema(schema_id).expect("unwrap");
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);
}
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]
fn test_database_load_by_name(table in symbol_table_strategy(), name in "[a-z]{3,15}") {
let mut db = MemoryDatabase::new();
db.store_schema(&name, &table).expect("unwrap");
let loaded = db.load_schema_by_name(&name).expect("unwrap");
prop_assert_eq!(table.domains.len(), loaded.domains.len());
prop_assert_eq!(table.predicates.len(), loaded.predicates.len());
}
#[test]
fn test_database_versioning(
table1 in symbol_table_strategy(),
table2 in symbol_table_strategy(),
) {
let mut db = MemoryDatabase::new();
let id1 = db.store_schema("versioned_schema", &table1).expect("unwrap");
let id2 = db.store_schema("versioned_schema", &table2).expect("unwrap");
prop_assert_ne!(id1, id2);
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]
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");
let results = db.search_schemas(&format!("{}_", prefix)).expect("unwrap");
prop_assert!(!results.is_empty());
prop_assert!(results.iter().any(|m| m.name.starts_with(&prefix)));
}
#[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");
prop_assert!(db.load_schema(id).is_ok());
db.delete_schema(id).expect("unwrap");
prop_assert!(db.load_schema(id).is_err());
}
#[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");
prop_assert_eq!(history.len(), 3);
prop_assert_eq!(history[0].version, 1);
prop_assert_eq!(history[1].version, 2);
prop_assert_eq!(history[2].version, 3);
}
#[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();
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");
prop_assert!(list.len() >= tables.len());
for name in &names {
prop_assert!(list.iter().any(|m| m.name == *name));
}
}
#[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]
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");
let metadata = list.iter().find(|m| m.id == id).expect("unwrap");
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]
fn test_database_independence(
table1 in symbol_table_strategy(),
table2 in symbol_table_strategy(),
) {
let mut db = MemoryDatabase::new();
let id1 = db.store_schema("schema_a", &table1).expect("unwrap");
let id2 = db.store_schema("schema_b", &table2).expect("unwrap");
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());
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 dom1_names != dom2_names {
for name in &dom1_names {
if !dom2_names.contains(name) {
prop_assert!(!loaded2.domains.contains_key(*name));
}
}
}
}
}
}
}