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
//! Per-variant validators for binary operators, `for`-loop iteration,
//! `if`-condition typing, and let-pattern destructuring shape checks.
use super::super::super::module_resolver::ModuleResolver;
use super::super::super::sem_type::SemType;
use super::super::super::SemanticAnalyzer;
use crate::ast::{BinaryOperator, BindingPattern, Expr, File};
use crate::error::CompilerError;
use crate::location::Span;
impl<R: ModuleResolver> SemanticAnalyzer<R> {
/// Validate binary operator type compatibility
pub(super) fn validate_binary_op(
&mut self,
left: &Expr,
op: BinaryOperator,
right: &Expr,
span: Span,
file: &File,
) {
let left_sem = self.infer_type_sem(left, file);
let right_sem = self.infer_type_sem(right, file);
// Skip validation when either operand type is unknown (field access, method calls, etc.)
if left_sem.is_unknown() || right_sem.is_unknown() {
return;
}
let left_type = left_sem.display();
let right_type = right_sem.display();
// Numeric primitives accepted for arithmetic/comparison/range;
// both operands must agree (no implicit width promotion).
// Backend-specific scalar/vector types are the codegen pass's job.
let is_numeric = |s: &str| matches!(s, "I32" | "I64" | "F32" | "F64");
let valid = match op {
// Add: matched-numeric pair, or String + String (concatenation)
BinaryOperator::Add => {
(is_numeric(&left_type) && left_type == right_type)
|| (left_type == "String" && right_type == "String")
}
// Arithmetic, comparison, and range operators: matched-numeric pair
BinaryOperator::Sub
| BinaryOperator::Mul
| BinaryOperator::Div
| BinaryOperator::Mod
| BinaryOperator::Lt
| BinaryOperator::Gt
| BinaryOperator::Le
| BinaryOperator::Ge
| BinaryOperator::Range => is_numeric(&left_type) && left_type == right_type,
// Equality operators: same types
BinaryOperator::Eq | BinaryOperator::Ne => left_type == right_type,
// Logical operators: Boolean + Boolean
BinaryOperator::And | BinaryOperator::Or => {
left_type == "Boolean" && right_type == "Boolean"
}
};
if !valid {
self.errors.push(CompilerError::InvalidBinaryOp {
op: format!("{op:?}"),
left_type,
right_type,
span,
});
}
}
/// Validate for loop collection is an array or range
pub(super) fn validate_for_loop(&mut self, collection: &Expr, span: Span, file: &File) {
let collection_sem = self.infer_type_sem(collection, file);
let is_iterable = matches!(collection_sem, SemType::Array(_) | SemType::Unknown)
|| matches!(&collection_sem, SemType::Generic { base, .. } if base == "Range");
if !is_iterable {
self.errors.push(CompilerError::ForLoopNotArray {
actual: collection_sem.display(),
span,
});
}
}
/// Validate destructuring pattern matches the value type
pub(in crate::semantic::validation) fn validate_destructuring_pattern(
&mut self,
pattern: &BindingPattern,
value: &Expr,
span: Span,
file: &File,
) {
let value_sem = self.infer_type_sem(value, file);
// Skip destructuring validation when value type is unknown (field access, etc.)
if value_sem.is_unknown() {
return;
}
match pattern {
BindingPattern::Array { elements, .. } => {
// Array destructuring requires an array type
if !matches!(value_sem, SemType::Array(_)) {
self.errors.push(CompilerError::ArrayDestructuringNotArray {
actual: value_sem.display(),
span,
});
} else if let Expr::Array {
elements: literal_elems,
..
} = value
{
// Known array length: pattern must not demand more fixed
// elements than the array provides. Partial patterns that
// cover fewer positions than the array (e.g.,
// `let [a, b] = [1, 2, 3]`) are permitted — extra values
// are simply unbound. A rest element accepts any tail.
let pattern_fixed = elements
.iter()
.filter(|e| !matches!(e, crate::ast::ArrayPatternElement::Rest(_)))
.count();
let actual = literal_elems.len();
if pattern_fixed > actual {
self.errors.push(CompilerError::TypeMismatch {
expected: format!("array with at least {pattern_fixed} element(s)"),
found: format!("array with {actual} element(s)"),
span,
});
}
}
}
BindingPattern::Struct { fields, .. } => {
// Struct destructuring requires a struct type.
// The type may also be `Generic { base, .. }` for instantiated
// generic structs — strip args for the lookup.
let lookup_name = match &value_sem {
SemType::Generic { base, .. } | SemType::Named(base) => Some(base.as_str()),
SemType::Primitive(_)
| SemType::Array(_)
| SemType::Optional(_)
| SemType::Tuple(_)
| SemType::Dictionary { .. }
| SemType::Closure { .. }
| SemType::Unknown
| SemType::InferredEnum
| SemType::Nil => None,
};
if let Some(struct_info) = lookup_name.and_then(|n| self.symbols.get_struct(n)) {
let field_names: Vec<&str> =
struct_info.fields.iter().map(|f| f.name.as_str()).collect();
for field in fields {
if !field_names.contains(&field.name.name.as_str()) {
self.errors.push(CompilerError::UnknownField {
field: field.name.name.clone(),
type_name: value_sem.display(),
span: field.name.span,
});
}
}
} else {
// Not a known struct - report error (includes primitives)
self.errors
.push(CompilerError::StructDestructuringNotStruct {
actual: value_sem.display(),
span,
});
}
}
BindingPattern::Tuple { elements, .. } => {
// Validate tuple pattern arity against tuple type "(x: T, y: U, ...)"
if let SemType::Tuple(fields) = &value_sem {
let field_count = fields.len();
let pattern_count = elements.len();
if pattern_count > field_count && field_count > 0 {
self.errors.push(CompilerError::TypeMismatch {
expected: format!("tuple with {field_count} field(s)"),
found: value_sem.display(),
span,
});
}
}
}
BindingPattern::Simple(_) => {
// Simple patterns don't require type validation here
}
}
}
/// Validate if condition is boolean or optional
pub(super) fn validate_if_condition(&mut self, condition: &Expr, span: Span, file: &File) {
use crate::ast::PrimitiveType;
let condition_sem = self.infer_type_sem(condition, file);
// Skip when type is unknown (field access, method calls — IR lowering handles these)
if condition_sem.is_unknown() {
return;
}
// Condition must be Boolean or optional
let is_valid = matches!(
condition_sem,
SemType::Primitive(PrimitiveType::Boolean) | SemType::Optional(_)
);
if !is_valid {
self.errors.push(CompilerError::InvalidIfCondition {
actual: condition_sem.display(),
span,
});
}
}
}