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/.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..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{}", @@ -126,6 +132,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([