BREAKING CHANGES: - Relationship syntax now requires blocks for all participants - Removed self/other perspective blocks from relationships - Replaced 'guard' keyword with 'if' for behavior tree decorators Language Features: - Add tree-sitter grammar with improved if/condition disambiguation - Add comprehensive tutorial and reference documentation - Add SBIR v0.2.0 binary format specification - Add resource linking system for behaviors and schedules - Add year-long schedule patterns (day, season, recurrence) - Add behavior tree enhancements (named nodes, decorators) Documentation: - Complete tutorial series (9 chapters) with baker family examples - Complete reference documentation for all language features - SBIR v0.2.0 specification with binary format details - Added locations and institutions documentation Examples: - Convert all examples to baker family scenario - Add comprehensive working examples Tooling: - Zed extension with LSP integration - Tree-sitter grammar for syntax highlighting - Build scripts and development tools Version Updates: - Main package: 0.1.0 → 0.2.0 - Tree-sitter grammar: 0.1.0 → 0.2.0 - Zed extension: 0.1.0 → 0.2.0 - Storybook editor: 0.1.0 → 0.2.0
318 lines
9.4 KiB
Rust
318 lines
9.4 KiB
Rust
//! Property-based tests for the resolution engine
|
|
|
|
use proptest::prelude::*;
|
|
|
|
use crate::{
|
|
resolve::names::{
|
|
DeclKind,
|
|
NameTable,
|
|
},
|
|
syntax::ast::*,
|
|
};
|
|
|
|
// ===== Generators =====
|
|
|
|
fn valid_ident() -> impl Strategy<Value = String> {
|
|
"[a-zA-Z_][a-zA-Z0-9_]{0,20}".prop_filter("not a keyword", |s| {
|
|
!matches!(
|
|
s.as_str(),
|
|
"use" |
|
|
"character" |
|
|
"template" |
|
|
"life_arc" |
|
|
"schedule" |
|
|
"behavior" |
|
|
"institution" |
|
|
"relationship" |
|
|
"location" |
|
|
"species" |
|
|
"enum" |
|
|
"state" |
|
|
"on" |
|
|
"as" |
|
|
"self" |
|
|
"other" |
|
|
"remove" |
|
|
"append" |
|
|
"forall" |
|
|
"exists" |
|
|
"in" |
|
|
"where" |
|
|
"and" |
|
|
"or" |
|
|
"not" |
|
|
"is" |
|
|
"true" |
|
|
"false"
|
|
)
|
|
})
|
|
}
|
|
|
|
fn valid_character_decl() -> impl Strategy<Value = (String, Declaration)> {
|
|
valid_ident().prop_map(|name| {
|
|
let decl = Declaration::Character(Character {
|
|
name: name.clone(),
|
|
species: None,
|
|
fields: vec![],
|
|
template: None,
|
|
uses_behaviors: None,
|
|
uses_schedule: None,
|
|
|
|
span: Span::new(0, 10),
|
|
});
|
|
(name, decl)
|
|
})
|
|
}
|
|
|
|
fn valid_template_decl() -> impl Strategy<Value = (String, Declaration)> {
|
|
valid_ident().prop_map(|name| {
|
|
let decl = Declaration::Template(Template {
|
|
name: name.clone(),
|
|
fields: vec![],
|
|
strict: false,
|
|
includes: vec![],
|
|
uses_behaviors: None,
|
|
uses_schedule: None,
|
|
span: Span::new(0, 10),
|
|
});
|
|
(name, decl)
|
|
})
|
|
}
|
|
|
|
fn valid_enum_decl() -> impl Strategy<Value = (String, Declaration)> {
|
|
(valid_ident(), prop::collection::vec(valid_ident(), 1..5)).prop_map(|(name, variants)| {
|
|
let decl = Declaration::Enum(EnumDecl {
|
|
name: name.clone(),
|
|
variants,
|
|
span: Span::new(0, 10),
|
|
});
|
|
(name, decl)
|
|
})
|
|
}
|
|
|
|
fn valid_use_single() -> impl Strategy<Value = Declaration> {
|
|
(valid_ident(), valid_ident()).prop_map(|(module, name)| {
|
|
Declaration::Use(UseDecl {
|
|
path: vec![module, name],
|
|
kind: UseKind::Single,
|
|
span: Span::new(0, 10),
|
|
})
|
|
})
|
|
}
|
|
|
|
fn valid_use_grouped() -> impl Strategy<Value = Declaration> {
|
|
(valid_ident(), prop::collection::vec(valid_ident(), 1..5)).prop_map(|(module, names)| {
|
|
Declaration::Use(UseDecl {
|
|
path: vec![module],
|
|
kind: UseKind::Grouped(names),
|
|
span: Span::new(0, 10),
|
|
})
|
|
})
|
|
}
|
|
|
|
fn valid_use_wildcard() -> impl Strategy<Value = Declaration> {
|
|
valid_ident().prop_map(|module| {
|
|
Declaration::Use(UseDecl {
|
|
path: vec![module],
|
|
kind: UseKind::Wildcard,
|
|
span: Span::new(0, 10),
|
|
})
|
|
})
|
|
}
|
|
|
|
// ===== Property Tests =====
|
|
|
|
proptest! {
|
|
#[test]
|
|
fn test_name_table_registers_all_declarations(
|
|
chars in prop::collection::vec(valid_character_decl(), 0..10)
|
|
) {
|
|
let declarations: Vec<_> = chars.iter().map(|(_, decl)| decl.clone()).collect();
|
|
let file = File { declarations };
|
|
|
|
let result = NameTable::from_file(&file);
|
|
|
|
if chars.is_empty() {
|
|
// Empty file should succeed
|
|
assert!(result.is_ok());
|
|
} else {
|
|
// Check if there are duplicates
|
|
let mut seen = std::collections::HashSet::new();
|
|
let has_duplicates = chars.iter().any(|(name, _)| !seen.insert(name));
|
|
|
|
if has_duplicates {
|
|
// Should fail with duplicate error
|
|
assert!(result.is_err());
|
|
} else {
|
|
// Should succeed and all names should be registered
|
|
let table = result.unwrap();
|
|
for (name, _) in &chars {
|
|
assert!(table.lookup(std::slice::from_ref(name)).is_some(),
|
|
"Name '{}' should be registered", name);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_duplicate_detection_always_fails(
|
|
name in valid_ident(),
|
|
count in 2usize..5
|
|
) {
|
|
let declarations: Vec<_> = (0..count).map(|i| {
|
|
Declaration::Character(Character {
|
|
name: name.clone(),
|
|
species: None,
|
|
fields: vec![],
|
|
template: None,
|
|
uses_behaviors: None,
|
|
uses_schedule: None,
|
|
|
|
span: Span::new(i * 10, i * 10 + 10),
|
|
})
|
|
}).collect();
|
|
|
|
let file = File { declarations };
|
|
let result = NameTable::from_file(&file);
|
|
|
|
// Should always fail with duplicate error
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_lookup_is_case_sensitive(name in valid_ident()) {
|
|
let file = File {
|
|
declarations: vec![
|
|
Declaration::Character(Character {
|
|
name: name.clone(),
|
|
species: None,
|
|
fields: vec![],
|
|
template: None,
|
|
uses_behaviors: None,
|
|
uses_schedule: None,
|
|
|
|
span: Span::new(0, 10),
|
|
}),
|
|
],
|
|
};
|
|
|
|
let table = NameTable::from_file(&file).unwrap();
|
|
|
|
// Original name should be found
|
|
assert!(table.lookup(std::slice::from_ref(&name)).is_some());
|
|
|
|
// Different case should not be found
|
|
let uppercase = name.to_uppercase();
|
|
if uppercase != name {
|
|
assert!(table.lookup(&[uppercase]).is_none());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_kind_filtering_works(
|
|
chars in prop::collection::vec(valid_character_decl(), 0..5),
|
|
templates in prop::collection::vec(valid_template_decl(), 0..5),
|
|
enums in prop::collection::vec(valid_enum_decl(), 0..5)
|
|
) {
|
|
let mut declarations = vec![];
|
|
declarations.extend(chars.iter().map(|(_, d)| d.clone()));
|
|
declarations.extend(templates.iter().map(|(_, d)| d.clone()));
|
|
declarations.extend(enums.iter().map(|(_, d)| d.clone()));
|
|
|
|
let file = File { declarations };
|
|
|
|
// Only proceed if no duplicates
|
|
let mut seen = std::collections::HashSet::new();
|
|
let has_duplicates = chars.iter().any(|(name, _)| !seen.insert(name))
|
|
|| templates.iter().any(|(name, _)| !seen.insert(name))
|
|
|| enums.iter().any(|(name, _)| !seen.insert(name));
|
|
|
|
if !has_duplicates {
|
|
let table = NameTable::from_file(&file).unwrap();
|
|
|
|
let char_count = table.entries_of_kind(DeclKind::Character).count();
|
|
let template_count = table.entries_of_kind(DeclKind::Template).count();
|
|
let enum_count = table.entries_of_kind(DeclKind::Enum).count();
|
|
|
|
assert_eq!(char_count, chars.len());
|
|
assert_eq!(template_count, templates.len());
|
|
assert_eq!(enum_count, enums.len());
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_use_statements_are_collected(
|
|
uses in prop::collection::vec(
|
|
prop_oneof![
|
|
valid_use_single(),
|
|
valid_use_grouped(),
|
|
valid_use_wildcard(),
|
|
],
|
|
0..10
|
|
)
|
|
) {
|
|
let file = File { declarations: uses.clone() };
|
|
let table = NameTable::from_file(&file).unwrap();
|
|
|
|
assert_eq!(table.imports().len(), uses.len());
|
|
}
|
|
|
|
#[test]
|
|
fn test_fuzzy_matching_finds_close_names(
|
|
name in valid_ident().prop_filter("long enough", |s| s.len() > 3)
|
|
) {
|
|
let file = File {
|
|
declarations: vec![
|
|
Declaration::Character(Character {
|
|
name: name.clone(),
|
|
species: None,
|
|
fields: vec![],
|
|
template: None,
|
|
uses_behaviors: None,
|
|
uses_schedule: None,
|
|
|
|
span: Span::new(0, 10),
|
|
}),
|
|
],
|
|
};
|
|
|
|
let table = NameTable::from_file(&file).unwrap();
|
|
|
|
// Create a typo by swapping two adjacent characters
|
|
if name.len() >= 2 {
|
|
let mut chars: Vec<char> = name.chars().collect();
|
|
chars.swap(0, 1);
|
|
let typo: String = chars.into_iter().collect();
|
|
|
|
// Should suggest the original name
|
|
if let Some(suggestion) = table.find_suggestion(&typo) {
|
|
assert_eq!(suggestion, name);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_mixed_declarations_and_imports(
|
|
chars in prop::collection::vec(valid_character_decl(), 1..5),
|
|
uses in prop::collection::vec(valid_use_single(), 0..3)
|
|
) {
|
|
let mut declarations = uses;
|
|
declarations.extend(chars.iter().map(|(_, d)| d.clone()));
|
|
|
|
let file = File { declarations };
|
|
|
|
// Check for duplicates
|
|
let mut seen = std::collections::HashSet::new();
|
|
let has_duplicates = chars.iter().any(|(name, _)| !seen.insert(name));
|
|
|
|
if !has_duplicates {
|
|
let table = NameTable::from_file(&file).unwrap();
|
|
|
|
// All characters should be registered
|
|
for (name, _) in &chars {
|
|
assert!(table.lookup(std::slice::from_ref(name)).is_some());
|
|
}
|
|
}
|
|
}
|
|
}
|