feat(type-system): implement concept_comparison with pattern matching

Added complete support for the new type system syntax including:

- concept: Base type declarations
- sub_concept: Enum and record sub-type definitions
- concept_comparison: Compile-time pattern matching with conditional guards

Parser changes:
- Added VariantPattern, FieldCondition, and Condition AST nodes
- Implemented "is" keyword for pattern matching (e.g., "CupType is Glass or CupType is Plastic")
- Added Value::Any variant to support universal type matching
- Disambiguated enum-like vs record-like sub_concept syntax

LSP updates:
- Added Value::Any match arms across code_actions, completion, hover, inlay_hints, and semantic_tokens
- Type inference and formatting support for Any values

Example fixes:
- Fixed syntax error in baker-family behaviors (missing closing brace in nested if)
- Removed deprecated core_enums.sb file
This commit is contained in:
2026-02-14 09:28:20 +00:00
parent 6e3b35e68f
commit 25d59d6107
30 changed files with 8639 additions and 6536 deletions

View File

@@ -69,7 +69,6 @@ pub enum Declaration {
Relationship(Relationship),
Location(Location),
Species(Species),
Enum(EnumDecl),
Concept(ConceptDecl),
SubConcept(SubConceptDecl),
ConceptComparison(ConceptComparisonDecl),
@@ -171,6 +170,7 @@ pub enum Value {
Object(Vec<Field>),
ProseBlock(ProseBlock),
Override(Override),
Any, // Special marker for type system - matches any value
}
/// Time literal (HH:MM or HH:MM:SS)
@@ -409,14 +409,6 @@ pub struct Species {
pub span: Span,
}
/// Enum definition
#[derive(Debug, Clone, PartialEq)]
pub struct EnumDecl {
pub name: String,
pub variants: Vec<String>,
pub span: Span,
}
/// Concept declaration - base type definition
#[derive(Debug, Clone, PartialEq)]
pub struct ConceptDecl {
@@ -440,21 +432,35 @@ pub enum SubConceptKind {
Record { fields: Vec<Field> },
}
/// Concept comparison - compile-time enum mapping between two concepts
/// Concept comparison - compile-time pattern matching for concept variants
#[derive(Debug, Clone, PartialEq)]
pub struct ConceptComparisonDecl {
pub name: String,
pub left_concept: String,
pub right_concept: String,
pub mappings: Vec<ConceptMapping>,
pub variants: Vec<VariantPattern>,
pub span: Span,
}
/// A single mapping entry in a concept comparison
/// A variant pattern with field conditions
#[derive(Debug, Clone, PartialEq)]
pub struct ConceptMapping {
pub left_variant: String,
pub right_variant: String,
pub struct VariantPattern {
pub name: String,
pub conditions: Vec<FieldCondition>,
pub span: Span,
}
/// A condition on a field within a variant pattern
#[derive(Debug, Clone, PartialEq)]
pub struct FieldCondition {
pub field_name: String,
pub condition: Condition,
pub span: Span,
}
/// The type of condition for field matching
#[derive(Debug, Clone, PartialEq)]
pub enum Condition {
Any, // matches any value
Is(Vec<String>), // matches specific values (e.g., "is Glass or is Plastic")
}
/// Expression AST for conditions and queries

View File

@@ -98,7 +98,6 @@ pub fn token_is_declaration_keyword(token: &Token) -> bool {
Token::Relationship |
Token::Institution |
Token::Location |
Token::Enum |
Token::Schedule
)
}
@@ -116,7 +115,6 @@ pub fn token_is_structural_keyword(token: &Token) -> bool {
Token::Relationship |
Token::Institution |
Token::Location |
Token::Enum |
Token::Schedule
)
}
@@ -132,7 +130,6 @@ pub fn declaration_token_to_str(token: &Token) -> Option<&'static str> {
| Token::Relationship => Some("relationship"),
| Token::Institution => Some("institution"),
| Token::Location => Some("location"),
| Token::Enum => Some("enum"),
| Token::Schedule => Some("schedule"),
| _ => None,
}

View File

@@ -29,8 +29,6 @@ pub enum Token {
Location,
#[token("species")]
Species,
#[token("enum")]
Enum,
#[token("concept")]
Concept,
#[token("sub_concept")]

View File

@@ -20,7 +20,9 @@ Declaration: Declaration = {
<r:Relationship> => Declaration::Relationship(r),
<loc:Location> => Declaration::Location(loc),
<sp:Species> => Declaration::Species(sp),
<e:EnumDecl> => Declaration::Enum(e),
<concept:ConceptDecl> => Declaration::Concept(concept),
<sub:SubConceptDecl> => Declaration::SubConcept(sub),
<comp:ConceptComparisonDecl> => Declaration::ConceptComparison(comp),
};
// ===== Use declarations =====
@@ -246,6 +248,7 @@ Value: Value = {
<FloatLit> => Value::Float(<>),
<StringLit> => Value::String(<>),
<BoolLit> => Value::Bool(<>),
"any" => Value::Any,
<lo:IntLit> ".." <hi:IntLit> => Value::Range(
Box::new(Value::Int(lo)),
Box::new(Value::Int(hi))
@@ -740,14 +743,120 @@ Species: Species = {
// ===== Enum =====
EnumDecl: EnumDecl = {
"enum" <name:Ident> "{" <variants:Comma<Ident>> "}" => EnumDecl {
// ===== Type System Declarations =====
ConceptDecl: ConceptDecl = {
"concept" <name:Ident> => ConceptDecl {
name,
span: Span::new(0, 0),
}
};
SubConceptDecl: SubConceptDecl = {
// Enum-like sub_concept: sub_concept PlateColor { Red, Blue, Green }
"sub_concept" <name:Ident> "{" <variants:Comma<Ident>> "}" => {
let parent = {
let mut last_cap = 0;
for (i, ch) in name.char_indices().skip(1) {
if ch.is_uppercase() {
last_cap = i;
}
}
if last_cap > 0 {
name[..last_cap].to_string()
} else {
name.clone()
}
};
SubConceptDecl {
name,
parent_concept: parent,
kind: SubConceptKind::Enum { variants },
span: Span::new(0, 0),
}
},
// Record-like sub_concept with at least one field
"sub_concept" <name:Ident> "{" <first:Ident> ":" <first_val:Value> <rest:("," <Ident> ":" <Value>)*> ","? "}" => {
let parent = {
let mut last_cap = 0;
for (i, ch) in name.char_indices().skip(1) {
if ch.is_uppercase() {
last_cap = i;
}
}
if last_cap > 0 {
name[..last_cap].to_string()
} else {
name.clone()
}
};
let mut fields = vec![Field {
name: first,
value: first_val,
span: Span::new(0, 0),
}];
for (field_name, field_val) in rest {
fields.push(Field {
name: field_name,
value: field_val,
span: Span::new(0, 0),
});
}
SubConceptDecl {
name,
parent_concept: parent,
kind: SubConceptKind::Record { fields },
span: Span::new(0, 0),
}
},
};
ConceptComparisonDecl: ConceptComparisonDecl = {
"concept_comparison" <name:Ident> "{" <variants:Comma<VariantPattern>> "}" => ConceptComparisonDecl {
name,
variants,
span: Span::new(0, 0),
}
};
VariantPattern: VariantPattern = {
<name:Ident> ":" "{" <conditions:Comma<FieldCondition>> "}" => VariantPattern {
name,
conditions,
span: Span::new(0, 0),
}
};
FieldCondition: FieldCondition = {
<field:Ident> ":" "any" => FieldCondition {
field_name: field,
condition: Condition::Any,
span: Span::new(0, 0),
},
<field:Ident> ":" <cond:IsCondition> => FieldCondition {
field_name: field,
condition: Condition::Is(cond),
span: Span::new(0, 0),
},
};
// Parse "FieldName is Value1 or FieldName is Value2" and extract the values
IsCondition: Vec<String> = {
<first:IsValue> <rest:("or" <IsValue>)*> => {
let mut values = vec![first];
values.extend(rest);
values
}
};
IsValue: String = {
<field:Ident> "is" <value:Ident> => value
};
// ===== Expressions =====
// Expression grammar with proper precedence:
// or > and > not > field_access > comparison > term
@@ -877,7 +986,10 @@ extern {
"relationship" => Token::Relationship,
"location" => Token::Location,
"species" => Token::Species,
"enum" => Token::Enum,
"concept" => Token::Concept,
"sub_concept" => Token::SubConcept,
"concept_comparison" => Token::ConceptComparison,
"any" => Token::Any,
"state" => Token::State,
"on" => Token::On,
"enter" => Token::Enter,

File diff suppressed because it is too large Load Diff

View File

@@ -179,7 +179,7 @@ proptest! {
#[test]
fn test_keywords_are_distinct_from_idents(
keyword in prop::sample::select(vec![
"character", "template", "enum", "use", "self", "other",
"character", "template", "use", "self", "other",
"and", "or", "not", "is", "true", "false",
"if", "when", "choose", "then", "include"
])
@@ -249,11 +249,6 @@ fn valid_template() -> impl Strategy<Value = String> {
})
}
fn valid_enum() -> impl Strategy<Value = String> {
(valid_ident(), prop::collection::vec(valid_ident(), 1..10))
.prop_map(|(name, variants)| format!("enum {} {{ {} }}", name, variants.join(", ")))
}
fn valid_schedule() -> impl Strategy<Value = String> {
(valid_ident(), prop::collection::vec(valid_time(), 1..5)).prop_map(|(name, times)| {
let blocks = times
@@ -366,22 +361,6 @@ proptest! {
}
}
#[test]
fn test_valid_enum_parses(input in valid_enum()) {
let lexer = Lexer::new(&input);
let parser = FileParser::new();
let result = parser.parse(lexer);
assert!(result.is_ok(), "Failed to parse valid enum: {}\nError: {:?}", input, result.err());
if let Ok(file) = result {
assert_eq!(file.declarations.len(), 1);
match &file.declarations[0] {
crate::syntax::ast::Declaration::Enum(_) => {},
_ => panic!("Expected Enum declaration"),
}
}
}
#[test]
fn test_valid_schedule_parses(input in valid_schedule()) {
let lexer = Lexer::new(&input);
@@ -466,11 +445,9 @@ proptest! {
fn test_multiple_declarations_parse(
chars in prop::collection::vec(valid_character(), 0..3),
templates in prop::collection::vec(valid_template(), 0..3),
enums in prop::collection::vec(valid_enum(), 0..3),
) {
let mut all = chars;
all.extend(templates);
all.extend(enums);
let input = all.join("\n\n");
let lexer = Lexer::new(&input);