From 6ca60b71c0bf187dfe99201da4aea953f947b09b Mon Sep 17 00:00:00 2001 From: LunaStev Date: Sat, 8 Aug 2026 17:58:51 +0900 Subject: [PATCH 1/2] Harden semantic validation and preserve source diagnostics Replace the minimal string-based verifier with a program-aware semantic validation pass that rejects invalid Wave programs before they reach LLVM code generation. Semantic and type-system changes: - collect functions, externs, globals, aliases, structs, enums, methods, and generic declarations before validating bodies - reject duplicate top-level symbols, local bindings, fields, variants, methods, aliases, parameters, and declarations in the same scope while preserving nested shadowing - validate unknown, void, pointer, array, named, alias, struct, enum, and generic types in declarations, signatures, casts, fields, and expressions - enforce function return contracts, missing-return detection, void-value restrictions, loop-only break and continue, and call argument compatibility - support default parameters and generic function, struct, field, and method type substitution without regressing pre-monomorphization validation - reject incorrect generic arity early, including errors originating in imported source files - require mutable lvalues for input and mutation, validate assignment and compound-assignment operands, and reject unsupported increment and decrement operations - validate explicit casts, contextual array literals, addressed arrays, integer literal ranges, pointer and integer conversions, and float width conversions - reject duplicate match constants by their evaluated numeric value and enforce consistent mixed-width floating-point semantics - reject aggregate formatting until a defined formatter contract exists while retaining scalar, string, pointer, and null formatting Structured diagnostic and import provenance changes: - add SemanticDiagnostic with a stable error code, message, top-level node index, primary span hint, label, note, and help text - retain the legacy string-returning parser API as a compatibility wrapper around detailed diagnostics - preserve imported source text and associate every expanded top-level AST node with its original source unit - validate the expanded AST before generic monomorphization so user-facing semantic failures retain their original source location - replace message parsing and heuristic substring lookup with structured declaration, keyword, and identifier span hints - scope occurrence resolution to the relevant top-level node so repeated identifiers, repeated returns, and duplicate declarations point at the actual failing occurrence - report semantic failures in imported modules against the imported path, line, column, and highlighted source span instead of the entry file at 1:1 LLVM and control-flow fixes: - stop emitting statements after a basic block already has a terminator - mark unreachable merge blocks and provably non-breaking infinite-loop exits as unreachable - use implicit coercion rules for return lowering after semantic validation has established compatibility - add explicit floating-point widening and narrowing emission with LLVM float casts Regression and corpus updates: - add integration coverage for invalid returns, missing returns, loop control, calls, casts, lvalues, duplicate symbols, unknown and void types, array contexts, integer truncation, match duplicates, formatting, float widths, generics, and diagnostic source locations - verify check and build reject the same invalid programs before backend panics or E9001 failures - cover repeated local and return locations, duplicate top-level declarations, imported unknown types, and imported generic arity diagnostics - update TCP and overflow examples to use explicit narrowing casts and avoid formatting an aggregate without a formatter - ignore the local .tmp planning directory Validation completed: - cargo fmt --all -- --check - cargo test --locked --all-targets: 15 tests passed - cargo clippy --locked --all-targets -- -D warnings - cargo build --locked --release - Wave end-to-end suite: 96 passed, 12 environment or architecture skips, 0 failed - all 13 examples passed wavec check - all 79 standard-library modules passed wavec check - git diff --check --- .gitignore | 1 + examples/tcp.wave | 2 +- front/parser/src/import.rs | 8 +- front/parser/src/verification.rs | 2791 +++++++++++++++++++++++++++--- llvm/src/codegen/ir.rs | 6 + llvm/src/statement/control.rs | 60 +- llvm/src/statement/mod.rs | 7 + llvm/src/statement/variable.rs | 6 + src/runner.rs | 328 +++- test/test56.wave | 2 +- test/test69.wave | 2 +- test/test71.wave | 4 +- tests/codegen_regressions.rs | 685 ++++++++ 13 files changed, 3642 insertions(+), 260 deletions(-) diff --git a/.gitignore b/.gitignore index c117f076..26af3e59 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,7 @@ desktop.ini *.old *.orig *.rej +/.tmp/ /tmp/ /temp/ diff --git a/examples/tcp.wave b/examples/tcp.wave index 73a2f062..f97f169d 100644 --- a/examples/tcp.wave +++ b/examples/tcp.wave @@ -107,7 +107,7 @@ fun syscall5(id: i64, a1: i64, a2: i64, a3: i64, p4: ptr, a5: i64) -> i64 { fun htons(x: i16) -> i16 { var a: i32 = x; var y: i32 = ((a & 255) << 8) | ((a >> 8) & 255); - return y; + return y as i16; } fun _setsockopt_reuseaddr(sockfd: i64) { diff --git a/front/parser/src/import.rs b/front/parser/src/import.rs index f54e72cd..508f7eda 100644 --- a/front/parser/src/import.rs +++ b/front/parser/src/import.rs @@ -332,6 +332,7 @@ pub fn preprocess_target_attrs(source: &str, target: &TargetConditionContext) -> pub struct ImportedUnit { pub abs_path: PathBuf, pub ast: Vec, + pub source: String, } #[derive(Debug, Clone, Default)] @@ -653,6 +654,7 @@ fn parse_wave_file( return Ok(ImportedUnit { abs_path, ast: vec![], + source: String::new(), }); } already_imported.insert(abs_path_str); @@ -719,5 +721,9 @@ fn parse_wave_file( we })?; - Ok(ImportedUnit { abs_path, ast }) + Ok(ImportedUnit { + abs_path, + ast, + source: content, + }) } diff --git a/front/parser/src/verification.rs b/front/parser/src/verification.rs index 33608343..1d65999e 100644 --- a/front/parser/src/verification.rs +++ b/front/parser/src/verification.rs @@ -10,337 +10,2684 @@ // SPDX-License-Identifier: MPL-2.0 // AI TRAINING NOTICE: Prohibited without prior written permission. No use for machine learning or generative AI training, fine-tuning, distillation, embedding, or dataset creation. -use crate::ast::{ASTNode, Expression, MatchPattern, Mutability, StatementNode}; +use crate::ast::{ + ASTNode, AssignOperator, Expression, FunctionNode, Literal, MatchPattern, Mutability, Operator, + StatementNode, WaveType, +}; +use crate::types::{parse_type, split_top_level_generic_args, token_type_to_wave_type}; use std::collections::{HashMap, HashSet}; +use std::fmt; -fn lookup_mutability( - name: &str, - scopes: &Vec>, - globals: &HashMap, -) -> Option { - for scope in scopes.iter().rev() { - if let Some(m) = scope.get(name) { - return Some(*m); - } +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum SemanticSpanKind { + Declaration, + Keyword, + Identifier, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SemanticSpanHint { + pub kind: SemanticSpanKind, + pub text: String, + pub occurrence: usize, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SemanticDiagnostic { + pub code: String, + pub message: String, + pub top_level_index: usize, + pub primary: Option, + pub label: String, + pub note: Option, + pub help: String, +} + +impl fmt::Display for SemanticDiagnostic { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(&self.message) } - globals.get(name).copied() } -fn find_base_var(target: &Expression, saw_deref: bool) -> Option<(String, bool)> { - match target { - Expression::Variable(name) => Some((name.clone(), saw_deref)), - Expression::Grouped(inner) => find_base_var(inner, saw_deref), +impl std::error::Error for SemanticDiagnostic {} - Expression::FieldAccess { object, .. } => find_base_var(object, saw_deref), - Expression::IndexAccess { target, .. } => find_base_var(target, saw_deref), +#[derive(Clone, Debug)] +struct Binding { + mutability: Mutability, + ty: WaveType, +} - Expression::Deref(inner) => find_base_var(inner, true), +#[derive(Clone, Debug)] +struct FunctionType { + params: Vec, + required_params: usize, + return_type: WaveType, + generic_params: Vec, +} - _ => None, - } +#[derive(Clone, Debug)] +enum ExpressionType { + Known(WaveType), + IntLiteral(String), + FloatLiteral, + Null, + ArrayLiteral(Vec), + AddressedArrayLiteral(Vec), + Unknown, } -fn ensure_mutable_write_target( - target: &Expression, - scopes: &Vec>, - globals: &HashMap, - why: &str, -) -> Result<(), String> { - let Some((base, saw_deref)) = find_base_var(target, false) else { - return Ok(()); - }; +#[derive(Default)] +struct ProgramTypes { + functions: HashMap, + methods: HashMap<(String, String), FunctionType>, + structs: HashMap>, + aliases: HashMap, + enum_reprs: HashMap, + globals: HashMap, + constant_values: HashMap, + type_names: HashSet, + generic_type_params: HashSet, + struct_generic_params: HashMap>, +} - if saw_deref { - return Ok(()); +impl ProgramTypes { + fn collect(nodes: &[ASTNode]) -> Result)> { + let mut out = Self::default(); + + for (index, node) in nodes.iter().enumerate() { + let type_name = match node { + ASTNode::Struct(structure) => Some(structure.name.as_str()), + ASTNode::TypeAlias(alias) => Some(alias.name.as_str()), + ASTNode::Enum(enumeration) => Some(enumeration.name.as_str()), + _ => None, + }; + if let Some(name) = type_name { + if !out.type_names.insert(name.to_string()) { + return Err(( + index, + format!("duplicate type declaration `{}`", name), + Some(top_level_span_hint(node)), + )); + } + } + match node { + ASTNode::Function(function) => { + out.generic_type_params + .extend(function.generic_params.iter().cloned()); + } + ASTNode::Struct(structure) => { + out.generic_type_params + .extend(structure.generic_params.iter().cloned()); + for method in &structure.methods { + out.generic_type_params + .extend(method.generic_params.iter().cloned()); + } + } + ASTNode::ProtoImpl(implementation) => { + for method in &implementation.methods { + out.generic_type_params + .extend(method.generic_params.iter().cloned()); + } + } + _ => {} + } + } + + let mut value_names = HashSet::new(); + + for (index, node) in nodes.iter().enumerate() { + let failure = + |message: String, primary: Option| (index, message, primary); + match node { + ASTNode::Function(function) => { + insert_unique_value_name(&mut value_names, &function.name) + .map_err(|message| failure(message, Some(top_level_span_hint(node))))?; + insert_unique_function( + &mut out.functions, + &function.name, + function_type(function), + ) + .map_err(|message| failure(message, Some(top_level_span_hint(node))))?; + } + ASTNode::ExternFunction(function) => { + insert_unique_value_name(&mut value_names, &function.name) + .map_err(|message| failure(message, Some(top_level_span_hint(node))))?; + insert_unique_function( + &mut out.functions, + &function.name, + FunctionType { + params: function.params.iter().map(|(_, ty)| ty.clone()).collect(), + required_params: function.params.len(), + return_type: function.return_type.clone(), + generic_params: Vec::new(), + }, + ) + .map_err(|message| failure(message, Some(top_level_span_hint(node))))?; + } + ASTNode::Struct(structure) => { + let mut fields = HashMap::new(); + for (name, ty) in &structure.fields { + if fields.insert(name.clone(), ty.clone()).is_some() { + return Err(failure( + format!( + "duplicate field `{}` in struct `{}`", + name, structure.name + ), + Some(SemanticSpanHint { + kind: SemanticSpanKind::Declaration, + text: name.clone(), + occurrence: 2, + }), + )); + } + } + out.structs.insert(structure.name.clone(), fields); + out.struct_generic_params + .insert(structure.name.clone(), structure.generic_params.clone()); + for method in &structure.methods { + insert_unique_method( + &mut out.methods, + &structure.name, + &method.name, + function_type(method), + ) + .map_err(|message| { + failure( + message, + Some(SemanticSpanHint { + kind: SemanticSpanKind::Declaration, + text: method.name.clone(), + occurrence: 2, + }), + ) + })?; + } + } + ASTNode::ProtoImpl(implementation) => { + for method in &implementation.methods { + let signature = function_type(method); + insert_unique_method( + &mut out.methods, + &implementation.target, + &method.name, + signature.clone(), + ) + .map_err(|message| { + failure( + message, + Some(SemanticSpanHint { + kind: SemanticSpanKind::Declaration, + text: method.name.clone(), + occurrence: 2, + }), + ) + })?; + let lowered = format!("{}_{}", implementation.target, method.name); + insert_unique_value_name(&mut value_names, &lowered) + .map_err(|message| failure(message, Some(top_level_span_hint(node))))?; + insert_unique_function(&mut out.functions, &lowered, signature) + .map_err(|message| failure(message, Some(top_level_span_hint(node))))?; + } + } + ASTNode::TypeAlias(alias) => { + out.aliases.insert(alias.name.clone(), alias.target.clone()); + } + ASTNode::Variable(variable) + if matches!(variable.mutability, Mutability::Const | Mutability::Static) => + { + insert_unique_value_name(&mut value_names, &variable.name) + .map_err(|message| failure(message, Some(top_level_span_hint(node))))?; + out.globals.insert( + variable.name.clone(), + Binding { + mutability: variable.mutability, + ty: variable.type_name.clone(), + }, + ); + if matches!(variable.mutability, Mutability::Const) { + if let Some(Expression::Literal(Literal::Int(raw))) = + &variable.initial_value + { + if let Some(value) = parse_integer_value(raw) { + out.constant_values.insert(variable.name.clone(), value); + } + } + } + } + ASTNode::Enum(enumeration) => { + out.enum_reprs + .insert(enumeration.name.clone(), enumeration.repr_type.clone()); + let mut variants = HashSet::new(); + let mut next = 0i128; + for variant in &enumeration.variants { + if !variants.insert(variant.name.clone()) { + return Err(failure( + format!( + "duplicate variant `{}` in enum `{}`", + variant.name, enumeration.name + ), + Some(SemanticSpanHint { + kind: SemanticSpanKind::Declaration, + text: variant.name.clone(), + occurrence: 2, + }), + )); + } + insert_unique_value_name(&mut value_names, &variant.name) + .map_err(|message| failure(message, Some(top_level_span_hint(node))))?; + out.globals.insert( + variant.name.clone(), + Binding { + mutability: Mutability::Const, + ty: enumeration.repr_type.clone(), + }, + ); + if let Some(raw) = &variant.explicit_value { + next = parse_integer_value(raw).ok_or_else(|| { + failure( + format!( + "enum `{}.{}` has invalid integer value `{}`", + enumeration.name, variant.name, raw + ), + Some(SemanticSpanHint { + kind: SemanticSpanKind::Declaration, + text: variant.name.clone(), + occurrence: 1, + }), + ) + })?; + } + out.constant_values.insert(variant.name.clone(), next); + next = next.checked_add(1).ok_or_else(|| { + failure( + format!("enum `{}` value overflow", enumeration.name), + Some(top_level_span_hint(node)), + ) + })?; + } + } + _ => {} + } + } + + Ok(out) } - if let Some(m) = lookup_mutability(&base, scopes, globals) { - match m { - Mutability::Let | Mutability::Const => { - return Err(format!( - "cannot {} immutable binding `{}` ({:?})", - why, base, m - )); + fn is_known_named_type(&self, name: &str) -> bool { + self.type_names.contains(name) + || name + .split_once('<') + .is_some_and(|(base, _)| self.type_names.contains(base.trim())) + } + + fn named_type_base<'a>(&self, name: &'a str) -> &'a str { + name.split_once('<').map_or(name, |(base, _)| base.trim()) + } + + fn struct_fields(&self, name: &str) -> Option<&HashMap> { + self.structs + .get(name) + .or_else(|| self.structs.get(self.named_type_base(name))) + } + + fn generic_substitution(&self, name: &str) -> HashMap { + let Some((base, arguments)) = parse_named_type_application(name) else { + return HashMap::new(); + }; + let Some(parameters) = self.struct_generic_params.get(&base) else { + return HashMap::new(); + }; + parameters.iter().cloned().zip(arguments).collect() + } + + fn struct_field_type(&self, owner: &str, field: &str) -> Option { + let ty = self.struct_fields(owner)?.get(field)?; + Some(substitute_wave_type(ty, &self.generic_substitution(owner))) + } + + fn method_type(&self, owner: &str, name: &str) -> Option { + let signature = self + .methods + .get(&(owner.to_string(), name.to_string())) + .or_else(|| { + self.methods + .get(&(self.named_type_base(owner).to_string(), name.to_string())) + })?; + Some(substitute_function_type( + signature, + &self.generic_substitution(owner), + )) + } + + fn is_generic_placeholder(&self, ty: &WaveType) -> bool { + matches!(ty, WaveType::Struct(name) if self.generic_type_params.contains(name)) + } + + fn validate_type( + &self, + ty: &WaveType, + generic_params: &HashSet, + allow_void: bool, + context: &str, + ) -> Result<(), String> { + match ty { + WaveType::Void if !allow_void => Err(format!("{} cannot use the `void` type", context)), + WaveType::Pointer(inner) | WaveType::Array(inner, _) => { + self.validate_type(inner, generic_params, false, context) + } + WaveType::Struct(name) => { + if generic_params.contains(name) { + return Ok(()); + } + let base = self.named_type_base(name); + if !self.is_known_named_type(name) { + return Err(format!("unknown type `{}` in {}", name, context)); + } + let expected_arity = self.struct_generic_params.get(base).map_or(0, Vec::len); + let arguments = parse_named_type_application(name) + .map(|(_, arguments)| arguments) + .unwrap_or_default(); + if arguments.len() != expected_arity { + return Err(format!( + "type `{}` expects {} generic argument(s), found {} in {}", + base, + expected_arity, + arguments.len(), + context + )); + } + for argument in &arguments { + self.validate_type(argument, generic_params, false, context)?; + } + Ok(()) } - _ => {} + _ => Ok(()), } } - Ok(()) + fn canonical_type(&self, ty: &WaveType) -> WaveType { + self.canonical_type_inner(ty, &mut HashSet::new()) + } + + fn canonical_type_inner(&self, ty: &WaveType, seen: &mut HashSet) -> WaveType { + match ty { + WaveType::Struct(name) => { + if !seen.insert(name.clone()) { + return ty.clone(); + } + let resolved = if let Some(target) = + self.aliases.get(name).or_else(|| self.enum_reprs.get(name)) + { + self.canonical_type_inner(target, seen) + } else if let Some((base, arguments)) = parse_named_type_application(name) { + let arguments = arguments + .iter() + .map(|argument| { + display_wave_type(&self.canonical_type_inner(argument, seen)) + }) + .collect::>() + .join(","); + WaveType::Struct(format!("{}<{}>", base, arguments)) + } else { + ty.clone() + }; + seen.remove(name); + resolved + } + WaveType::Pointer(inner) => { + WaveType::Pointer(Box::new(self.canonical_type_inner(inner, seen))) + } + WaveType::Array(inner, size) => { + WaveType::Array(Box::new(self.canonical_type_inner(inner, seen)), *size) + } + _ => ty.clone(), + } + } +} + +fn insert_unique_value_name(names: &mut HashSet, name: &str) -> Result<(), String> { + if names.insert(name.to_string()) { + Ok(()) + } else { + Err(format!("duplicate value declaration `{}`", name)) + } } -fn validate_expr( - expr: &Expression, - scopes: &Vec>, - globals: &HashMap, +fn insert_unique_function( + functions: &mut HashMap, + name: &str, + signature: FunctionType, ) -> Result<(), String> { - match expr { - Expression::IncDec { target, .. } => { - ensure_mutable_write_target(target, scopes, globals, "modify with ++/--")?; - validate_expr(target, scopes, globals)?; - } + if functions.insert(name.to_string(), signature).is_none() { + Ok(()) + } else { + Err(format!("duplicate function declaration `{}`", name)) + } +} - Expression::AssignOperation { target, value, .. } => { - ensure_mutable_write_target(target, scopes, globals, "assign")?; - validate_expr(target, scopes, globals)?; - validate_expr(value, scopes, globals)?; - } +fn insert_unique_method( + methods: &mut HashMap<(String, String), FunctionType>, + owner: &str, + name: &str, + signature: FunctionType, +) -> Result<(), String> { + if methods + .insert((owner.to_string(), name.to_string()), signature) + .is_none() + { + Ok(()) + } else { + Err(format!("duplicate method `{}.{}`", owner, name)) + } +} - Expression::Assignment { target, value } => { - ensure_mutable_write_target(target, scopes, globals, "assign")?; - validate_expr(target, scopes, globals)?; - validate_expr(value, scopes, globals)?; - } +fn parse_integer_value(raw: &str) -> Option { + let raw = raw.trim().replace('_', ""); + let (negative, unsigned) = if let Some(value) = raw.strip_prefix('-') { + (true, value) + } else { + (false, raw.strip_prefix('+').unwrap_or(&raw)) + }; + let (radix, digits) = if let Some(value) = unsigned + .strip_prefix("0x") + .or_else(|| unsigned.strip_prefix("0X")) + { + (16, value) + } else if let Some(value) = unsigned + .strip_prefix("0b") + .or_else(|| unsigned.strip_prefix("0B")) + { + (2, value) + } else if let Some(value) = unsigned + .strip_prefix("0o") + .or_else(|| unsigned.strip_prefix("0O")) + { + (8, value) + } else { + (10, unsigned) + }; + let value = i128::from_str_radix(digits, radix).ok()?; + if negative { + value.checked_neg() + } else { + Some(value) + } +} - Expression::BinaryExpression { left, right, .. } => { - validate_expr(left, scopes, globals)?; - validate_expr(right, scopes, globals)?; - } +fn function_type(function: &FunctionNode) -> FunctionType { + FunctionType { + params: function + .parameters + .iter() + .map(|parameter| parameter.param_type.clone()) + .collect(), + required_params: function + .parameters + .iter() + .filter(|parameter| parameter.initial_value.is_none()) + .count(), + return_type: function.return_type.clone().unwrap_or(WaveType::Void), + generic_params: function.generic_params.clone(), + } +} - Expression::Unary { expr, .. } => validate_expr(expr, scopes, globals)?, - Expression::Cast { expr, .. } => validate_expr(expr, scopes, globals)?, +fn parse_named_type_application(name: &str) -> Option<(String, Vec)> { + let (base, tail) = name.split_once('<')?; + let inner = tail.strip_suffix('>')?; + let arguments = split_top_level_generic_args(inner)? + .into_iter() + .map(|argument| { + let token = parse_type(&argument)?; + token_type_to_wave_type(&token) + }) + .collect::>>()?; + Some((base.trim().to_string(), arguments)) +} - Expression::FunctionCall { args, .. } => { - for a in args { - validate_expr(a, scopes, globals)?; +fn substitute_wave_type(ty: &WaveType, substitutions: &HashMap) -> WaveType { + match ty { + WaveType::Struct(name) => { + if let Some(substitution) = substitutions.get(name) { + return substitution.clone(); } - } - Expression::MethodCall { object, args, .. } => { - validate_expr(object, scopes, globals)?; - for a in args { - validate_expr(a, scopes, globals)?; + if let Some((base, arguments)) = parse_named_type_application(name) { + let arguments = arguments + .iter() + .map(|argument| { + display_wave_type(&substitute_wave_type(argument, substitutions)) + }) + .collect::>() + .join(", "); + WaveType::Struct(format!("{}<{}>", base, arguments)) + } else { + ty.clone() } } + WaveType::Pointer(inner) => { + WaveType::Pointer(Box::new(substitute_wave_type(inner, substitutions))) + } + WaveType::Array(inner, size) => { + WaveType::Array(Box::new(substitute_wave_type(inner, substitutions)), *size) + } + _ => ty.clone(), + } +} + +fn substitute_function_type( + signature: &FunctionType, + substitutions: &HashMap, +) -> FunctionType { + FunctionType { + params: signature + .params + .iter() + .map(|parameter| substitute_wave_type(parameter, substitutions)) + .collect(), + required_params: signature.required_params, + return_type: substitute_wave_type(&signature.return_type, substitutions), + generic_params: signature.generic_params.clone(), + } +} + +struct Validator<'a> { + program: &'a ProgramTypes, + scopes: Vec>, + current_function: Option, + current_return_type: Option, + current_type_params: HashSet, + loop_depth: usize, + top_level_index: usize, + span_counts: HashMap<(SemanticSpanKind, String), usize>, + primary_span: Option, +} - Expression::IndexAccess { target, index } => { - validate_expr(target, scopes, globals)?; - validate_expr(index, scopes, globals)?; +impl<'a> Validator<'a> { + fn new(program: &'a ProgramTypes) -> Self { + Self { + program, + scopes: vec![HashMap::new()], + current_function: None, + current_return_type: None, + current_type_params: HashSet::new(), + loop_depth: 0, + top_level_index: 0, + span_counts: HashMap::new(), + primary_span: None, } + } - Expression::ArrayLiteral(items) => { - for it in items { - validate_expr(it, scopes, globals)?; - } + fn begin_top_level(&mut self, index: usize, hint: SemanticSpanHint) { + self.top_level_index = index; + self.span_counts.clear(); + self.primary_span = Some(hint); + } + + fn mark_span(&mut self, kind: SemanticSpanKind, text: impl Into) { + let text = text.into(); + let occurrence = self + .span_counts + .entry((kind.clone(), text.clone())) + .and_modify(|count| *count += 1) + .or_insert(1); + self.primary_span = Some(SemanticSpanHint { + kind, + text, + occurrence: *occurrence, + }); + } + + fn diagnostic(&self, message: String) -> SemanticDiagnostic { + SemanticDiagnostic { + code: "E3001".to_string(), + label: message.clone(), + message, + top_level_index: self.top_level_index, + primary: self.primary_span.clone(), + note: None, + help: "fix type, mutability, scope, and control-flow errors".to_string(), } + } + + fn lookup_binding(&self, name: &str) -> Option { + self.scopes + .iter() + .rev() + .find_map(|scope| scope.get(name).cloned()) + .or_else(|| self.program.globals.get(name).cloned()) + } - Expression::FieldAccess { object, .. } => validate_expr(object, scopes, globals)?, + fn with_scope( + &mut self, + validate: impl FnOnce(&mut Self) -> Result, + ) -> Result { + self.scopes.push(HashMap::new()); + let result = validate(self); + self.scopes.pop(); + result + } - Expression::StructLiteral { fields, .. } => { - for (_, v) in fields { - validate_expr(v, scopes, globals)?; + fn validate_function( + &mut self, + function: &FunctionNode, + display_name: &str, + inherited_type_params: &[String], + ) -> Result<(), String> { + let mut declared_type_params: HashSet<&str> = + inherited_type_params.iter().map(String::as_str).collect(); + for param in &function.generic_params { + if !declared_type_params.insert(param) { + return Err(format!( + "duplicate generic parameter `{}` in function `{}`", + param, display_name + )); } } + let previous_function = self.current_function.replace(display_name.to_string()); + let previous_return = self + .current_return_type + .replace(function.return_type.clone().unwrap_or(WaveType::Void)); + let previous_loop_depth = std::mem::replace(&mut self.loop_depth, 0); + let previous_type_params = std::mem::take(&mut self.current_type_params); + self.current_type_params + .extend(inherited_type_params.iter().cloned()); + self.current_type_params + .extend(function.generic_params.iter().cloned()); - Expression::AsmBlock { - inputs, outputs, .. - } => { - for (_, e) in inputs { - validate_expr(e, scopes, globals)?; + for parameter in &function.parameters { + self.program.validate_type( + ¶meter.param_type, + &self.current_type_params, + false, + &format!( + "parameter `{}` of function `{}`", + parameter.name, display_name + ), + )?; + } + let return_type = function.return_type.clone().unwrap_or(WaveType::Void); + self.program.validate_type( + &return_type, + &self.current_type_params, + true, + &format!("return type of function `{}`", display_name), + )?; + + let result = self.with_scope(|validator| { + for parameter in &function.parameters { + validator.insert_current_binding( + parameter.name.clone(), + Binding { + mutability: Mutability::Var, + ty: parameter.param_type.clone(), + }, + "parameter", + )?; } - for (_, e) in outputs { - validate_expr(e, scopes, globals)?; + + let falls_through = validator.validate_block(&function.body)?; + let return_type = function.return_type.clone().unwrap_or(WaveType::Void); + if return_type != WaveType::Void && falls_through { + return Err(format!( + "non-void function `{}` may exit without returning `{}`", + display_name, + display_wave_type(&return_type) + )); } - } - Expression::Deref(inner) | Expression::AddressOf(inner) => { - validate_expr(inner, scopes, globals)?; - } + Ok(()) + }); - Expression::Null => {} + self.current_function = previous_function; + self.current_return_type = previous_return; + self.loop_depth = previous_loop_depth; + self.current_type_params = previous_type_params; + result + } - Expression::Literal(_) => {} + fn insert_current_binding( + &mut self, + name: String, + binding: Binding, + kind: &str, + ) -> Result<(), String> { + let scope = self.scopes.last_mut().unwrap(); + if scope.contains_key(&name) { + return Err(format!( + "duplicate {} declaration `{}` in the same scope", + kind, name + )); + } + scope.insert(name, binding); + Ok(()) + } - #[allow(clippy::collapsible_match)] - Expression::Variable(name) => { - if lookup_mutability(name, scopes, globals).is_none() { - return Err(format!("use of undeclared identifier `{}`", name)); + fn validate_block(&mut self, nodes: &[ASTNode]) -> Result { + let mut falls_through = true; + for node in nodes { + let node_falls_through = self.validate_node(node)?; + if falls_through { + falls_through = node_falls_through; } } + Ok(falls_through) + } - _ => {} + fn validate_scoped_block(&mut self, nodes: &[ASTNode]) -> Result { + self.with_scope(|validator| validator.validate_block(nodes)) } - Ok(()) -} + fn validate_node(&mut self, node: &ASTNode) -> Result { + match node { + ASTNode::Variable(variable) => { + self.mark_span(SemanticSpanKind::Declaration, variable.name.clone()); + self.program.validate_type( + &variable.type_name, + &self.current_type_params, + false, + &format!("variable `{}`", variable.name), + )?; + if self.scopes.last().unwrap().contains_key(&variable.name) { + return Err(format!( + "duplicate variable declaration `{}` in the same scope", + variable.name + )); + } + let asm_initializer = + matches!(variable.initial_value, Some(Expression::AsmBlock { .. })); + if asm_initializer { + self.insert_current_binding( + variable.name.clone(), + Binding { + mutability: variable.mutability, + ty: variable.type_name.clone(), + }, + "variable", + )?; + } -fn validate_node( - node: &ASTNode, - scopes: &mut Vec>, - globals: &HashMap, -) -> Result<(), String> { - match node { - ASTNode::Variable(v) => { - scopes - .last_mut() - .unwrap() - .insert(v.name.clone(), v.mutability.clone()); - } + if let Some(initial_value) = &variable.initial_value { + let actual = self.validate_expr(initial_value)?; + self.require_assignable( + &actual, + &variable.type_name, + &format!("initializer for `{}`", variable.name), + )?; + } - ASTNode::Statement(stmt) => match stmt { - StatementNode::Expression(e) => validate_expr(e, scopes, globals)?, + if !asm_initializer { + self.insert_current_binding( + variable.name.clone(), + Binding { + mutability: variable.mutability, + ty: variable.type_name.clone(), + }, + "variable", + )?; + } + Ok(true) + } + ASTNode::Statement(statement) => self.validate_statement(statement), + ASTNode::Expression(expression) => { + self.validate_expr(expression)?; + Ok(true) + } + _ => Ok(true), + } + } + fn validate_statement(&mut self, statement: &StatementNode) -> Result { + match statement { + StatementNode::Expression(expression) => { + self.validate_expr(expression)?; + Ok(true) + } StatementNode::Assign { variable, value } => { - let fake_target = Expression::Variable(variable.clone()); - ensure_mutable_write_target(&fake_target, scopes, globals, "assign")?; - validate_expr(value, scopes, globals)?; + self.mark_span(SemanticSpanKind::Identifier, variable.clone()); + let binding = self + .lookup_binding(variable) + .ok_or_else(|| format!("use of undeclared identifier `{}`", variable))?; + self.ensure_mutable_binding(variable, &binding, "assign")?; + let actual = self.validate_expr(value)?; + self.require_assignable( + &actual, + &binding.ty, + &format!("assignment to `{}`", variable), + )?; + Ok(true) } - - StatementNode::PrintlnFormat { args, .. } | StatementNode::PrintFormat { args, .. } => { - for a in args { - validate_expr(a, scopes, globals)?; + StatementNode::PrintFormat { args, .. } | StatementNode::PrintlnFormat { args, .. } => { + self.mark_span(SemanticSpanKind::Keyword, "println|print"); + for argument in args { + let ty = self.validate_expr(argument)?; + self.validate_format_argument(&ty)?; } + Ok(true) + } + StatementNode::Input { args, .. } => { + self.mark_span(SemanticSpanKind::Keyword, "input"); + for argument in args { + if !is_lvalue_expression(argument) { + return Err("input argument must be a mutable lvalue".to_string()); + } + self.ensure_mutable_write_target(argument, "write input into")?; + let ty = self.validate_expr(argument)?; + let supported = match &ty { + ExpressionType::Known(ty) => matches!( + self.program.canonical_type(ty), + WaveType::Bool + | WaveType::Int(_) + | WaveType::Uint(_) + | WaveType::Float(_) + | WaveType::Char + | WaveType::Byte + ), + _ => false, + }; + if !supported { + return Err(format!( + "input argument must be a mutable scalar lvalue, found `{}`", + display_expression_type(&ty) + )); + } + } + Ok(true) } - - StatementNode::Return(Some(e)) => validate_expr(e, scopes, globals)?, - StatementNode::If { condition, body, else_if_blocks, else_block, } => { - validate_expr(condition, scopes, globals)?; - - scopes.push(HashMap::new()); - for n in body { - validate_node(n, scopes, globals)?; - } - scopes.pop(); + self.mark_span(SemanticSpanKind::Keyword, "if"); + self.validate_condition(condition, "if condition")?; + let mut any_branch_falls_through = self.validate_scoped_block(body)?; if let Some(blocks) = else_if_blocks { - for (cond, b) in blocks.iter() { - validate_expr(cond, scopes, globals)?; - scopes.push(HashMap::new()); - for n in b { - validate_node(n, scopes, globals)?; - } - scopes.pop(); + for (condition, block) in blocks.iter() { + self.validate_condition(condition, "else-if condition")?; + any_branch_falls_through |= self.validate_scoped_block(block)?; } } - if let Some(b) = else_block { - scopes.push(HashMap::new()); - for n in b.iter() { - validate_node(n, scopes, globals)?; - } - scopes.pop(); + if let Some(block) = else_block { + any_branch_falls_through |= self.validate_scoped_block(block)?; + } else { + any_branch_falls_through = true; } - } - StatementNode::While { condition, body } => { - validate_expr(condition, scopes, globals)?; - scopes.push(HashMap::new()); - for n in body { - validate_node(n, scopes, globals)?; - } - scopes.pop(); + Ok(any_branch_falls_through) } + StatementNode::While { condition, body } => { + self.mark_span(SemanticSpanKind::Keyword, "while"); + self.validate_condition(condition, "while condition")?; + self.loop_depth += 1; + let body_result = self.validate_scoped_block(body); + self.loop_depth -= 1; + body_result?; + Ok(!expression_is_true(condition) || block_breaks_current_loop(body)) + } StatementNode::For { initialization, condition, increment, body, - } => { - scopes.push(HashMap::new()); - - validate_node(initialization, scopes, globals)?; - validate_expr(condition, scopes, globals)?; - validate_expr(increment, scopes, globals)?; - - for n in body { - validate_node(n, scopes, globals)?; - } - - scopes.pop(); - } + } => self.with_scope(|validator| { + validator.mark_span(SemanticSpanKind::Keyword, "for"); + validator.validate_node(initialization)?; + validator.validate_condition(condition, "for condition")?; + validator.validate_expr(increment)?; + validator.loop_depth += 1; + let body_result = validator.validate_block(body); + validator.loop_depth -= 1; + body_result?; + Ok(!expression_is_true(condition) || block_breaks_current_loop(body)) + }), StatementNode::Match { value, arms } => { - validate_expr(value, scopes, globals)?; + self.mark_span(SemanticSpanKind::Keyword, "match"); + let value_type = self.validate_expr(value)?; + if !self.is_integer_expression(&value_type) { + return Err(format!( + "match value must be an integer or enum, found `{}`", + display_expression_type(&value_type) + )); + } + let mut seen = HashSet::new(); + let mut has_wildcard = false; + let mut all_arms_terminate = !arms.is_empty(); - let mut seen: HashSet = HashSet::new(); for arm in arms { let key = match &arm.pattern { - MatchPattern::Int(raw) => format!("int:{}", raw.trim().replace('_', "")), - MatchPattern::Ident(name) => format!("ident:{}", name), - MatchPattern::Wildcard => "wildcard:_".to_string(), + MatchPattern::Int(raw) => { + self.mark_span(SemanticSpanKind::Keyword, raw.clone()); + let value = parse_integer_value(raw) + .ok_or_else(|| format!("invalid integer match case `{}`", raw))?; + format!("value:{}", value) + } + MatchPattern::Ident(name) => { + self.mark_span(SemanticSpanKind::Identifier, name.clone()); + let binding = self + .lookup_binding(name) + .ok_or_else(|| format!("unknown match case constant `{}`", name))?; + if !matches!(binding.mutability, Mutability::Const) + || !is_integer_type(&self.program.canonical_type(&binding.ty)) + { + return Err(format!( + "match case `{}` must name an integer or enum constant", + name + )); + } + let value = + self.program.constant_values.get(name).ok_or_else(|| { + format!( + "match case `{}` does not have a compile-time integer value", + name + ) + })?; + format!("value:{}", value) + } + MatchPattern::Wildcard => { + self.mark_span(SemanticSpanKind::Keyword, "_"); + has_wildcard = true; + "wildcard:_".to_string() + } }; - if !seen.insert(key.clone()) { return Err(format!("duplicate match case pattern `{}`", key)); } - scopes.push(HashMap::new()); - for n in &arm.body { - validate_node(n, scopes, globals)?; - } - scopes.pop(); + all_arms_terminate &= !self.validate_scoped_block(&arm.body)?; } - } - - _ => {} - }, - ASTNode::Function(func) => { - scopes.push(HashMap::new()); - - for p in &func.parameters { - scopes - .last_mut() - .unwrap() - .insert(p.name.clone(), Mutability::Var); + Ok(!(has_wildcard && all_arms_terminate)) + } + StatementNode::Break => { + self.mark_span(SemanticSpanKind::Keyword, "break"); + if self.loop_depth == 0 { + return Err("`break` can only be used inside a loop".to_string()); + } + Ok(false) + } + StatementNode::Continue => { + self.mark_span(SemanticSpanKind::Keyword, "continue"); + if self.loop_depth == 0 { + return Err("`continue` can only be used inside a loop".to_string()); + } + Ok(false) + } + StatementNode::Return(value) => { + self.mark_span(SemanticSpanKind::Keyword, "return"); + self.validate_return(value.as_ref())?; + Ok(false) + } + StatementNode::AsmBlock { + inputs, outputs, .. + } => { + for (_, expression) in inputs.iter().chain(outputs.iter()) { + self.validate_expr(expression)?; + } + Ok(true) } + _ => Ok(true), + } + } + + fn validate_return(&mut self, value: Option<&Expression>) -> Result<(), String> { + let function = self + .current_function + .clone() + .unwrap_or_else(|| "".to_string()); + let expected = self.current_return_type.clone().unwrap_or(WaveType::Void); - for n in &func.body { - validate_node(n, scopes, globals)?; + match (expected, value) { + (WaveType::Void, None) => Ok(()), + (WaveType::Void, Some(_)) => Err(format!( + "void function `{}` cannot return a value", + function + )), + (expected, None) => Err(format!( + "non-void function `{}` must return `{}`", + function, + display_wave_type(&expected) + )), + (expected, Some(expression)) => { + let actual = self.validate_expr(expression)?; + self.require_assignable( + &actual, + &expected, + &format!("return value of function `{}`", function), + ) } + } + } - scopes.pop(); + fn validate_condition(&mut self, expression: &Expression, context: &str) -> Result<(), String> { + let ty = self.validate_expr(expression)?; + if self.is_truthy_expression(&ty) { + return Ok(()); } - #[allow(clippy::collapsible_match)] - ASTNode::ExternFunction(ext) => { - if !ext.abi.eq_ignore_ascii_case("c") { - return Err(format!( - "unsupported extern ABI '{}' for function '{}': only extern(c) is currently supported", - ext.abi, ext.name - )); + Err(format!( + "{} must be bool, numeric, pointer, or string, found `{}`", + context, + display_expression_type(&ty) + )) + } + + fn validate_format_argument(&self, ty: &ExpressionType) -> Result<(), String> { + let supported = match ty { + ExpressionType::IntLiteral(_) + | ExpressionType::FloatLiteral + | ExpressionType::Null + | ExpressionType::Unknown => true, + ExpressionType::Known(ty) => { + self.program.is_generic_placeholder(ty) + || matches!( + self.program.canonical_type(ty), + WaveType::Bool + | WaveType::Int(_) + | WaveType::Uint(_) + | WaveType::Float(_) + | WaveType::Char + | WaveType::Byte + | WaveType::String + | WaveType::Pointer(_) + ) } + ExpressionType::ArrayLiteral(_) | ExpressionType::AddressedArrayLiteral(_) => false, + }; + if supported { + Ok(()) + } else { + Err(format!( + "format argument must be a scalar, string, pointer, or null, found `{}`", + display_expression_type(ty) + )) } - - _ => {} } - Ok(()) -} + fn is_truthy_expression(&self, ty: &ExpressionType) -> bool { + match ty { + ExpressionType::IntLiteral(_) | ExpressionType::FloatLiteral => true, + ExpressionType::Known(ty) => matches!( + self.program.canonical_type(ty), + WaveType::Bool + | WaveType::Int(_) + | WaveType::Uint(_) + | WaveType::Float(_) + | WaveType::Char + | WaveType::Byte + | WaveType::String + | WaveType::Pointer(_) + ), + ExpressionType::Null + | ExpressionType::ArrayLiteral(_) + | ExpressionType::AddressedArrayLiteral(_) + | ExpressionType::Unknown => false, + } + } -pub fn validate_program(nodes: &Vec) -> Result<(), String> { - let mut globals: HashMap = HashMap::new(); + fn is_integer_expression(&self, ty: &ExpressionType) -> bool { + match ty { + ExpressionType::IntLiteral(_) => true, + ExpressionType::Known(ty) => is_integer_type(&self.program.canonical_type(ty)), + _ => false, + } + } - for n in nodes { - match n { - ASTNode::Variable(v) => { - if matches!(v.mutability, Mutability::Const | Mutability::Static) { - globals.insert(v.name.clone(), v.mutability); + fn validate_expr(&mut self, expression: &Expression) -> Result { + match expression { + Expression::Literal(literal) => Ok(match literal { + Literal::Int(raw) => ExpressionType::IntLiteral(raw.clone()), + Literal::Float(_) => ExpressionType::FloatLiteral, + Literal::String(_) => ExpressionType::Known(WaveType::String), + Literal::Bool(_) => ExpressionType::Known(WaveType::Bool), + Literal::Char(_) => ExpressionType::Known(WaveType::Char), + Literal::Byte(_) => ExpressionType::Known(WaveType::Byte), + }), + Expression::Null => Ok(ExpressionType::Null), + Expression::Variable(name) => { + if let Some(binding) = self.lookup_binding(name) { + Ok(ExpressionType::Known(binding.ty)) + } else { + self.mark_span(SemanticSpanKind::Identifier, name.clone()); + Err(format!("use of undeclared identifier `{}`", name)) } } - - // NEW: enum variants are constants - ASTNode::Enum(e) => { - for v in &e.variants { - globals.insert(v.name.clone(), Mutability::Const); + Expression::Grouped(inner) => self.validate_expr(inner), + Expression::Cast { expr, target_type } => { + self.mark_span(SemanticSpanKind::Keyword, "as"); + self.program.validate_type( + target_type, + &self.current_type_params, + false, + "cast target", + )?; + let source = self.validate_expr(expr)?; + if !self.is_valid_cast(&source, target_type) { + return Err(format!( + "invalid cast from `{}` to `{}`", + display_expression_type(&source), + display_wave_type(target_type) + )); + } + Ok(ExpressionType::Known(target_type.clone())) + } + Expression::AddressOf(inner) => { + self.mark_span(SemanticSpanKind::Keyword, "&"); + if !is_lvalue_expression(inner) + && !matches!(inner.as_ref(), Expression::ArrayLiteral(_)) + { + return Err("cannot take the address of a non-lvalue expression".to_string()); + } + let inner_type = self.validate_expr(inner)?; + Ok(match inner_type { + ExpressionType::Known(ty) => { + ExpressionType::Known(WaveType::Pointer(Box::new(ty))) + } + ExpressionType::ArrayLiteral(elements) => { + ExpressionType::AddressedArrayLiteral(elements) + } + _ => ExpressionType::Unknown, + }) + } + Expression::Deref(inner) => { + self.mark_span(SemanticSpanKind::Keyword, "deref"); + if matches!(inner.as_ref(), Expression::FieldAccess { .. }) { + return self.validate_expr(inner); + } + if matches!(inner.as_ref(), Expression::IndexAccess { .. }) { + let indexed_type = self.validate_expr(inner)?; + return Ok(match indexed_type { + ExpressionType::Known(WaveType::Pointer(ty)) => ExpressionType::Known(*ty), + other => other, + }); + } + let inner_type = self.validate_expr(inner)?; + match inner_type { + ExpressionType::Known(WaveType::Pointer(ty)) => Ok(ExpressionType::Known(*ty)), + other => Err(format!( + "deref expects a pointer, found `{}`", + display_expression_type(&other) + )), + } + } + Expression::BinaryExpression { + left, + operator, + right, + } => { + self.mark_span( + SemanticSpanKind::Keyword, + operator_source_symbol(operator).unwrap_or("binary operator"), + ); + let left_type = self.validate_expr(left)?; + let right_type = self.validate_expr(right)?; + infer_binary_type(self.program, operator, left_type, right_type) + } + Expression::Unary { operator, expr } => { + let ty = self.validate_expr(expr)?; + self.validate_unary(operator, ty) + } + Expression::FunctionCall { + name, + type_args, + args, + } => { + self.mark_span(SemanticSpanKind::Identifier, name.clone()); + self.validate_function_call(name, type_args, args) + } + Expression::MethodCall { object, name, args } => { + self.mark_span(SemanticSpanKind::Identifier, name.clone()); + self.validate_method_call(object, name, args) + } + Expression::StructLiteral { name, fields } => { + self.mark_span(SemanticSpanKind::Identifier, name.clone()); + let known_fields = self + .program + .struct_fields(name) + .cloned() + .ok_or_else(|| format!("unknown struct `{}`", name))?; + let mut provided = HashSet::new(); + for (field_name, value) in fields { + self.mark_span(SemanticSpanKind::Declaration, field_name.clone()); + if !provided.insert(field_name.as_str()) { + return Err(format!( + "struct literal `{}` initializes field `{}` more than once", + name, field_name + )); + } + let expected = self + .program + .struct_field_type(name, field_name) + .ok_or_else(|| { + format!("struct `{}` has no field `{}`", name, field_name) + })?; + let actual = self.validate_expr(value)?; + self.require_assignable( + &actual, + &expected, + &format!("field `{}.{}`", name, field_name), + )?; } + let mut missing: Vec<&str> = known_fields + .keys() + .map(String::as_str) + .filter(|field| !provided.contains(field)) + .collect(); + missing.sort_unstable(); + if !missing.is_empty() { + return Err(format!( + "struct literal `{}` is missing field(s): {}", + name, + missing.join(", ") + )); + } + Ok(ExpressionType::Known(WaveType::Struct(name.clone()))) } + Expression::FieldAccess { object, field } => { + self.mark_span(SemanticSpanKind::Identifier, field.clone()); + let object_type = self.validate_expr(object)?; + let structure = match &object_type { + ExpressionType::Known(WaveType::Struct(name)) => Some(name.clone()), + ExpressionType::Known(WaveType::Pointer(inner)) => match inner.as_ref() { + WaveType::Struct(name) => Some(name.clone()), + _ => None, + }, + _ => None, + }; - _ => {} + let structure = structure.ok_or_else(|| { + format!( + "field access requires a struct or pointer-to-struct, found `{}`", + display_expression_type(&object_type) + ) + })?; + let field_type = self + .program + .struct_field_type(&structure, field) + .ok_or_else(|| format!("struct `{}` has no field `{}`", structure, field))?; + Ok(ExpressionType::Known(field_type)) + } + Expression::IndexAccess { target, index } => { + self.mark_span(SemanticSpanKind::Keyword, "["); + let target_type = self.validate_expr(target)?; + let index_type = self.validate_expr(index)?; + if !self.is_integer_expression(&index_type) { + return Err(format!( + "index expression must be an integer, found `{}`", + display_expression_type(&index_type) + )); + } + if !is_codegen_supported_index(index) { + return Err( + "index expression must currently be an integer literal or integer lvalue" + .to_string(), + ); + } + match target_type { + ExpressionType::Known(WaveType::String) => { + Ok(ExpressionType::Known(WaveType::Int(8))) + } + ExpressionType::Known(WaveType::Array(element, _)) => { + Ok(ExpressionType::Known(*element)) + } + ExpressionType::Known(WaveType::Pointer(element)) => match *element { + WaveType::Array(array_element, _) => { + Ok(ExpressionType::Known(*array_element)) + } + other => Ok(ExpressionType::Known(other)), + }, + other => Err(format!( + "index access requires an array or pointer, found `{}`", + display_expression_type(&other) + )), + } + } + Expression::ArrayLiteral(values) => { + self.mark_span(SemanticSpanKind::Keyword, "["); + let mut element_types = Vec::with_capacity(values.len()); + for value in values { + element_types.push(self.validate_expr(value)?); + } + Ok(ExpressionType::ArrayLiteral(element_types)) + } + Expression::Assignment { target, value } => { + self.mark_span(SemanticSpanKind::Keyword, "="); + if !is_lvalue_expression(target) { + return Err("assignment target is not an lvalue".to_string()); + } + self.ensure_mutable_write_target(target, "assign")?; + let target_type = self.validate_expr(target)?; + let value_type = self.validate_expr(value)?; + if let ExpressionType::Known(expected) = &target_type { + let context = find_base_var(target, false) + .map(|(name, _)| format!("assignment to `{}`", name)) + .unwrap_or_else(|| "assignment expression".to_string()); + self.require_assignable(&value_type, expected, &context)?; + } + Ok(target_type) + } + Expression::AssignOperation { + target, + operator, + value, + } => { + self.mark_span( + SemanticSpanKind::Keyword, + assign_operator_source_symbol(operator), + ); + if !is_lvalue_expression(target) { + return Err("compound assignment target is not an lvalue".to_string()); + } + self.ensure_mutable_write_target(target, "modify with compound assignment")?; + let target_type = self.validate_expr(target)?; + let value_type = self.validate_expr(value)?; + if matches!(operator, AssignOperator::Assign) { + if let ExpressionType::Known(expected) = &target_type { + let context = find_base_var(target, false) + .map(|(name, _)| format!("assignment to `{}`", name)) + .unwrap_or_else(|| "assignment expression".to_string()); + self.require_assignable(&value_type, expected, &context)?; + } + return Ok(target_type); + } + let target_is_numeric = match &target_type { + ExpressionType::Known(ty) => is_numeric_type(&self.program.canonical_type(ty)), + _ => false, + }; + let value_is_numeric = match &value_type { + ExpressionType::IntLiteral(_) | ExpressionType::FloatLiteral => true, + ExpressionType::Known(ty) => is_numeric_type(&self.program.canonical_type(ty)), + _ => false, + }; + if !target_is_numeric || !value_is_numeric { + return Err(format!( + "compound assignment `{:?}` requires numeric operands, found `{}` and `{}`", + operator, + display_expression_type(&target_type), + display_expression_type(&value_type) + )); + } + if let ExpressionType::Known(expected) = &target_type { + self.require_assignable( + &value_type, + expected, + "right operand of compound assignment", + )?; + } + Ok(target_type) + } + Expression::IncDec { target, .. } => { + self.mark_span(SemanticSpanKind::Keyword, "++|--"); + if !is_lvalue_expression(target) { + return Err("++/-- target is not an lvalue".to_string()); + } + self.ensure_mutable_write_target(target, "modify with ++/--")?; + let ty = self.validate_expr(target)?; + let supported = match &ty { + ExpressionType::Known(ty) => matches!( + self.program.canonical_type(ty), + WaveType::Int(_) + | WaveType::Uint(_) + | WaveType::Float(_) + | WaveType::Char + | WaveType::Byte + | WaveType::Pointer(_) + ), + _ => false, + }; + if !supported { + return Err(format!( + "++/-- requires a numeric or pointer lvalue, found `{}`", + display_expression_type(&ty) + )); + } + Ok(ty) + } + Expression::AsmBlock { + inputs, outputs, .. + } => { + for (_, expression) in inputs.iter().chain(outputs.iter()) { + self.validate_expr(expression)?; + } + Ok(ExpressionType::Unknown) + } } } - let mut scopes: Vec> = vec![HashMap::new()]; + fn validate_function_call( + &mut self, + name: &str, + type_args: &[WaveType], + args: &[Expression], + ) -> Result { + let signature = self.program.functions.get(name).map(|signature| { + let substitutions: HashMap = signature + .generic_params + .iter() + .cloned() + .zip(type_args.iter().cloned()) + .collect(); + substitute_function_type(signature, &substitutions) + }); + let Some(signature) = signature else { + for argument in args { + self.validate_expr(argument)?; + } + return Err(format!("call to unknown function `{}`", name)); + }; - for n in nodes { - validate_node(n, &mut scopes, &globals)?; + let declared_generic_count = self + .program + .functions + .get(name) + .map_or(0, |function| function.generic_params.len()); + if type_args.len() != declared_generic_count { + return Err(format!( + "function `{}` expects {} generic argument(s), found {}", + name, + declared_generic_count, + type_args.len() + )); + } + + self.validate_call_arguments( + "function", + name, + args, + &signature.params, + signature.required_params, + )?; + Ok(ExpressionType::Known(signature.return_type)) } + fn validate_method_call( + &mut self, + object: &Expression, + name: &str, + args: &[Expression], + ) -> Result { + let object_type = self.validate_expr(object)?; + let structure = match &object_type { + ExpressionType::Known(WaveType::Struct(name)) => Some(name.clone()), + ExpressionType::Known(WaveType::Pointer(inner)) => match inner.as_ref() { + WaveType::Struct(name) => Some(name.clone()), + _ => None, + }, + _ => None, + }; + + if let Some(ref structure) = structure { + if let Some(signature) = self.program.method_type(structure, name) { + if let Some(expected_self) = signature.params.first() { + self.require_assignable( + &object_type, + expected_self, + &format!("receiver of method `{}.{}`", structure, name), + )?; + } + let params = signature.params.get(1..).unwrap_or(&[]); + self.validate_call_arguments( + "method", + name, + args, + params, + signature.required_params.saturating_sub(1), + )?; + return Ok(ExpressionType::Known(signature.return_type)); + } + } + + if let Some(signature) = self.program.functions.get(name).cloned() { + if let Some(expected_self) = signature.params.first() { + self.require_assignable( + &object_type, + expected_self, + &format!("receiver of method-style call `{}`", name), + )?; + self.validate_call_arguments( + "method", + name, + args, + &signature.params[1..], + signature.required_params.saturating_sub(1), + )?; + return Ok(ExpressionType::Known(signature.return_type)); + } + } + + for argument in args { + self.validate_expr(argument)?; + } + match structure { + Some(structure) => Err(format!("struct `{}` has no method `{}`", structure, name)), + None => Err(format!( + "method call `{}` requires a struct receiver, found `{}`", + name, + display_expression_type(&object_type) + )), + } + } + + fn validate_unary( + &self, + operator: &Operator, + ty: ExpressionType, + ) -> Result { + if matches!(&ty, ExpressionType::Known(inner) if self.program.is_generic_placeholder(inner)) + { + return if matches!(operator, Operator::Not | Operator::LogicalNot) { + Ok(ExpressionType::Known(WaveType::Bool)) + } else { + Ok(ty) + }; + } + let canonical = match &ty { + ExpressionType::Known(ty) => Some(self.program.canonical_type(ty)), + ExpressionType::IntLiteral(_) => Some(WaveType::Int(32)), + ExpressionType::FloatLiteral => Some(WaveType::Float(32)), + _ => None, + }; + + let supported = match operator { + Operator::Neg => canonical.as_ref().is_some_and(is_numeric_type), + Operator::Not | Operator::LogicalNot => canonical.as_ref().is_some_and(|ty| { + matches!( + ty, + WaveType::Bool + | WaveType::Int(_) + | WaveType::Uint(_) + | WaveType::Char + | WaveType::Byte + ) + }), + Operator::BitwiseNot => canonical.as_ref().is_some_and(is_integer_type), + _ => false, + }; + + if !supported { + return Err(format!( + "unary operator `{:?}` is not supported for `{}`", + operator, + display_expression_type(&ty) + )); + } + + if matches!(operator, Operator::Not | Operator::LogicalNot) { + Ok(ExpressionType::Known(WaveType::Bool)) + } else if matches!(operator, Operator::Neg) { + match ty { + ExpressionType::IntLiteral(raw) => { + let value = parse_integer_value(&raw) + .and_then(i128::checked_neg) + .ok_or_else(|| format!("integer literal `{}` overflows", raw))?; + Ok(ExpressionType::IntLiteral(value.to_string())) + } + other => Ok(other), + } + } else { + Ok(ty) + } + } + + fn validate_call_arguments( + &mut self, + kind: &str, + name: &str, + args: &[Expression], + params: &[WaveType], + required_params: usize, + ) -> Result<(), String> { + if args.len() < required_params || args.len() > params.len() { + let expectation = if required_params == params.len() { + params.len().to_string() + } else { + format!("between {} and {}", required_params, params.len()) + }; + return Err(format!( + "{} `{}` expects {} argument(s), found {}", + kind, + name, + expectation, + args.len() + )); + } + + for (index, (argument, expected)) in args.iter().zip(params).enumerate() { + let actual = self.validate_expr(argument)?; + self.require_assignable( + &actual, + expected, + &format!("argument {} of {} `{}`", index + 1, kind, name), + )?; + } + Ok(()) + } + + fn require_assignable( + &self, + actual: &ExpressionType, + expected: &WaveType, + context: &str, + ) -> Result<(), String> { + if self.program.is_generic_placeholder(expected) { + return Ok(()); + } + if let ExpressionType::ArrayLiteral(elements) = actual { + let expected = self.program.canonical_type(expected); + let WaveType::Array(element_type, expected_len) = expected else { + return Err(format!( + "type mismatch in {}: expected `{}`, found `array literal`", + context, + display_wave_type(&expected) + )); + }; + if elements.len() != expected_len as usize { + return Err(format!( + "array length mismatch in {}: expected {}, found {}", + context, + expected_len, + elements.len() + )); + } + for (index, element) in elements.iter().enumerate() { + self.require_assignable( + element, + element_type.as_ref(), + &format!("element {} of {}", index, context), + )?; + } + return Ok(()); + } + + if let ExpressionType::AddressedArrayLiteral(elements) = actual { + let expected = self.program.canonical_type(expected); + let WaveType::Pointer(ref pointee) = expected else { + return Err(format!( + "type mismatch in {}: expected `{}`, found `addressed array literal`", + context, + display_wave_type(&expected) + )); + }; + let WaveType::Array(element_type, expected_len) = pointee.as_ref() else { + return Err(format!( + "addressed array literal in {} requires `ptr>`, found `{}`", + context, + display_wave_type(&expected) + )); + }; + if elements.len() != *expected_len as usize { + return Err(format!( + "array length mismatch in {}: expected {}, found {}", + context, + expected_len, + elements.len() + )); + } + for (index, element) in elements.iter().enumerate() { + self.require_assignable( + element, + element_type.as_ref(), + &format!("element {} of {}", index, context), + )?; + } + return Ok(()); + } + + if self.is_assignable(actual, expected) { + return Ok(()); + } + + Err(format!( + "type mismatch in {}: expected `{}`, found `{}`", + context, + display_wave_type(expected), + display_expression_type(actual) + )) + } + + fn is_assignable(&self, actual: &ExpressionType, expected: &WaveType) -> bool { + let expected = self.program.canonical_type(expected); + match actual { + ExpressionType::Unknown => true, + ExpressionType::Null => matches!(expected, WaveType::Pointer(_)), + ExpressionType::ArrayLiteral(_) | ExpressionType::AddressedArrayLiteral(_) => false, + ExpressionType::IntLiteral(raw) => { + integer_literal_fits(raw, &expected) + || (matches!(expected, WaveType::Pointer(_)) && int_literal_is_zero(raw)) + } + ExpressionType::FloatLiteral => matches!(expected, WaveType::Float(_)), + ExpressionType::Known(actual) => { + if self.program.is_generic_placeholder(actual) { + return true; + } + let actual = self.program.canonical_type(actual); + if actual == expected { + return true; + } + + match (&actual, &expected) { + (actual, expected) + if integer_bit_width(actual).is_some() + && integer_bit_width(expected).is_some() => + { + integer_bit_width(actual) <= integer_bit_width(expected) + } + (WaveType::Int(_) | WaveType::Uint(_), WaveType::Float(_)) + | (WaveType::Float(_), WaveType::Int(_) | WaveType::Uint(_)) => true, + (WaveType::String, WaveType::Pointer(inner)) => { + is_byte_like_type(inner.as_ref()) + || matches!(inner.as_ref(), WaveType::String) + } + (WaveType::Pointer(_), WaveType::Pointer(_)) => true, + _ => false, + } + } + } + } + + fn is_valid_cast(&self, source: &ExpressionType, target: &WaveType) -> bool { + let target = self.program.canonical_type(target); + if matches!( + target, + WaveType::Void | WaveType::Array(_, _) | WaveType::Struct(_) + ) { + return false; + } + + if matches!(source, ExpressionType::Unknown) { + return false; + } + if matches!(source, ExpressionType::Null) { + return matches!(target, WaveType::Pointer(_)); + } + if matches!( + source, + ExpressionType::ArrayLiteral(_) | ExpressionType::AddressedArrayLiteral(_) + ) { + return false; + } + + let source = match source { + ExpressionType::Known(ty) => self.program.canonical_type(ty), + ExpressionType::IntLiteral(_) => WaveType::Int(32), + ExpressionType::FloatLiteral => WaveType::Float(32), + _ => return false, + }; + if matches!( + source, + WaveType::Void | WaveType::Array(_, _) | WaveType::Struct(_) + ) { + return false; + } + + let source_integer = integer_bit_width(&source).is_some(); + let target_integer = integer_bit_width(&target).is_some(); + let source_float = matches!(source, WaveType::Float(_)); + let target_float = matches!(target, WaveType::Float(_)); + let source_pointer = is_pointer_like_type(&source); + let target_pointer = matches!(target, WaveType::Pointer(_) | WaveType::String); + + (source_integer && (target_integer || target_float || target_pointer)) + || (source_float && (target_integer || target_float)) + || (source_pointer && (target_integer || target_pointer)) + } + + fn ensure_mutable_write_target( + &self, + target: &Expression, + operation: &str, + ) -> Result<(), String> { + let Some((base, saw_deref)) = find_base_var(target, false) else { + return Ok(()); + }; + if saw_deref { + return Ok(()); + } + + if let Some(binding) = self.lookup_binding(&base) { + self.ensure_mutable_binding(&base, &binding, operation)?; + } + Ok(()) + } + + fn ensure_mutable_binding( + &self, + name: &str, + binding: &Binding, + operation: &str, + ) -> Result<(), String> { + if matches!(binding.mutability, Mutability::Let | Mutability::Const) { + return Err(format!( + "cannot {} immutable binding `{}` ({:?})", + operation, name, binding.mutability + )); + } + Ok(()) + } +} + +fn infer_binary_type( + program: &ProgramTypes, + operator: &Operator, + left: ExpressionType, + right: ExpressionType, +) -> Result { + let has_generic_operand = [&left, &right].iter().any(|operand| { + matches!(operand, ExpressionType::Known(ty) if program.is_generic_placeholder(ty)) + }); + if has_generic_operand { + return if matches!( + operator, + Operator::GreaterEqual + | Operator::LessEqual + | Operator::Greater + | Operator::Less + | Operator::Equal + | Operator::NotEqual + | Operator::LogicalAnd + | Operator::LogicalOr + ) { + Ok(ExpressionType::Known(WaveType::Bool)) + } else { + Ok(ExpressionType::Unknown) + }; + } + let left_canonical = canonical_expression_type(program, &left); + let right_canonical = canonical_expression_type(program, &right); + let comparison = matches!( + operator, + Operator::GreaterEqual + | Operator::LessEqual + | Operator::Greater + | Operator::Less + | Operator::Equal + | Operator::NotEqual + ); + let logical = matches!(operator, Operator::LogicalAnd | Operator::LogicalOr); + let arithmetic = matches!( + operator, + Operator::Add + | Operator::Subtract + | Operator::Multiply + | Operator::Divide + | Operator::Remainder + ); + let integer_only = matches!( + operator, + Operator::ShiftLeft + | Operator::ShiftRight + | Operator::BitwiseAnd + | Operator::BitwiseOr + | Operator::BitwiseXor + ); + + let left_pointer = left_canonical.as_ref().is_some_and(is_pointer_like_type); + let right_pointer = right_canonical.as_ref().is_some_and(is_pointer_like_type); + let left_integer = left_canonical + .as_ref() + .is_some_and(|ty| integer_bit_width(ty).is_some()); + let right_integer = right_canonical + .as_ref() + .is_some_and(|ty| integer_bit_width(ty).is_some()); + let left_numeric = left_canonical.as_ref().is_some_and(is_numeric_type); + let right_numeric = right_canonical.as_ref().is_some_and(is_numeric_type); + + if let (ExpressionType::Known(left_known), ExpressionType::Known(right_known)) = (&left, &right) + { + if let (WaveType::Float(left_bits), WaveType::Float(right_bits)) = ( + program.canonical_type(left_known), + program.canonical_type(right_known), + ) { + if left_bits != right_bits { + return Err(format!( + "mixed float widths require an explicit cast: found `f{}` and `f{}`", + left_bits, right_bits + )); + } + } + } + + if matches!(operator, Operator::Equal | Operator::NotEqual) + && ((left_pointer && matches!(right, ExpressionType::Null)) + || (right_pointer && matches!(left, ExpressionType::Null))) + { + return Ok(ExpressionType::Known(WaveType::Bool)); + } + + if left_pointer || right_pointer { + let valid = match (left_pointer, right_pointer) { + (true, true) => matches!( + operator, + Operator::Equal | Operator::NotEqual | Operator::Subtract + ), + (true, false) if right_integer => matches!( + operator, + Operator::Add | Operator::Subtract | Operator::Equal | Operator::NotEqual + ), + (false, true) if left_integer => { + matches!( + operator, + Operator::Add | Operator::Equal | Operator::NotEqual + ) + } + _ => false, + }; + if !valid { + return Err(binary_type_error(operator, &left, &right)); + } + if comparison { + return Ok(ExpressionType::Known(WaveType::Bool)); + } + if left_pointer { + if right_pointer { + return Ok(ExpressionType::Known(WaveType::Int(64))); + } + return Ok(left); + } + return Ok(right); + } + + if logical { + if left_integer && right_integer { + return Ok(ExpressionType::Known(WaveType::Bool)); + } + return Err(binary_type_error(operator, &left, &right)); + } + + if integer_only { + if left_integer && right_integer { + return Ok(wider_integer_expression(program, left, right)); + } + return Err(binary_type_error(operator, &left, &right)); + } + + if arithmetic || comparison { + if !left_numeric || !right_numeric { + return Err(binary_type_error(operator, &left, &right)); + } + if comparison { + return Ok(ExpressionType::Known(WaveType::Bool)); + } + if matches!(left_canonical, Some(WaveType::Float(_))) { + return Ok(left); + } + if matches!(right_canonical, Some(WaveType::Float(_))) { + return Ok(right); + } + if matches!(left, ExpressionType::FloatLiteral) { + return Ok(left); + } + if matches!(right, ExpressionType::FloatLiteral) { + return Ok(right); + } + return Ok(wider_integer_expression(program, left, right)); + } + + Err(binary_type_error(operator, &left, &right)) +} + +fn canonical_expression_type(program: &ProgramTypes, ty: &ExpressionType) -> Option { + match ty { + ExpressionType::Known(ty) => Some(program.canonical_type(ty)), + ExpressionType::IntLiteral(_) => Some(WaveType::Int(32)), + ExpressionType::FloatLiteral => Some(WaveType::Float(32)), + _ => None, + } +} + +fn wider_integer_expression( + program: &ProgramTypes, + left: ExpressionType, + right: ExpressionType, +) -> ExpressionType { + let left_width = canonical_expression_type(program, &left) + .as_ref() + .and_then(integer_bit_width) + .unwrap_or(32); + let right_width = canonical_expression_type(program, &right) + .as_ref() + .and_then(integer_bit_width) + .unwrap_or(32); + if left_width >= right_width { + left + } else { + right + } +} + +fn binary_type_error(operator: &Operator, left: &ExpressionType, right: &ExpressionType) -> String { + format!( + "binary operator `{:?}` is not supported for `{}` and `{}`", + operator, + display_expression_type(left), + display_expression_type(right) + ) +} + +fn operator_source_symbol(operator: &Operator) -> Option<&'static str> { + match operator { + Operator::Add => Some("+"), + Operator::Subtract | Operator::Neg => Some("-"), + Operator::Multiply => Some("*"), + Operator::Divide => Some("/"), + Operator::Remainder => Some("%"), + Operator::GreaterEqual => Some(">="), + Operator::LessEqual => Some("<="), + Operator::Greater => Some(">"), + Operator::Less => Some("<"), + Operator::Equal => Some("=="), + Operator::NotEqual => Some("!="), + Operator::LogicalAnd => Some("&&"), + Operator::BitwiseAnd => Some("&"), + Operator::LogicalOr => Some("||"), + Operator::BitwiseOr => Some("|"), + Operator::ShiftLeft => Some("<<"), + Operator::ShiftRight => Some(">>"), + Operator::BitwiseXor => Some("^"), + Operator::LogicalNot | Operator::Not => Some("!"), + Operator::BitwiseNot => Some("~"), + Operator::Assign => Some("="), + } +} + +fn assign_operator_source_symbol(operator: &AssignOperator) -> &'static str { + match operator { + AssignOperator::Assign => "=", + AssignOperator::AddAssign => "+=", + AssignOperator::SubAssign => "-=", + AssignOperator::MulAssign => "*=", + AssignOperator::DivAssign => "/=", + AssignOperator::RemAssign => "%=", + } +} + +impl From for ExpressionType { + fn from(value: WaveType) -> Self { + Self::Known(value) + } +} + +fn find_base_var(target: &Expression, saw_deref: bool) -> Option<(String, bool)> { + match target { + Expression::Variable(name) => Some((name.clone(), saw_deref)), + Expression::Grouped(inner) => find_base_var(inner, saw_deref), + Expression::FieldAccess { object, .. } => find_base_var(object, saw_deref), + Expression::IndexAccess { target, .. } => find_base_var(target, saw_deref), + Expression::Deref(inner) => find_base_var(inner, true), + _ => None, + } +} + +fn is_lvalue_expression(expression: &Expression) -> bool { + match expression { + Expression::Variable(_) + | Expression::FieldAccess { .. } + | Expression::IndexAccess { .. } + | Expression::Deref(_) => true, + Expression::Grouped(inner) => is_lvalue_expression(inner), + _ => false, + } +} + +fn is_codegen_supported_index(expression: &Expression) -> bool { + match expression { + Expression::Literal(Literal::Int(_)) + | Expression::Variable(_) + | Expression::FieldAccess { .. } + | Expression::IndexAccess { .. } + | Expression::Deref(_) + | Expression::AddressOf(_) => true, + Expression::Grouped(inner) => is_codegen_supported_index(inner), + _ => false, + } +} + +fn expression_is_true(expression: &Expression) -> bool { + matches!(expression, Expression::Literal(Literal::Bool(true))) +} + +fn block_breaks_current_loop(nodes: &[ASTNode]) -> bool { + nodes.iter().any(node_breaks_current_loop) +} + +fn node_breaks_current_loop(node: &ASTNode) -> bool { + let ASTNode::Statement(statement) = node else { + return false; + }; + + match statement { + StatementNode::Break => true, + StatementNode::If { + body, + else_if_blocks, + else_block, + .. + } => { + block_breaks_current_loop(body) + || else_if_blocks.as_ref().is_some_and(|blocks| { + blocks + .iter() + .any(|(_, block)| block_breaks_current_loop(block)) + }) + || else_block + .as_ref() + .is_some_and(|block| block_breaks_current_loop(block)) + } + StatementNode::Match { arms, .. } => { + arms.iter().any(|arm| block_breaks_current_loop(&arm.body)) + } + StatementNode::While { .. } | StatementNode::For { .. } => false, + _ => false, + } +} + +fn int_literal_is_zero(raw: &str) -> bool { + let raw = raw.trim().replace('_', ""); + let raw = raw.strip_prefix('+').unwrap_or(&raw); + if let Some(hex) = raw.strip_prefix("0x").or_else(|| raw.strip_prefix("0X")) { + return u128::from_str_radix(hex, 16).ok() == Some(0); + } + if let Some(binary) = raw.strip_prefix("0b").or_else(|| raw.strip_prefix("0B")) { + return u128::from_str_radix(binary, 2).ok() == Some(0); + } + if let Some(octal) = raw.strip_prefix("0o").or_else(|| raw.strip_prefix("0O")) { + return u128::from_str_radix(octal, 8).ok() == Some(0); + } + raw.parse::().ok() == Some(0) +} + +fn integer_literal_fits(raw: &str, ty: &WaveType) -> bool { + let Some((negative, radix, digits)) = integer_literal_parts(raw) else { + return false; + }; + let Some(bit_len) = unsigned_literal_bit_len(radix, &digits) else { + return false; + }; + let is_zero = bit_len == 0; + + match ty { + WaveType::Int(bits) if *bits > 0 => { + let bits = usize::from(*bits); + if negative { + is_zero + || bit_len < bits + || (bit_len == bits && unsigned_is_power_of_two(radix, &digits)) + } else if radix == 10 { + bit_len < bits + } else { + // Non-decimal literals may spell the full-width bit pattern. + bit_len <= bits + } + } + WaveType::Uint(bits) if *bits > 0 => !negative && bit_len <= usize::from(*bits), + WaveType::Char | WaveType::Byte => !negative && bit_len <= 8, + _ => false, + } +} + +fn integer_literal_parts(raw: &str) -> Option<(bool, u32, String)> { + let raw = raw.trim().replace('_', ""); + let (negative, unsigned) = if let Some(value) = raw.strip_prefix('-') { + (true, value) + } else { + (false, raw.strip_prefix('+').unwrap_or(&raw)) + }; + let (radix, digits) = if let Some(value) = unsigned + .strip_prefix("0x") + .or_else(|| unsigned.strip_prefix("0X")) + { + (16, value) + } else if let Some(value) = unsigned + .strip_prefix("0b") + .or_else(|| unsigned.strip_prefix("0B")) + { + (2, value) + } else if let Some(value) = unsigned + .strip_prefix("0o") + .or_else(|| unsigned.strip_prefix("0O")) + { + (8, value) + } else { + (10, unsigned) + }; + if digits.is_empty() || !digits.chars().all(|ch| ch.is_digit(radix)) { + return None; + } + Some((negative, radix, digits.trim_start_matches('0').to_string())) +} + +fn unsigned_literal_bit_len(radix: u32, digits: &str) -> Option { + if digits.is_empty() { + return Some(0); + } + match radix { + 2 => Some(digits.len()), + 8 | 16 => { + let bits_per_digit = if radix == 8 { 3 } else { 4 }; + let first = digits.chars().next()?.to_digit(radix)?; + let first_bits = (u32::BITS - first.leading_zeros()) as usize; + Some((digits.len() - 1) * bits_per_digit + first_bits) + } + 10 => { + let mut decimal: Vec = digits.bytes().map(|byte| byte - b'0').collect(); + let mut bits = 0usize; + while decimal.iter().any(|digit| *digit != 0) { + let mut carry = 0u8; + for digit in &mut decimal { + let value = carry * 10 + *digit; + *digit = value / 2; + carry = value % 2; + } + bits = bits.checked_add(1)?; + } + Some(bits) + } + _ => None, + } +} + +fn unsigned_is_power_of_two(radix: u32, digits: &str) -> bool { + if digits.is_empty() { + return false; + } + if radix == 10 { + let mut decimal: Vec = digits.bytes().map(|byte| byte - b'0').collect(); + loop { + let first_nonzero = decimal.iter().position(|digit| *digit != 0); + let Some(first_nonzero) = first_nonzero else { + return false; + }; + if decimal[first_nonzero..] == [1] { + return true; + } + if decimal.last().is_none_or(|digit| digit % 2 != 0) { + return false; + } + let mut carry = 0u8; + for digit in &mut decimal { + let value = carry * 10 + *digit; + *digit = value / 2; + carry = value % 2; + } + } + } + let mut seen_one = false; + for ch in digits.chars() { + let Some(mut value) = ch.to_digit(radix) else { + return false; + }; + while value != 0 { + if value & 1 == 1 { + if seen_one { + return false; + } + seen_one = true; + } + value >>= 1; + } + } + seen_one +} + +fn is_integer_type(ty: &WaveType) -> bool { + matches!( + ty, + WaveType::Int(_) | WaveType::Uint(_) | WaveType::Char | WaveType::Byte + ) +} + +fn integer_bit_width(ty: &WaveType) -> Option { + match ty { + WaveType::Int(bits) | WaveType::Uint(bits) => Some(*bits), + WaveType::Bool => Some(1), + WaveType::Char | WaveType::Byte => Some(8), + _ => None, + } +} + +fn is_numeric_type(ty: &WaveType) -> bool { + is_integer_type(ty) || matches!(ty, WaveType::Float(_)) +} + +fn is_pointer_like_type(ty: &WaveType) -> bool { + matches!(ty, WaveType::Pointer(_) | WaveType::String) +} + +fn is_byte_like_type(ty: &WaveType) -> bool { + matches!( + ty, + WaveType::Int(8) | WaveType::Uint(8) | WaveType::Char | WaveType::Byte + ) +} + +fn display_expression_type(ty: &ExpressionType) -> String { + match ty { + ExpressionType::Known(ty) => display_wave_type(ty), + ExpressionType::IntLiteral(_) => "integer literal".to_string(), + ExpressionType::FloatLiteral => "float literal".to_string(), + ExpressionType::Null => "null".to_string(), + ExpressionType::ArrayLiteral(_) => "array literal".to_string(), + ExpressionType::AddressedArrayLiteral(_) => "addressed array literal".to_string(), + ExpressionType::Unknown => "unknown".to_string(), + } +} + +fn display_wave_type(ty: &WaveType) -> String { + match ty { + WaveType::Int(bits) => format!("i{}", bits), + WaveType::Uint(bits) => format!("u{}", bits), + WaveType::Float(bits) => format!("f{}", bits), + WaveType::Bool => "bool".to_string(), + WaveType::Char => "char".to_string(), + WaveType::Byte => "byte".to_string(), + WaveType::String => "str".to_string(), + WaveType::Pointer(inner) => format!("ptr<{}>", display_wave_type(inner)), + WaveType::Array(inner, size) => format!("array<{}, {}>", display_wave_type(inner), size), + WaveType::Void => "void".to_string(), + WaveType::Struct(name) => name.clone(), + } +} + +pub fn validate_program(nodes: &Vec) -> Result<(), String> { + validate_program_detailed(nodes).map_err(|diagnostic| diagnostic.message) +} + +pub fn validate_program_detailed(nodes: &[ASTNode]) -> Result<(), SemanticDiagnostic> { + let program = ProgramTypes::collect(nodes).map_err(|(index, message, primary)| { + semantic_diagnostic_for_top_level(nodes, index, message, primary) + })?; + validate_declaration_types(nodes, &program)?; + let mut validator = Validator::new(&program); + + for (index, node) in nodes.iter().enumerate() { + validator.begin_top_level(index, top_level_span_hint(node)); + let result = match node { + ASTNode::Function(function) => { + if let Some(export) = &function.export { + if !export.abi.eq_ignore_ascii_case("c") { + validator.mark_span(SemanticSpanKind::Keyword, "export"); + Err(format!( + "unsupported export ABI '{}' for function '{}': only export(c) is currently supported", + export.abi, function.name + )) + } else { + validator.validate_function(function, &function.name, &[]) + } + } else { + validator.validate_function(function, &function.name, &[]) + } + } + ASTNode::ProtoImpl(implementation) => { + let mut result = Ok(()); + for method in &implementation.methods { + validator.mark_span(SemanticSpanKind::Declaration, method.name.clone()); + result = validator.validate_function( + method, + &format!("{}.{}", implementation.target, method.name), + &[], + ); + if result.is_err() { + break; + } + } + result + } + ASTNode::Struct(structure) => { + let mut result = Ok(()); + for method in &structure.methods { + validator.mark_span(SemanticSpanKind::Declaration, method.name.clone()); + result = validator.validate_function( + method, + &format!("{}.{}", structure.name, method.name), + &structure.generic_params, + ); + if result.is_err() { + break; + } + } + result + } + ASTNode::ExternFunction(function) => { + if !function.abi.eq_ignore_ascii_case("c") { + validator.mark_span(SemanticSpanKind::Keyword, "extern"); + Err(format!( + "unsupported extern ABI '{}' for function '{}': only extern(c) is currently supported", + function.abi, function.name + )) + } else { + Ok(()) + } + } + ASTNode::Variable(_) | ASTNode::Statement(_) | ASTNode::Expression(_) => { + validator.validate_node(node).map(|_| ()) + } + _ => Ok(()), + }; + if let Err(message) = result { + return Err(validator.diagnostic(message)); + } + } + + Ok(()) +} + +fn top_level_span_hint(node: &ASTNode) -> SemanticSpanHint { + let (kind, text) = match node { + ASTNode::Function(function) => (SemanticSpanKind::Declaration, function.name.clone()), + ASTNode::ExternFunction(function) => (SemanticSpanKind::Declaration, function.name.clone()), + ASTNode::Struct(structure) => (SemanticSpanKind::Declaration, structure.name.clone()), + ASTNode::ProtoImpl(implementation) => { + (SemanticSpanKind::Declaration, implementation.target.clone()) + } + ASTNode::TypeAlias(alias) => (SemanticSpanKind::Declaration, alias.name.clone()), + ASTNode::Enum(enumeration) => (SemanticSpanKind::Declaration, enumeration.name.clone()), + ASTNode::Variable(variable) => (SemanticSpanKind::Declaration, variable.name.clone()), + ASTNode::Statement(_) | ASTNode::Expression(_) | ASTNode::Program(_) => { + (SemanticSpanKind::Keyword, "program".to_string()) + } + }; + SemanticSpanHint { + kind, + text, + occurrence: 1, + } +} + +fn semantic_diagnostic_for_top_level( + nodes: &[ASTNode], + index: usize, + message: String, + primary: Option, +) -> SemanticDiagnostic { + let primary = primary.or_else(|| nodes.get(index).map(top_level_span_hint)); + SemanticDiagnostic { + code: "E3001".to_string(), + label: message.clone(), + message, + top_level_index: index, + primary, + note: None, + help: "fix type, mutability, scope, and control-flow errors".to_string(), + } +} + +fn validate_declaration_types( + nodes: &[ASTNode], + program: &ProgramTypes, +) -> Result<(), SemanticDiagnostic> { + let no_generics = HashSet::new(); + let mut checked_aliases = HashSet::new(); + + for (index, node) in nodes.iter().enumerate() { + let result = match node { + ASTNode::Function(function) => { + validate_unique_generic_params(&function.generic_params, &function.name) + } + ASTNode::Struct(structure) => { + let mut result = + validate_unique_generic_params(&structure.generic_params, &structure.name); + if result.is_ok() { + let generics: HashSet = + structure.generic_params.iter().cloned().collect(); + for (field, ty) in &structure.fields { + result = program.validate_type( + ty, + &generics, + false, + &format!("field `{}.{}`", structure.name, field), + ); + if result.is_err() { + break; + } + } + } + result + } + ASTNode::ProtoImpl(implementation) => { + if !program.is_known_named_type(&implementation.target) { + Err(format!( + "proto implementation targets unknown type `{}`", + implementation.target + )) + } else { + Ok(()) + } + } + ASTNode::TypeAlias(alias) => program + .validate_type( + &alias.target, + &no_generics, + false, + &format!("type alias `{}`", alias.name), + ) + .and_then(|_| { + validate_alias_cycle( + &alias.name, + program, + &mut HashSet::new(), + &mut checked_aliases, + ) + }), + ASTNode::Enum(enumeration) => { + let result = program.validate_type( + &enumeration.repr_type, + &no_generics, + false, + &format!("representation of enum `{}`", enumeration.name), + ); + if result.is_ok() + && !is_integer_type(&program.canonical_type(&enumeration.repr_type)) + { + Err(format!( + "enum `{}` representation must be an integer type, found `{}`", + enumeration.name, + display_wave_type(&enumeration.repr_type) + )) + } else { + result + } + } + ASTNode::ExternFunction(function) => { + let mut params = HashSet::new(); + let mut result = Ok(()); + for (name, ty) in &function.params { + if !params.insert(name) { + result = Err(format!( + "duplicate parameter declaration `{}` in extern function `{}`", + name, function.name + )); + break; + } + result = program.validate_type( + ty, + &no_generics, + false, + &format!( + "parameter `{}` of extern function `{}`", + name, function.name + ), + ); + if result.is_err() { + break; + } + } + if result.is_ok() { + result = program.validate_type( + &function.return_type, + &no_generics, + true, + &format!("return type of extern function `{}`", function.name), + ); + } + result + } + ASTNode::Variable(variable) => program.validate_type( + &variable.type_name, + &no_generics, + false, + &format!("global variable `{}`", variable.name), + ), + _ => Ok(()), + }; + if let Err(message) = result { + return Err(semantic_diagnostic_for_top_level( + nodes, index, message, None, + )); + } + } + + Ok(()) +} + +fn validate_alias_cycle( + alias: &str, + program: &ProgramTypes, + active: &mut HashSet, + checked: &mut HashSet, +) -> Result<(), String> { + if checked.contains(alias) { + return Ok(()); + } + if !active.insert(alias.to_string()) { + return Err(format!("cyclic type alias involving `{}`", alias)); + } + if let Some(target) = program.aliases.get(alias) { + validate_alias_type_cycle(target, program, active, checked)?; + } + active.remove(alias); + checked.insert(alias.to_string()); + Ok(()) +} + +fn validate_alias_type_cycle( + ty: &WaveType, + program: &ProgramTypes, + active: &mut HashSet, + checked: &mut HashSet, +) -> Result<(), String> { + match ty { + WaveType::Struct(name) if program.aliases.contains_key(name) => { + validate_alias_cycle(name, program, active, checked) + } + WaveType::Pointer(inner) | WaveType::Array(inner, _) => { + validate_alias_type_cycle(inner, program, active, checked) + } + _ => Ok(()), + } +} + +fn validate_unique_generic_params(params: &[String], owner: &str) -> Result<(), String> { + let mut seen = HashSet::new(); + for param in params { + if !seen.insert(param) { + return Err(format!( + "duplicate generic parameter `{}` in `{}`", + param, owner + )); + } + } Ok(()) } diff --git a/llvm/src/codegen/ir.rs b/llvm/src/codegen/ir.rs index db640d01..ea0552b7 100644 --- a/llvm/src/codegen/ir.rs +++ b/llvm/src/codegen/ir.rs @@ -568,6 +568,12 @@ fn build_module( } for stmt in &func_node.body { + if builder + .get_insert_block() + .is_some_and(|block| block.get_terminator().is_some()) + { + break; + } if let ASTNode::Statement(_) | ASTNode::Variable(_) = stmt { generate_statement_ir( context, diff --git a/llvm/src/statement/control.rs b/llvm/src/statement/control.rs index 4dab10ba..48149236 100644 --- a/llvm/src/statement/control.rs +++ b/llvm/src/statement/control.rs @@ -21,7 +21,7 @@ use inkwell::types::StringRadix; use inkwell::types::StructType; use inkwell::values::{AnyValue, BasicValueEnum, FunctionValue}; use inkwell::{FloatPredicate, IntPredicate}; -use parser::ast::{ASTNode, Expression, MatchArm, MatchPattern, WaveType}; +use parser::ast::{ASTNode, Expression, Literal, MatchArm, MatchPattern, StatementNode, WaveType}; use std::collections::{HashMap, HashSet}; fn truthy_to_i1<'ctx>( @@ -52,6 +52,45 @@ fn truthy_to_i1<'ctx>( } } +fn expression_is_true(expression: &Expression) -> bool { + matches!(expression, Expression::Literal(Literal::Bool(true))) +} + +fn block_breaks_current_loop(nodes: &[ASTNode]) -> bool { + nodes.iter().any(node_breaks_current_loop) +} + +fn node_breaks_current_loop(node: &ASTNode) -> bool { + let ASTNode::Statement(statement) = node else { + return false; + }; + + match statement { + StatementNode::Break => true, + StatementNode::If { + body, + else_if_blocks, + else_block, + .. + } => { + block_breaks_current_loop(body) + || else_if_blocks.as_ref().is_some_and(|blocks| { + blocks + .iter() + .any(|(_, block)| block_breaks_current_loop(block)) + }) + || else_block + .as_ref() + .is_some_and(|block| block_breaks_current_loop(block)) + } + StatementNode::Match { arms, .. } => { + arms.iter().any(|arm| block_breaks_current_loop(&arm.body)) + } + StatementNode::While { .. } | StatementNode::For { .. } => false, + _ => false, + } +} + fn parse_signed_decimal<'a>(s: &'a str) -> (bool, &'a str) { if let Some(rest) = s.strip_prefix('-') { (true, rest) @@ -165,6 +204,7 @@ pub(super) fn gen_if_ir<'ctx>( let then_block = context.append_basic_block(current_fn, "then"); let else_block_bb = context.append_basic_block(current_fn, "else"); let merge_block = context.append_basic_block(current_fn, "merge"); + let mut merge_reachable = false; builder .build_conditional_branch(cond_i1, then_block, else_block_bb) @@ -194,6 +234,7 @@ pub(super) fn gen_if_ir<'ctx>( let then_end = builder.get_insert_block().unwrap(); if then_end.get_terminator().is_none() { builder.build_unconditional_branch(merge_block).unwrap(); + merge_reachable = true; } builder.position_at_end(else_block_bb); @@ -249,6 +290,7 @@ pub(super) fn gen_if_ir<'ctx>( let end_bb = builder.get_insert_block().unwrap(); if end_bb.get_terminator().is_none() { builder.build_unconditional_branch(merge_block).unwrap(); + merge_reachable = true; } current_check_bb = next_check_bb; @@ -281,9 +323,13 @@ pub(super) fn gen_if_ir<'ctx>( let else_end = builder.get_insert_block().unwrap(); if else_end.get_terminator().is_none() { builder.build_unconditional_branch(merge_block).unwrap(); + merge_reachable = true; } builder.position_at_end(merge_block); + if !merge_reachable { + builder.build_unreachable().unwrap(); + } return; } @@ -314,9 +360,13 @@ pub(super) fn gen_if_ir<'ctx>( let else_end = builder.get_insert_block().unwrap(); if else_end.get_terminator().is_none() { builder.build_unconditional_branch(merge_block).unwrap(); + merge_reachable = true; } builder.position_at_end(merge_block); + if !merge_reachable { + builder.build_unreachable().unwrap(); + } } pub(super) fn gen_while_ir<'ctx>( @@ -398,6 +448,9 @@ pub(super) fn gen_while_ir<'ctx>( loop_continue_stack.pop(); builder.position_at_end(merge_block); + if expression_is_true(condition) && !block_breaks_current_loop(body) { + builder.build_unreachable().unwrap(); + } } pub(super) fn gen_match_ir<'ctx>( @@ -665,6 +718,9 @@ pub(super) fn gen_for_ir<'ctx>( loop_continue_stack.pop(); builder.position_at_end(merge_block); + if expression_is_true(condition) && !block_breaks_current_loop(body) { + builder.build_unreachable().unwrap(); + } *variables = outer_scope_variables; } @@ -748,7 +804,7 @@ pub(super) fn gen_return_ir<'ctx>( v, ret_ty, "ret_cast", - CoercionMode::Explicit, + CoercionMode::Implicit, ); } diff --git a/llvm/src/statement/mod.rs b/llvm/src/statement/mod.rs index 0565c499..1e8a76ae 100644 --- a/llvm/src/statement/mod.rs +++ b/llvm/src/statement/mod.rs @@ -45,6 +45,13 @@ pub fn generate_statement_ir<'ctx>( target_data: &'ctx TargetData, extern_c_info: &HashMap>, ) { + if builder + .get_insert_block() + .is_some_and(|block| block.get_terminator().is_some()) + { + return; + } + match stmt { ASTNode::Variable(var_node) => { variable::gen_variable_ir( diff --git a/llvm/src/statement/variable.rs b/llvm/src/statement/variable.rs index fa0fa0e1..ac40268c 100644 --- a/llvm/src/statement/variable.rs +++ b/llvm/src/statement/variable.rs @@ -72,6 +72,12 @@ pub fn coerce_basic_value<'ctx>( } } + // float <-> float + (BasicValueEnum::FloatValue(fv), BasicTypeEnum::FloatType(dst)) => builder + .build_float_cast(fv, dst, tag) + .unwrap() + .as_basic_value_enum(), + // float -> int (BasicValueEnum::FloatValue(fv), BasicTypeEnum::IntType(dst)) => builder .build_float_to_signed_int(fv, dst, tag) diff --git a/src/runner.rs b/src/runner.rs index 0c6ac5bc..000ee769 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -15,7 +15,7 @@ use ::error::*; use ::parser::ast::*; use ::parser::generics::monomorphize_generics; use ::parser::import::*; -use ::parser::verification::validate_program; +use ::parser::verification::{validate_program_detailed, SemanticSpanHint, SemanticSpanKind}; use ::parser::*; use lexer::Lexer; use llvm::backend::*; @@ -146,25 +146,240 @@ fn parse_wave_tokens_or_exit( }) } -fn validate_wave_ast_or_exit(file_path: &Path, source: &str, ast: &Vec) { - if let Err(msg) = validate_program(ast) { - WaveError::new( - WaveErrorKind::InvalidStatement(msg.clone()), - format!("semantic validation failed: {}", msg), +fn validate_wave_ast_or_exit(file_path: &Path, source: &str, ast: &[ASTNode]) { + if let Err(diagnostic) = validate_program_detailed(ast) { + let node = ast.get(diagnostic.top_level_index); + let (line, column, span_len) = diagnostic + .primary + .as_ref() + .and_then(|hint| semantic_hint_position(source, node, 1, hint)) + .unwrap_or((1, 1, 1)); + let mut error = WaveError::new( + WaveErrorKind::InvalidStatement(diagnostic.message.clone()), + format!("semantic validation failed: {}", diagnostic.message), file_path.display().to_string(), - 1, - 1, + line, + column, ) - .with_code("E3001") + .with_code(diagnostic.code) .with_source_code(source.to_string()) + .with_span_len(span_len) .with_context("semantic validation") - .with_help("fix mutability, scope, and expression validity issues") - .display_auto(); + .with_label(diagnostic.label) + .with_help(diagnostic.help); + if let Some(note) = diagnostic.note { + error = error.with_note(note); + } + error.display_auto(); process::exit(1); } } +fn validate_expanded_ast_or_exit(expanded: &ExpandedWaveAst) { + let Err(diagnostic) = validate_program_detailed(&expanded.ast) else { + return; + }; + let origin = expanded + .origins + .get(diagnostic.top_level_index) + .copied() + .unwrap_or(0); + let source_unit = expanded.sources.get(origin).unwrap_or(&expanded.sources[0]); + let node = expanded.ast.get(diagnostic.top_level_index); + let scope_occurrence = node.map_or(1, |target| { + let key = semantic_node_key(target); + 1 + expanded.ast[..diagnostic.top_level_index] + .iter() + .zip(&expanded.origins[..diagnostic.top_level_index]) + .filter(|(candidate, candidate_origin)| { + **candidate_origin == origin && semantic_node_key(candidate) == key + }) + .count() + }); + let (line, column, span_len) = diagnostic + .primary + .as_ref() + .and_then(|hint| semantic_hint_position(&source_unit.source, node, scope_occurrence, hint)) + .unwrap_or((1, 1, 1)); + + let mut error = WaveError::new( + WaveErrorKind::InvalidStatement(diagnostic.message.clone()), + format!("semantic validation failed: {}", diagnostic.message), + source_unit.path.display().to_string(), + line, + column, + ) + .with_code(diagnostic.code) + .with_source_code(source_unit.source.clone()) + .with_span_len(span_len) + .with_context("semantic validation") + .with_label(diagnostic.label) + .with_help(diagnostic.help); + if let Some(note) = diagnostic.note { + error = error.with_note(note); + } + error.display_auto(); + + process::exit(1); +} + +fn semantic_hint_position( + source: &str, + node: Option<&ASTNode>, + scope_occurrence: usize, + hint: &SemanticSpanHint, +) -> Option<(usize, usize, usize)> { + let (scope_start, scope_end) = + semantic_node_scope(source, node, scope_occurrence).unwrap_or((0, source.len())); + let scope = &source[scope_start..scope_end]; + let alternatives: Vec<&str> = hint.text.split('|').collect(); + let mut matches = Vec::new(); + + for alternative in alternatives { + if alternative.is_empty() { + continue; + } + let mut offset = 0usize; + while let Some(relative) = scope[offset..].find(alternative) { + let found = offset + relative; + let absolute = scope_start + found; + let boundary_ok = if alternative + .chars() + .all(|ch| ch.is_ascii_alphanumeric() || ch == '_') + { + identifier_boundary(source, absolute, alternative.len()) + } else { + true + }; + let declaration_ok = !matches!(hint.kind, SemanticSpanKind::Declaration) + || is_declaration_occurrence(source, absolute, alternative); + if boundary_ok && declaration_ok { + matches.push((absolute, alternative.len())); + } + offset = found + alternative.len(); + } + } + + matches.sort_unstable(); + matches.dedup(); + let (offset, span_len) = *matches.get(hint.occurrence.saturating_sub(1))?; + let (line, column) = source_position(source, offset); + Some((line, column, span_len.max(1))) +} + +fn identifier_boundary(source: &str, offset: usize, len: usize) -> bool { + let is_identifier = |byte: u8| byte.is_ascii_alphanumeric() || byte == b'_'; + let before_ok = offset == 0 || !is_identifier(source.as_bytes()[offset - 1]); + let after = offset + len; + let after_ok = after >= source.len() || !is_identifier(source.as_bytes()[after]); + before_ok && after_ok +} + +fn is_declaration_occurrence(source: &str, offset: usize, name: &str) -> bool { + let line_start = source[..offset].rfind('\n').map_or(0, |index| index + 1); + let prefix = source[line_start..offset].trim_start(); + [ + "fun ", "struct ", "proto ", "enum ", "type ", "let ", "var ", "const ", "static ", + ] + .iter() + .any(|keyword| prefix.ends_with(keyword)) + || source[offset + name.len()..].trim_start().starts_with(':') +} + +fn semantic_node_key(node: &ASTNode) -> (u8, String) { + match node { + ASTNode::Function(function) => (0, function.name.clone()), + ASTNode::ExternFunction(function) => (0, function.name.clone()), + ASTNode::Struct(structure) => (1, structure.name.clone()), + ASTNode::ProtoImpl(implementation) => (2, implementation.target.clone()), + ASTNode::TypeAlias(alias) => (3, alias.name.clone()), + ASTNode::Enum(enumeration) => (4, enumeration.name.clone()), + ASTNode::Variable(variable) => (5, variable.name.clone()), + ASTNode::Statement(_) => (6, String::new()), + ASTNode::Expression(_) => (7, String::new()), + ASTNode::Program(_) => (8, String::new()), + } +} + +fn semantic_node_scope( + source: &str, + node: Option<&ASTNode>, + occurrence: usize, +) -> Option<(usize, usize)> { + let node = node?; + let needle = match node { + ASTNode::Function(function) => format!("fun {}(", function.name), + ASTNode::ExternFunction(function) => format!("fun {}(", function.name), + ASTNode::Struct(structure) => format!("struct {}", structure.name), + ASTNode::ProtoImpl(implementation) => format!("proto {}", implementation.target), + ASTNode::TypeAlias(alias) => format!("type {}", alias.name), + ASTNode::Enum(enumeration) => format!("enum {}", enumeration.name), + ASTNode::Variable(variable) => variable.name.clone(), + ASTNode::Statement(_) | ASTNode::Expression(_) | ASTNode::Program(_) => { + return Some((0, source.len())); + } + }; + let mut starts = source.match_indices(&needle); + let start = starts.nth(occurrence.saturating_sub(1))?.0; + let Some(open_relative) = source[start..].find('{') else { + let end = source[start..] + .find('\n') + .map_or(source.len(), |relative| start + relative); + return Some((start, end)); + }; + let open = start + open_relative; + let end = matching_source_brace(source, open).unwrap_or(source.len()); + Some((start, end)) +} + +fn matching_source_brace(source: &str, open: usize) -> Option { + let bytes = source.as_bytes(); + let mut depth = 0usize; + let mut index = open; + let mut quote = None; + let mut escaped = false; + while index < bytes.len() { + let byte = bytes[index]; + if let Some(active_quote) = quote { + if escaped { + escaped = false; + } else if byte == b'\\' { + escaped = true; + } else if byte == active_quote { + quote = None; + } + index += 1; + continue; + } + if byte == b'"' || byte == b'\'' { + quote = Some(byte); + } else if byte == b'/' && bytes.get(index + 1) == Some(&b'/') { + index = source[index..] + .find('\n') + .map_or(bytes.len(), |relative| index + relative); + continue; + } else if byte == b'{' { + depth += 1; + } else if byte == b'}' { + depth = depth.checked_sub(1)?; + if depth == 0 { + return Some(index + 1); + } + } + index += 1; + } + None +} + +fn source_position(source: &str, byte_offset: usize) -> (usize, usize) { + let prefix = &source[..byte_offset]; + let line = prefix.bytes().filter(|byte| *byte == b'\n').count() + 1; + let line_start = prefix.rfind('\n').map_or(0, |index| index + 1); + let column = source[line_start..byte_offset].chars().count() + 1; + (line, column) +} + fn panic_payload_to_string(payload: &(dyn std::any::Any + Send)) -> String { if let Some(s) = payload.downcast_ref::() { return s.clone(); @@ -594,19 +809,33 @@ fn build_import_config(dep: &DepFlags, target: TargetConditionContext) -> Import config } +struct SemanticSourceUnit { + path: PathBuf, + source: String, +} + +struct ExpandedWaveAst { + ast: Vec, + origins: Vec, + sources: Vec, +} + fn expand_imports_for_codegen( entry_path: &Path, + entry_source: &str, ast: Vec, import_config: &ImportConfig, -) -> Result, WaveError> { +) -> Result { fn expand_from_dir( base_dir: &Path, ast: Vec, + origin: usize, + out: &mut Vec, + origins: &mut Vec, + sources: &mut Vec, already: &mut HashSet, import_config: &ImportConfig, - ) -> Result, WaveError> { - let mut out = Vec::new(); - + ) -> Result<(), WaveError> { for node in ast { match node { ASTNode::Statement(StatementNode::Import(module)) => { @@ -618,16 +847,31 @@ fn expand_imports_for_codegen( } let next_dir = unit.abs_path.parent().unwrap_or(base_dir); - - let expanded = expand_from_dir(next_dir, unit.ast, already, import_config)?; - out.extend(expanded); + let imported_origin = sources.len(); + sources.push(SemanticSourceUnit { + path: unit.abs_path.clone(), + source: unit.source, + }); + expand_from_dir( + next_dir, + unit.ast, + imported_origin, + out, + origins, + sources, + already, + import_config, + )?; } - other => out.push(other), + other => { + out.push(other); + origins.push(origin); + } } } - Ok(out) + Ok(()) } let mut already = HashSet::new(); @@ -639,7 +883,27 @@ fn expand_imports_for_codegen( } let base_dir = entry_path.parent().unwrap_or(Path::new(".")); - expand_from_dir(base_dir, ast, &mut already, import_config) + let mut out = Vec::new(); + let mut origins = Vec::new(); + let mut sources = vec![SemanticSourceUnit { + path: entry_path.to_path_buf(), + source: entry_source.to_string(), + }]; + expand_from_dir( + base_dir, + ast, + 0, + &mut out, + &mut origins, + &mut sources, + &mut already, + import_config, + )?; + Ok(ExpandedWaveAst { + ast: out, + origins, + sources, + }) } #[allow(dead_code)] @@ -757,14 +1021,15 @@ fn frontend_prepare_wave_ast( } let import_config = build_import_config(dep, target); - let ast = match expand_imports_for_codegen(file_path, parsed_ast, &import_config) { + let expanded = match expand_imports_for_codegen(file_path, &code, parsed_ast, &import_config) { Ok(a) => a, Err(e) => { e.display_auto(); process::exit(1); } }; - let ast = match monomorphize_generics(ast) { + validate_expanded_ast_or_exit(&expanded); + let ast = match monomorphize_generics(expanded.ast) { Ok(a) => a, Err(msg) => { WaveError::new( @@ -986,14 +1251,15 @@ pub(crate) unsafe fn run_wave_file( let import_config = build_import_config(dep, target); - let ast = match expand_imports_for_codegen(file_path, ast, &import_config) { + let expanded = match expand_imports_for_codegen(file_path, &code, ast, &import_config) { Ok(a) => a, Err(e) => { e.display_auto(); process::exit(1); } }; - let ast = match monomorphize_generics(ast) { + validate_expanded_ast_or_exit(&expanded); + let ast = match monomorphize_generics(expanded.ast) { Ok(a) => a, Err(msg) => { WaveError::new( @@ -1119,11 +1385,13 @@ pub(crate) unsafe fn object_build_wave_file( let import_config = build_import_config(dep, target); - let ast = expand_imports_for_codegen(file_path, ast, &import_config).unwrap_or_else(|e| { - e.display_auto(); - process::exit(1); - }); - let ast = monomorphize_generics(ast).unwrap_or_else(|msg| { + let expanded = expand_imports_for_codegen(file_path, &code, ast, &import_config) + .unwrap_or_else(|e| { + e.display_auto(); + process::exit(1); + }); + validate_expanded_ast_or_exit(&expanded); + let ast = monomorphize_generics(expanded.ast).unwrap_or_else(|msg| { WaveError::new( WaveErrorKind::InvalidStatement(msg.clone()), format!("generic monomorphization failed: {}", msg), diff --git a/test/test56.wave b/test/test56.wave index 3db8f0ae..041ec605 100644 --- a/test/test56.wave +++ b/test/test56.wave @@ -108,7 +108,7 @@ fun syscall5(id: i64, a1: i64, a2: i64, a3: i64, p4: ptr, a5: i64) -> i64 { fun htons(x: i16) -> i16 { var a: i32 = x; var y: i32 = ((a & 255) << 8) | ((a >> 8) & 255); - return y; + return y as i16; } fun _setsockopt_reuseaddr(sockfd: i64) { diff --git a/test/test69.wave b/test/test69.wave index ff58f9d5..a462103f 100644 --- a/test/test69.wave +++ b/test/test69.wave @@ -9,6 +9,6 @@ fun main() { println("before overflow: {}", max); - var overflow: i32 = max + 1; + var overflow: i32 = (max + 1) as i32; println("after overflow: {}", overflow); } diff --git a/test/test71.wave b/test/test71.wave index 3f3e119a..ff419828 100644 --- a/test/test71.wave +++ b/test/test71.wave @@ -8,5 +8,5 @@ fun main() { var g: char = 'A'; var h: ptr = &a; var i: array = [1, 2, 3, 4, 5]; - println("{}\n {}\n {}\n {}\n {}\n {}\n {}\n {}\n {}", a, b, c, d, e, f, g, h, i); -} \ No newline at end of file + println("{}\n {}\n {}\n {}\n {}\n {}\n {}\n {}\n {}", a, b, c, d, e, f, g, h, i[0]); +} diff --git a/tests/codegen_regressions.rs b/tests/codegen_regressions.rs index c1db014a..3bbeff10 100644 --- a/tests/codegen_regressions.rs +++ b/tests/codegen_regressions.rs @@ -126,6 +126,691 @@ fn run_link_tests_enabled() -> bool { std::env::var_os("WAVE_RUN_LINK_TESTS").is_some() } +#[test] +fn semantic_validation_rejects_invalid_returns_calls_and_loop_control() { + let dir = temp_case_dir("semantic-validation"); + let cases = [ + ( + "wrong_return_type.wave", + r#" +fun value() -> i32 { + return "not an integer"; +} + +"#, + "type mismatch in return value of function `value`", + ), + ( + "value_from_void.wave", + r#" +fun noop() { + return 1; +} +"#, + "void function `noop` cannot return a value", + ), + ( + "empty_non_void_return.wave", + r#" +fun value() -> i32 { + return; +} +"#, + "non-void function `value` must return `i32`", + ), + ( + "missing_return_path.wave", + r#" +fun value(flag: bool) -> i32 { + if (flag) { + return 1; + } +} +"#, + "non-void function `value` may exit without returning `i32`", + ), + ( + "break_outside_loop.wave", + r#" +fun main() { + break; +} +"#, + "`break` can only be used inside a loop", + ), + ( + "continue_outside_loop.wave", + r#" +fun main() { + continue; +} +"#, + "`continue` can only be used inside a loop", + ), + ( + "wrong_call_type.wave", + r#" +fun identity(value: i32) -> i32 { + return value; +} + +fun main() { + identity("text"); +} +"#, + "type mismatch in argument 1 of function `identity`", + ), + ]; + + for (file_name, source, expected) in cases { + let source = write_wave(&dir, file_name, source); + for mode in ["check", "build"] { + let output = if mode == "check" { + run_wavec_raw([OsStr::new("check"), source.as_os_str()]) + } else { + run_wavec_raw([ + OsStr::new("build"), + source.as_os_str(), + OsStr::new("--emit=obj"), + OsStr::new("--out-dir"), + dir.as_os_str(), + ]) + }; + assert!( + !output.status.success(), + "{} unexpectedly passed semantic validation in {} mode", + file_name, + mode + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!( + stderr.contains("error[E3001]"), + "{} ({}): {}", + file_name, + mode, + stderr + ); + assert!( + stderr.contains(expected), + "{} ({}): {}", + file_name, + mode, + stderr + ); + assert!( + !stderr.contains("E9001") && !stderr.contains("compiler internal error"), + "{} ({}) leaked a backend failure: {}", + file_name, + mode, + stderr + ); + } + } +} + +#[test] +fn semantic_validation_rejects_backend_only_type_failures_early() { + let dir = temp_case_dir("semantic-backend-parity"); + let cases = [ + ( + "narrow_return.wave", + "fun narrow(x: i64) -> i32 { return x; }\nfun main() {}\n", + "expected `i32`, found `i64`", + ), + ( + "narrow_call.wave", + "fun take(x: i32) {}\nfun main() { let wide: i64 = 1; take(wide); }\n", + "argument 1 of function `take`", + ), + ( + "narrow_initializer.wave", + "fun main() { let wide: i64 = 1; let narrow: i32 = wide; }\n", + "initializer for `narrow`", + ), + ( + "narrow_assignment.wave", + "fun main() { let wide: i64 = 1; var narrow: i32 = 0; narrow = wide; }\n", + "assignment to `narrow`", + ), + ( + "unknown_function.wave", + "fun main() { missing_function(); }\n", + "call to unknown function `missing_function`", + ), + ( + "unknown_field.wave", + "struct Point { x: i32; }\nfun read(p: Point) -> i32 { return p.missing; }\nfun main() {}\n", + "struct `Point` has no field `missing`", + ), + ( + "unknown_method.wave", + "struct Point { x: i32; }\nfun main() { let p: Point = Point { x: 1 }; p.missing(); }\n", + "struct `Point` has no method `missing`", + ), + ( + "unknown_struct_literal_field.wave", + "struct Point { x: i32; }\nfun make() -> Point { return Point { missing: 1 }; }\nfun main() {}\n", + "struct `Point` has no field `missing`", + ), + ( + "missing_struct_literal_field.wave", + "struct Point { x: i32; y: i32; }\nfun main() { let p: Point = Point { x: 1 }; }\n", + "struct literal `Point` is missing field(s): y", + ), + ( + "array_return.wave", + "fun values() -> i32 { return [1, 2]; }\nfun main() {}\n", + "found `array literal`", + ), + ( + "array_element.wave", + "fun main() { let values: array = [\"text\"]; }\n", + "element 0 of initializer for `values`", + ), + ( + "invalid_condition.wave", + "struct Flag { value: i32; }\nfun main() { let flag: Flag = Flag { value: 1 }; if (flag) {} }\n", + "if condition must be bool, numeric, pointer, or string", + ), + ( + "invalid_match.wave", + "struct Value { x: i32; }\nfun main() { let v: Value = Value { x: 1 }; match (v) { _ => {} } }\n", + "match value must be an integer or enum", + ), + ( + "invalid_deref.wave", + "fun main() { let value: i32 = 1; println(\"{}\", deref value); }\n", + "deref expects a pointer", + ), + ( + "invalid_index_target.wave", + "fun main() { let value: i32 = 1; println(\"{}\", value[0]); }\n", + "index access requires an array or pointer", + ), + ( + "invalid_index_type.wave", + "fun main() { let values: array = [1]; println(\"{}\", values[\"text\"]); }\n", + "index expression must be an integer", + ), + ( + "invalid_unary.wave", + "fun main() { println(\"{}\", -\"text\"); }\n", + "unary operator `Neg` is not supported for `str`", + ), + ( + "invalid_increment.wave", + "fun main() { var flag: bool = true; flag++; }\n", + "++/-- requires a numeric or pointer lvalue", + ), + ( + "invalid_export.wave", + "export(rust) fun exposed() -> i32 { return 1; }\nfun main() {}\n", + "unsupported export ABI 'rust'", + ), + ]; + + for (file_name, source, expected) in cases { + let source = write_wave(&dir, file_name, source); + for mode in ["check", "build"] { + let output = if mode == "check" { + run_wavec_raw([OsStr::new("check"), source.as_os_str()]) + } else { + run_wavec_raw([ + OsStr::new("build"), + source.as_os_str(), + OsStr::new("--emit=obj"), + OsStr::new("--out-dir"), + dir.as_os_str(), + ]) + }; + assert!( + !output.status.success(), + "{} unexpectedly succeeded in {} mode", + file_name, + mode + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("error[E3001]"), "{}: {}", file_name, stderr); + assert!(stderr.contains(expected), "{}: {}", file_name, stderr); + assert!( + !stderr.contains("E9001") + && !stderr.contains("compiler internal error") + && !stderr.contains("panic location"), + "{} leaked a backend failure in {} mode: {}", + file_name, + mode, + stderr + ); + } + } +} + +#[test] +fn second_semantic_audit_rejects_unsafe_programs_before_codegen() { + let dir = temp_case_dir("semantic-audit-two"); + let cases = [ + ( + "array_format.wave", + "fun main() { println(\"{}\", [1, 2]); }\n", + "format argument must be a scalar", + ), + ( + "void_value.wave", + "fun noop() {}\nfun main() { println(\"{}\", noop()); }\n", + "found `void`", + ), + ( + "struct_format.wave", + "struct Pair { x: i32; }\nfun main() { let p: Pair = Pair { x: 1 }; println(\"{}\", p); }\n", + "found `Pair`", + ), + ( + "compound_string.wave", + "fun main() { var text: str = \"a\"; text += \"b\"; }\n", + "compound assignment `AddAssign` requires numeric operands", + ), + ( + "compound_bool.wave", + "fun main() { var flag: bool = true; flag += false; }\n", + "compound assignment `AddAssign` requires numeric operands", + ), + ( + "input_literal.wave", + "fun main() { input(\"{}\", 1); }\n", + "input argument must be a mutable lvalue", + ), + ( + "input_immutable.wave", + "fun main() { let value: i32 = 0; input(\"{}\", value); }\n", + "cannot write input into immutable binding `value`", + ), + ( + "invalid_struct_cast.wave", + "struct Pair { x: i32; }\nfun main() { let p: Pair = Pair { x: 1 }; let bits: i32 = p as i32; }\n", + "invalid cast from `Pair` to `i32`", + ), + ( + "invalid_string_float_cast.wave", + "fun main() { let value: f32 = \"text\" as f32; }\n", + "invalid cast from `str` to `f32`", + ), + ( + "invalid_void_cast.wave", + "fun noop() {}\nfun main() { let value: i32 = noop() as i32; }\n", + "invalid cast from `void` to `i32`", + ), + ( + "unknown_cast_type.wave", + "fun main() { let value: i32 = 1 as Missing; }\n", + "unknown type `Missing` in cast target", + ), + ( + "unknown_variable_type.wave", + "fun main() { var value: Missing; }\n", + "unknown type `Missing` in variable `value`", + ), + ( + "unknown_return_type.wave", + "fun make() -> Missing { var value: Missing; return value; }\nfun main() {}\n", + "unknown type `Missing` in return type of function `make`", + ), + ( + "unknown_struct_field_type.wave", + "struct Holder { value: Missing; }\nfun main() {}\n", + "unknown type `Missing` in field `Holder.value`", + ), + ( + "unknown_alias_target.wave", + "type Alias = Missing;\nfun main() {}\n", + "unknown type `Missing` in type alias `Alias`", + ), + ( + "unknown_proto_target.wave", + "proto Missing { fun read(self: Missing) -> i32 { return 1; } }\nfun main() {}\n", + "proto implementation targets unknown type `Missing`", + ), + ( + "void_variable.wave", + "fun main() { var value: void; }\n", + "variable `value` cannot use the `void` type", + ), + ( + "void_struct_field.wave", + "struct Invalid { value: void; }\nfun main() {}\n", + "field `Invalid.value` cannot use the `void` type", + ), + ( + "cyclic_pointer_alias.wave", + "type Loop = ptr;\nfun main() {}\n", + "cyclic type alias involving `Loop`", + ), + ( + "float_enum_repr.wave", + "enum Invalid -> f32 { Value = 1 }\nfun main() {}\n", + "enum `Invalid` representation must be an integer type", + ), + ( + "out_of_range_literal.wave", + "fun main() { let value: i8 = 300; }\n", + "initializer for `value`", + ), + ( + "negative_out_of_range_literal.wave", + "fun main() { let value: i8 = -129; }\n", + "initializer for `value`", + ), + ( + "negative_unsigned_literal.wave", + "fun main() { let value: u8 = -1; }\n", + "initializer for `value`", + ), + ( + "wrong_addressed_array_element.wave", + "fun values() -> ptr> { return &[\"text\"]; }\nfun main() {}\n", + "element 0 of return value of function `values`", + ), + ( + "duplicate_local.wave", + "fun main() { let value: i32 = 1; let value: i32 = 2; }\n", + "duplicate variable declaration `value` in the same scope", + ), + ( + "duplicate_struct_field.wave", + "struct Pair { x: i32; x: i64; }\nfun main() {}\n", + "duplicate field `x` in struct `Pair`", + ), + ( + "duplicate_enum_variant.wave", + "enum Mode -> i32 { Same = 1, Same = 2 }\nfun main() {}\n", + "duplicate variant `Same` in enum `Mode`", + ), + ( + "duplicate_method.wave", + "struct Pair { x: i32; }\nproto Pair { fun read(self: Pair) -> i32 { return self.x; } fun read(self: Pair) -> i32 { return self.x; } }\nfun main() {}\n", + "duplicate method `Pair.read`", + ), + ( + "duplicate_match_value.wave", + "enum Mode -> i32 { First = 1, Second = 1 }\nfun main() { let mode: Mode = First; match (mode) { First => {} Second => {} } }\n", + "duplicate match case pattern `value:1`", + ), + ( + "mixed_float_widths.wave", + "fun add(left: f32, right: f64) -> f32 { return left + right; }\nfun main() {}\n", + "mixed float widths require an explicit cast", + ), + ]; + + for (file_name, source, expected) in cases { + let source = write_wave(&dir, file_name, source); + for mode in ["check", "build"] { + let output = if mode == "check" { + run_wavec_raw([OsStr::new("check"), source.as_os_str()]) + } else { + run_wavec_raw([ + OsStr::new("build"), + source.as_os_str(), + OsStr::new("--emit=obj"), + OsStr::new("--out-dir"), + dir.as_os_str(), + ]) + }; + assert!( + !output.status.success(), + "{} unexpectedly succeeded in {} mode", + file_name, + mode + ); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("error[E3001]"), "{}: {}", file_name, stderr); + assert!(stderr.contains(expected), "{}: {}", file_name, stderr); + assert!( + !stderr.contains("E9001") + && !stderr.contains("compiler internal error") + && !stderr.contains("panic location"), + "{} leaked a backend failure in {} mode: {}", + file_name, + mode, + stderr + ); + } + } +} + +#[test] +fn second_semantic_audit_preserves_explicit_and_contextual_valid_cases() { + let dir = temp_case_dir("semantic-audit-two-valid"); + let source = write_wave( + &dir, + "valid.wave", + r#" +enum Mode -> i32 { + First = 1, + Alias = 1 +} + +fun noop() {} + +fun add(left: f32, right: f64) -> f32 { + return left + (right as f32); +} + +fun main() { + noop(); + let minimum: i8 = -128; + let bit_pattern: i8 = 0xFF; + let unsigned_max: u128 = 340282366920938463463374607431768211455; + let explicit: i8 = 300 as i8; + let values: ptr> = &[1, 2]; + var input_value: i32 = 0; + if (true) { + let minimum: i32 = 1; + println("{}", minimum); + } + println("{} {} {} {} {}", minimum, bit_pattern, unsigned_max, explicit, values); +} +"#, + ); + + run_wavec([OsStr::new("check"), source.as_os_str()]); + run_wavec([ + OsStr::new("build"), + source.as_os_str(), + OsStr::new("--emit=obj"), + OsStr::new("--out-dir"), + dir.as_os_str(), + ]); +} + +#[test] +fn semantic_diagnostics_point_at_the_relevant_source() { + let dir = temp_case_dir("semantic-source-position"); + let source = write_wave( + &dir, + "wrong_return.wave", + r#" +fun value() -> i32 { + return "text"; +} +"#, + ); + let output = run_wavec_raw([OsStr::new("check"), source.as_os_str()]); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("wrong_return.wave:3:5"), "{}", stderr); + assert!(stderr.contains("return \"text\";"), "{}", stderr); + + let repeated_return = write_wave( + &dir, + "repeated_return.wave", + r#" +fun value(flag: bool) -> i32 { + if (flag) { + return 1; + } + return "text"; +} +"#, + ); + let output = run_wavec_raw([OsStr::new("check"), repeated_return.as_os_str()]); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("repeated_return.wave:6:5"), "{}", stderr); + assert!(stderr.contains("return \"text\";"), "{}", stderr); + + let duplicate = write_wave( + &dir, + "duplicate.wave", + r#" +fun other() { + let value: i32 = 0; +} + +fun main() { + let value: i32 = 1; + let value: i32 = 2; +} +"#, + ); + let output = run_wavec_raw([OsStr::new("check"), duplicate.as_os_str()]); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("duplicate.wave:8:9"), "{}", stderr); + assert!(stderr.contains("let value: i32 = 2;"), "{}", stderr); + + let duplicate_type = write_wave( + &dir, + "duplicate_type.wave", + "struct Item { first: i32; }\nstruct Item { second: i32; }\nfun main() {}\n", + ); + let output = run_wavec_raw([OsStr::new("check"), duplicate_type.as_os_str()]); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("duplicate_type.wave:2:8"), "{}", stderr); + assert!( + stderr.contains("struct Item { second: i32; }"), + "{}", + stderr + ); + + let imported = dir.join("broken.wave"); + fs::write( + &imported, + "fun broken() {\n let value: Missing = 1;\n}\n", + ) + .unwrap(); + let entry = write_wave( + &dir, + "import_main.wave", + "import(\"broken\");\n\nfun main() {}\n", + ); + let output = run_wavec_raw([OsStr::new("check"), entry.as_os_str()]); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("broken.wave:2:9"), "{}", stderr); + assert!(stderr.contains("let value: Missing = 1;"), "{}", stderr); + assert!(!stderr.contains("import_main.wave:1:1"), "{}", stderr); + + let generic_import = dir.join("generic_broken.wave"); + fs::write( + &generic_import, + "struct Box { value: T; }\nfun broken() {\n let value: Box;\n}\n", + ) + .unwrap(); + let generic_entry = write_wave( + &dir, + "generic_import_main.wave", + "import(\"generic_broken\");\n\nfun main() {}\n", + ); + let output = run_wavec_raw([OsStr::new("check"), generic_entry.as_os_str()]); + assert!(!output.status.success()); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("generic_broken.wave:3:9"), "{}", stderr); + assert!( + stderr.contains("type `Box` expects 1 generic argument(s), found 2"), + "{}", + stderr + ); + assert!( + !stderr.contains("generic monomorphization failed"), + "{}", + stderr + ); +} + +#[test] +fn semantic_validation_accepts_complete_returns_and_explicit_casts() { + let dir = temp_case_dir("semantic-validation-valid"); + let source = write_wave( + &dir, + "valid.wave", + r#" +struct Pair { + x: i32; + y: i32; +} + +fun choose(flag: bool) -> i32 { + if (flag) { + return 1; + } else { + return 2; + } + println("unreachable"); +} + +fun dead_after_return() { + return; + println("unreachable"); +} + +fun widen(value: i32) -> i64 { + return value; +} + +fun narrow_explicitly(value: i64) -> i32 { + return value as i32; +} + +fun values() -> array { + return [1, 2]; +} + +fun pointer_bits() -> i64 { + return "text" as i64; +} + +fun infinite() -> i32 { + while (true) { + continue; + } +} + +fun main() -> i32 { + let value: i32 = choose(true); + let bits: i64 = pointer_bits(); + let pair: Pair = Pair { x: 1, y: 2 }; + let items: array = values(); + let pointer: ptr = &pair; + if (pointer) { + if (value == 1 && bits != 0 && items[0] == 1 && widen(pair.y) == 2) { + return narrow_explicitly(0); + } + } + return infinite(); +} +"#, + ); + let out_dir = dir.join("out"); + run_wavec([ + OsStr::new("build"), + source.as_os_str(), + OsStr::new("--emit=ir"), + OsStr::new("--out-dir"), + out_dir.as_os_str(), + ]); +} + #[test] fn vex_cli_print_json_contracts_are_machine_readable() { let (stdout, stderr) = run_wavec_capture([ From 65da8bcb646ed623818d92eb3934e760002d243f Mon Sep 17 00:00:00 2001 From: LunaStev Date: Sat, 8 Aug 2026 19:27:39 +0900 Subject: [PATCH 2/2] Stabilize Rust tests across CI architectures Make compiler integration tests deterministic when GitHub Actions enables colored Cargo output. All test helpers now launch wavec with NO_COLOR=1, preventing ANSI styling from splitting diagnostic tokens such as error[E3001] and causing valid semantic-diagnostic assertions to fail on Linux and macOS runners. Expose the tested host architecture directly in each workflow check name: - rename build-ubuntu to build-linux-amd64 - rename build-macos to build-macos-arm64 - rename build-windows to build-windows-amd64 Repair Windows toolchain provisioning after the current MSYS2 package database stopped resolving the versioned llvm-21 and lld-21 package names through pacman -S. Keep the regular runtime dependencies in setup-msys2 and install the official LLVM 21.1.8-5 and LLD 21.1.8-5 packages from pinned MSYS2 mirror URLs so the llvm-sys 21 contract and /mingw64/opt/llvm-21 layout remain stable. Validation: - reproduced the GitHub runner environment with NO_COLOR unset and CARGO_TERM_COLOR=always - cargo test --locked --all-targets: 15 tests passed - cargo fmt --all -- --check - cargo clippy --locked --all-targets -- -D warnings - parsed .github/workflows/rust.yml successfully - verified both pinned MSYS2 package URLs return HTTP 200 - git diff --check --- .github/workflows/rust.yml | 17 ++++++++++++----- tests/codegen_regressions.rs | 14 ++++++++++---- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 772336cd..4a12653b 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -18,7 +18,7 @@ concurrency: cancel-in-progress: true jobs: - build-ubuntu: + build-linux-amd64: runs-on: ubuntu-latest timeout-minutes: 45 @@ -69,7 +69,7 @@ jobs: - name: Run Wave end-to-end tests run: python3 tools/run_tests.py - build-macos: + build-macos-arm64: runs-on: macos-latest timeout-minutes: 45 @@ -129,7 +129,7 @@ jobs: - name: Run Wave end-to-end tests run: python3 tools/run_tests.py - build-windows: + build-windows-amd64: runs-on: windows-latest timeout-minutes: 60 @@ -144,12 +144,19 @@ jobs: install: >- mingw-w64-x86_64-gcc mingw-w64-x86_64-python - mingw-w64-x86_64-llvm-21 - mingw-w64-x86_64-lld-21 mingw-w64-x86_64-libffi mingw-w64-x86_64-zstd mingw-w64-x86_64-zlib + - name: Install pinned LLVM 21 and LLD packages + shell: msys2 {0} + run: | + set -euo pipefail + + pacman --noconfirm -U \ + https://mirror.msys2.org/mingw/mingw64/mingw-w64-x86_64-llvm-21-21.1.8-5-any.pkg.tar.zst \ + https://mirror.msys2.org/mingw/mingw64/mingw-w64-x86_64-lld-21-21.1.8-5-any.pkg.tar.zst + - name: Install Rust GNU toolchain shell: msys2 {0} run: | diff --git a/tests/codegen_regressions.rs b/tests/codegen_regressions.rs index 3bbeff10..647b47ce 100644 --- a/tests/codegen_regressions.rs +++ b/tests/codegen_regressions.rs @@ -36,12 +36,18 @@ fn write_wave(dir: &Path, name: &str, source: &str) -> PathBuf { path } +fn wavec_command() -> Command { + let mut command = Command::new(wavec_bin()); + command.env("NO_COLOR", "1"); + command +} + fn run_wavec(args: I) where I: IntoIterator, S: AsRef, { - let output = Command::new(wavec_bin()).args(args).output().unwrap(); + let output = wavec_command().args(args).output().unwrap(); assert!( output.status.success(), "wavec failed with status {}\nstdout:\n{}\nstderr:\n{}", @@ -56,7 +62,7 @@ where I: IntoIterator, S: AsRef, { - let output = Command::new(wavec_bin()).args(args).output().unwrap(); + let output = wavec_command().args(args).output().unwrap(); assert!( output.status.success(), "wavec failed with status {}\nstdout:\n{}\nstderr:\n{}", @@ -76,7 +82,7 @@ where I: IntoIterator, S: AsRef, { - Command::new(wavec_bin()).args(args).output().unwrap() + wavec_command().args(args).output().unwrap() } fn run_wavec_expect_failure(args: I) -> String @@ -84,7 +90,7 @@ where I: IntoIterator, S: AsRef, { - let output = Command::new(wavec_bin()).args(args).output().unwrap(); + let output = wavec_command().args(args).output().unwrap(); assert!( !output.status.success(), "wavec unexpectedly succeeded\nstdout:\n{}\nstderr:\n{}",