From 79c6054755fa97a335ead243fc82143c338c9d86 Mon Sep 17 00:00:00 2001 From: hiyouwhatsapp <89997383+hiyouwhatsapp@users.noreply.github.com> Date: Tue, 3 Nov 2026 15:33:10 +0200 Subject: [PATCH 1/6] refactor(hir): isolate type canonicalization and pattern traversal Move internal canonicalization and match-pattern walkers out of TypedProgram without changing their order or behavior Preserve the public target-resolution API and add focused identity and source-span regressions Signed-off-by: hiyouwhatsapp <89997383+hiyouwhatsapp@users.noreply.github.com> --- front/hir/src/canonicalization.rs | 349 +++++++++++++++ front/hir/src/lib.rs | 423 +----------------- front/hir/src/traversal.rs | 92 ++++ front/hir/tests/canonicalization_traversal.rs | 56 +++ 4 files changed, 505 insertions(+), 415 deletions(-) create mode 100644 front/hir/src/canonicalization.rs create mode 100644 front/hir/src/traversal.rs create mode 100644 front/hir/tests/canonicalization_traversal.rs diff --git a/front/hir/src/canonicalization.rs b/front/hir/src/canonicalization.rs new file mode 100644 index 00000000..e130f9a5 --- /dev/null +++ b/front/hir/src/canonicalization.rs @@ -0,0 +1,349 @@ +// This file is part of the Wave language project. +// Copyright (c) 2024–2026 Wave Foundation +// Copyright (c) 2024–2026 LunaStev and contributors +// +// This Source Code Form is subject to the terms of the +// Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. +// +// 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. +//! Target-sized type resolution and backend-boundary type canonicalization. + +use parser::ast::{ASTNode, Expression, StatementNode, WaveType}; +use parser::types::{parse_type, split_top_level_generic_args, token_type_to_wave_type}; +use std::collections::{HashMap, HashSet}; + +/// Resolve target-sized integers before semantic analysis and monomorphization. +/// This pass uses only the selected pointer width, never LLVM or the host width. +pub fn resolve_target_types(nodes: &mut [ASTNode], pointer_bits: u16) -> Result<(), String> { + if !matches!(pointer_bits, 32 | 64) { + return Err(format!("unsupported target pointer width {pointer_bits}")); + } + let named = HashMap::from([ + ("isz".to_string(), WaveType::Int(pointer_bits)), + ("usz".to_string(), WaveType::Uint(pointer_bits)), + ]); + for node in nodes { + canonicalize_node_types(node, &named); + } + Ok(()) +} + +pub(super) fn canonicalize_syntax_types(nodes: &mut [ASTNode]) { + let named = collect_named_types(nodes); + for node in nodes { + canonicalize_node_types(node, &named); + } +} + +fn collect_named_types(nodes: &[ASTNode]) -> HashMap { + let mut named = HashMap::new(); + for node in nodes { + match node.unspanned() { + ASTNode::TypeAlias(alias) => { + named.insert(alias.name.clone(), alias.target.clone()); + }, + ASTNode::Enum(enumeration) => { + named.insert(enumeration.name.clone(), enumeration.repr_type.clone()); + }, + ASTNode::Variant(variant) => { + named.insert(variant.name.clone(), WaveType::Variant(variant.name.clone())); + }, + _ => {}, + } + } + named +} + +fn canonical_type( + ty: &WaveType, + named: &HashMap, + visiting: &mut HashSet, +) -> WaveType { + match ty { + WaveType::Isz => named.get("isz").cloned().unwrap_or(WaveType::Isz), + WaveType::Usz => named.get("usz").cloned().unwrap_or(WaveType::Usz), + WaveType::Future(inner) => { + WaveType::Future(Box::new(canonical_type(inner, named, visiting))) + }, + WaveType::Pointer(inner) => { + WaveType::Pointer(Box::new(canonical_type(inner, named, visiting))) + }, + WaveType::Array(inner, length) => { + WaveType::Array(Box::new(canonical_type(inner, named, visiting)), *length) + }, + WaveType::Struct(name) => canonical_named_type(name, named, visiting) + .unwrap_or_else(|| WaveType::Struct(name.clone())), + WaveType::Variant(name) => canonical_variant_application(name, named, visiting) + .unwrap_or_else(|| WaveType::Variant(name.clone())), + _ => ty.clone(), + } +} + +fn canonical_named_type( + name: &str, + named: &HashMap, + visiting: &mut HashSet, +) -> Option { + if let Some(target) = named.get(name) { + assert!( + visiting.insert(name.to_string()), + "semantic validation allowed a named type cycle at `{name}`" + ); + let resolved = canonical_type(target, named, visiting); + visiting.remove(name); + return Some(resolved); + } + canonical_variant_application(name, named, visiting) +} + +fn canonical_variant_application( + name: &str, + named: &HashMap, + visiting: &mut HashSet, +) -> Option { + let (base, arguments) = split_named_application(name)?; + let arguments = arguments + .into_iter() + .map(|argument| canonical_type(&argument, named, visiting)) + .map(|argument| display_wave_type(&argument)) + .collect::>() + .join(","); + let name = format!("{base}<{arguments}>"); + Some(if matches!(named.get(base), Some(WaveType::Variant(_))) { + WaveType::Variant(name) + } else { + WaveType::Struct(name) + }) +} + +fn split_named_application(name: &str) -> Option<(&str, Vec)> { + let (base, tail) = name.split_once('<')?; + let inner = tail.strip_suffix('>')?; + let arguments = split_top_level_generic_args(inner)? + .into_iter() + .map(|argument| token_type_to_wave_type(&parse_type(&argument)?)) + .collect::>>()?; + Some((base.trim(), arguments)) +} + +fn display_wave_type(ty: &WaveType) -> String { + match ty { + WaveType::Isz => "isz".to_string(), + WaveType::Usz => "usz".to_string(), + 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::Future(inner) => format!("Future<{}>", display_wave_type(inner)), + WaveType::Pointer(inner) => format!("ptr<{}>", display_wave_type(inner)), + WaveType::Array(inner, length) => { + format!("array<{},{}>", display_wave_type(inner), length) + }, + WaveType::Void => "void".to_string(), + WaveType::Never => "!".to_string(), + WaveType::Struct(name) | WaveType::Variant(name) => name.clone(), + } +} + +fn canonicalize_type(ty: &mut WaveType, named: &HashMap) { + *ty = canonical_type(ty, named, &mut HashSet::new()); +} + +fn canonicalize_function_types( + function: &mut parser::ast::FunctionNode, + named: &HashMap, +) { + for parameter in &mut function.parameters { + canonicalize_type(&mut parameter.param_type, named); + if let Some(default) = &mut parameter.initial_value { + canonicalize_expression_types(default, named); + } + } + if let Some(return_type) = &mut function.return_type { + canonicalize_type(return_type, named); + } + for node in &mut function.body { + canonicalize_node_types(node, named); + } +} + +fn canonicalize_node_types(node: &mut ASTNode, named: &HashMap) { + match node { + ASTNode::Located { value, .. } => canonicalize_node_types(value, named), + ASTNode::Function(function) => canonicalize_function_types(function, named), + ASTNode::ExternFunction(function) => { + for (_, parameter_type) in &mut function.params { + canonicalize_type(parameter_type, named); + } + canonicalize_type(&mut function.return_type, named); + }, + ASTNode::Program(parameter) => canonicalize_type(&mut parameter.param_type, named), + ASTNode::Statement(statement) => canonicalize_statement_types(statement, named), + ASTNode::Variable(variable) => { + canonicalize_type(&mut variable.type_name, named); + if let Some(initializer) = &mut variable.initial_value { + canonicalize_expression_types(initializer, named); + } + }, + ASTNode::Expression(expression) => canonicalize_expression_types(expression, named), + ASTNode::Struct(structure) => { + for (_, field_type) in &mut structure.fields { + canonicalize_type(field_type, named); + } + for method in &mut structure.methods { + canonicalize_function_types(method, named); + } + }, + ASTNode::ProtoImpl(implementation) => { + for method in &mut implementation.methods { + canonicalize_function_types(method, named); + } + }, + ASTNode::TypeAlias(alias) => canonicalize_type(&mut alias.target, named), + ASTNode::Enum(enumeration) => canonicalize_type(&mut enumeration.repr_type, named), + ASTNode::Variant(variant) => { + for case in &mut variant.cases { + for payload_type in &mut case.payload_types { + canonicalize_type(payload_type, named); + } + } + }, + } +} + +fn canonicalize_statement_types(statement: &mut StatementNode, named: &HashMap) { + match statement { + StatementNode::PrintFormat { args, .. } + | StatementNode::PrintlnFormat { args, .. } + | StatementNode::Input { args, .. } => { + for argument in args { + canonicalize_expression_types(argument, named); + } + }, + StatementNode::If { condition, body, else_if_blocks, else_block } => { + canonicalize_expression_types(condition, named); + for node in body { + canonicalize_node_types(node, named); + } + if let Some(blocks) = else_if_blocks { + for (condition, body) in blocks.iter_mut() { + canonicalize_expression_types(condition, named); + for node in body { + canonicalize_node_types(node, named); + } + } + } + if let Some(body) = else_block { + for node in body.iter_mut() { + canonicalize_node_types(node, named); + } + } + }, + StatementNode::For { initialization, condition, increment, body } => { + canonicalize_node_types(initialization, named); + canonicalize_expression_types(condition, named); + canonicalize_expression_types(increment, named); + for node in body { + canonicalize_node_types(node, named); + } + }, + StatementNode::While { condition, body } => { + canonicalize_expression_types(condition, named); + for node in body { + canonicalize_node_types(node, named); + } + }, + StatementNode::Match { value, arms } => { + canonicalize_expression_types(value, named); + for arm in arms { + for node in &mut arm.body { + canonicalize_node_types(node, named); + } + } + }, + StatementNode::Assign { value, .. } => canonicalize_expression_types(value, named), + StatementNode::AsmBlock { inputs, outputs, .. } => { + for (_, expression) in inputs.iter_mut().chain(outputs.iter_mut()) { + canonicalize_expression_types(expression, named); + } + }, + StatementNode::Return(Some(expression)) | StatementNode::Expression(expression) => { + canonicalize_expression_types(expression, named); + }, + StatementNode::Print(_) + | StatementNode::Println(_) + | StatementNode::Variable(_) + | StatementNode::Import(_) + | StatementNode::Break + | StatementNode::Continue + | StatementNode::Return(None) => {}, + } +} + +fn canonicalize_expression_types(expression: &mut Expression, named: &HashMap) { + match expression { + Expression::Located { value, .. } => canonicalize_expression_types(value, named), + Expression::StructLiteral { name, fields } => { + if let Some(ty) = canonical_variant_application(name, named, &mut HashSet::new()) { + *name = display_wave_type(&ty); + } + for (_, value) in fields { + canonicalize_expression_types(value, named); + } + }, + Expression::FunctionCall { type_args, args, .. } => { + for type_argument in type_args { + canonicalize_type(type_argument, named); + } + for argument in args { + canonicalize_expression_types(argument, named); + } + }, + Expression::MethodCall { object, args, type_args, .. } => { + for type_argument in type_args { + canonicalize_type(type_argument, named); + } + canonicalize_expression_types(object, named); + for argument in args { + canonicalize_expression_types(argument, named); + } + }, + Expression::Deref(inner) + | Expression::AddressOf(inner) + | Expression::Await(inner) + | Expression::Grouped(inner) + | Expression::Unary { expr: inner, .. } + | Expression::FieldAccess { object: inner, .. } + | Expression::IncDec { target: inner, .. } => { + canonicalize_expression_types(inner, named); + }, + Expression::Cast { expr, target_type } => { + canonicalize_expression_types(expr, named); + canonicalize_type(target_type, named); + }, + Expression::BinaryExpression { left, right, .. } + | Expression::IndexAccess { target: left, index: right } + | Expression::AssignOperation { target: left, value: right, .. } + | Expression::Assignment { target: left, value: right } => { + canonicalize_expression_types(left, named); + canonicalize_expression_types(right, named); + }, + Expression::ArrayLiteral(values) => { + for value in values { + canonicalize_expression_types(value, named); + } + }, + Expression::AsmBlock { inputs, outputs, .. } => { + for (_, expression) in inputs.iter_mut().chain(outputs.iter_mut()) { + canonicalize_expression_types(expression, named); + } + }, + Expression::Null | Expression::Literal(_) | Expression::Variable(_) => {}, + } +} diff --git a/front/hir/src/lib.rs b/front/hir/src/lib.rs index 44a89a92..351cc8c6 100644 --- a/front/hir/src/lib.rs +++ b/front/hir/src/lib.rs @@ -24,18 +24,22 @@ pub mod async_lower; pub mod conversions; +mod canonicalization; mod defaults; pub mod enums; pub use enums::{EnumConversion, EnumType, EnumVariant, TypeId}; mod numeric_checks; +mod traversal; +use canonicalization::canonicalize_syntax_types; +pub use canonicalization::resolve_target_types; use conversions::{ConversionError, NumericExpressionInfo}; +use traversal::walk_patterns_in_nodes; pub use numeric_checks::{integer_literal_float, ConstantValue}; -use parser::ast::visit::{walk_expression, walk_node, walk_nodes}; +use parser::ast::visit::walk_nodes; -use parser::ast::{ASTNode, Expression, MatchPattern, StatementNode, WaveType}; -use parser::types::{parse_type, split_top_level_generic_args, token_type_to_wave_type}; +use parser::ast::{ASTNode, Expression, MatchPattern, WaveType}; use parser::verification::{analyze_semantic_facts, SemanticDiagnostic, SemanticFacts}; -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use std::fmt; /// Stable identity of an AST declaration or statement in one typed program. @@ -434,414 +438,3 @@ impl TypedProgram { self.pattern_id(pattern).and_then(|id| self.variant_pattern(id)) } } - -/// Resolve target-sized integers before semantic analysis and monomorphization. -/// This pass uses only the selected pointer width, never LLVM or the host width. -pub fn resolve_target_types(nodes: &mut [ASTNode], pointer_bits: u16) -> Result<(), String> { - if !matches!(pointer_bits, 32 | 64) { - return Err(format!("unsupported target pointer width {pointer_bits}")); - } - let named = HashMap::from([ - ("isz".to_string(), WaveType::Int(pointer_bits)), - ("usz".to_string(), WaveType::Uint(pointer_bits)), - ]); - for node in nodes { - canonicalize_node_types(node, &named); - } - Ok(()) -} - -fn canonicalize_syntax_types(nodes: &mut [ASTNode]) { - let named = collect_named_types(nodes); - for node in nodes { - canonicalize_node_types(node, &named); - } -} - -fn collect_named_types(nodes: &[ASTNode]) -> HashMap { - let mut named = HashMap::new(); - for node in nodes { - match node.unspanned() { - ASTNode::TypeAlias(alias) => { - named.insert(alias.name.clone(), alias.target.clone()); - }, - ASTNode::Enum(enumeration) => { - named.insert(enumeration.name.clone(), enumeration.repr_type.clone()); - }, - ASTNode::Variant(variant) => { - named.insert(variant.name.clone(), WaveType::Variant(variant.name.clone())); - }, - _ => {}, - } - } - named -} - -fn canonical_type( - ty: &WaveType, - named: &HashMap, - visiting: &mut HashSet, -) -> WaveType { - match ty { - WaveType::Isz => named.get("isz").cloned().unwrap_or(WaveType::Isz), - WaveType::Usz => named.get("usz").cloned().unwrap_or(WaveType::Usz), - WaveType::Future(inner) => { - WaveType::Future(Box::new(canonical_type(inner, named, visiting))) - }, - WaveType::Pointer(inner) => { - WaveType::Pointer(Box::new(canonical_type(inner, named, visiting))) - }, - WaveType::Array(inner, length) => { - WaveType::Array(Box::new(canonical_type(inner, named, visiting)), *length) - }, - WaveType::Struct(name) => canonical_named_type(name, named, visiting) - .unwrap_or_else(|| WaveType::Struct(name.clone())), - WaveType::Variant(name) => canonical_variant_application(name, named, visiting) - .unwrap_or_else(|| WaveType::Variant(name.clone())), - _ => ty.clone(), - } -} - -fn canonical_named_type( - name: &str, - named: &HashMap, - visiting: &mut HashSet, -) -> Option { - if let Some(target) = named.get(name) { - assert!( - visiting.insert(name.to_string()), - "semantic validation allowed a named type cycle at `{name}`" - ); - let resolved = canonical_type(target, named, visiting); - visiting.remove(name); - return Some(resolved); - } - canonical_variant_application(name, named, visiting) -} - -fn canonical_variant_application( - name: &str, - named: &HashMap, - visiting: &mut HashSet, -) -> Option { - let (base, arguments) = split_named_application(name)?; - let arguments = arguments - .into_iter() - .map(|argument| canonical_type(&argument, named, visiting)) - .map(|argument| display_wave_type(&argument)) - .collect::>() - .join(","); - let name = format!("{base}<{arguments}>"); - Some(if matches!(named.get(base), Some(WaveType::Variant(_))) { - WaveType::Variant(name) - } else { - WaveType::Struct(name) - }) -} - -fn split_named_application(name: &str) -> Option<(&str, Vec)> { - let (base, tail) = name.split_once('<')?; - let inner = tail.strip_suffix('>')?; - let arguments = split_top_level_generic_args(inner)? - .into_iter() - .map(|argument| token_type_to_wave_type(&parse_type(&argument)?)) - .collect::>>()?; - Some((base.trim(), arguments)) -} - -fn display_wave_type(ty: &WaveType) -> String { - match ty { - WaveType::Isz => "isz".to_string(), - WaveType::Usz => "usz".to_string(), - 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::Future(inner) => format!("Future<{}>", display_wave_type(inner)), - WaveType::Pointer(inner) => format!("ptr<{}>", display_wave_type(inner)), - WaveType::Array(inner, length) => { - format!("array<{},{}>", display_wave_type(inner), length) - }, - WaveType::Void => "void".to_string(), - WaveType::Never => "!".to_string(), - WaveType::Struct(name) | WaveType::Variant(name) => name.clone(), - } -} - -fn canonicalize_type(ty: &mut WaveType, named: &HashMap) { - *ty = canonical_type(ty, named, &mut HashSet::new()); -} - -fn canonicalize_function_types( - function: &mut parser::ast::FunctionNode, - named: &HashMap, -) { - for parameter in &mut function.parameters { - canonicalize_type(&mut parameter.param_type, named); - if let Some(default) = &mut parameter.initial_value { - canonicalize_expression_types(default, named); - } - } - if let Some(return_type) = &mut function.return_type { - canonicalize_type(return_type, named); - } - for node in &mut function.body { - canonicalize_node_types(node, named); - } -} - -fn canonicalize_node_types(node: &mut ASTNode, named: &HashMap) { - match node { - ASTNode::Located { value, .. } => canonicalize_node_types(value, named), - ASTNode::Function(function) => canonicalize_function_types(function, named), - ASTNode::ExternFunction(function) => { - for (_, parameter_type) in &mut function.params { - canonicalize_type(parameter_type, named); - } - canonicalize_type(&mut function.return_type, named); - }, - ASTNode::Program(parameter) => canonicalize_type(&mut parameter.param_type, named), - ASTNode::Statement(statement) => canonicalize_statement_types(statement, named), - ASTNode::Variable(variable) => { - canonicalize_type(&mut variable.type_name, named); - if let Some(initializer) = &mut variable.initial_value { - canonicalize_expression_types(initializer, named); - } - }, - ASTNode::Expression(expression) => canonicalize_expression_types(expression, named), - ASTNode::Struct(structure) => { - for (_, field_type) in &mut structure.fields { - canonicalize_type(field_type, named); - } - for method in &mut structure.methods { - canonicalize_function_types(method, named); - } - }, - ASTNode::ProtoImpl(implementation) => { - for method in &mut implementation.methods { - canonicalize_function_types(method, named); - } - }, - ASTNode::TypeAlias(alias) => canonicalize_type(&mut alias.target, named), - ASTNode::Enum(enumeration) => canonicalize_type(&mut enumeration.repr_type, named), - ASTNode::Variant(variant) => { - for case in &mut variant.cases { - for payload_type in &mut case.payload_types { - canonicalize_type(payload_type, named); - } - } - }, - } -} - -fn canonicalize_statement_types(statement: &mut StatementNode, named: &HashMap) { - match statement { - StatementNode::PrintFormat { args, .. } - | StatementNode::PrintlnFormat { args, .. } - | StatementNode::Input { args, .. } => { - for argument in args { - canonicalize_expression_types(argument, named); - } - }, - StatementNode::If { condition, body, else_if_blocks, else_block } => { - canonicalize_expression_types(condition, named); - for node in body { - canonicalize_node_types(node, named); - } - if let Some(blocks) = else_if_blocks { - for (condition, body) in blocks.iter_mut() { - canonicalize_expression_types(condition, named); - for node in body { - canonicalize_node_types(node, named); - } - } - } - if let Some(body) = else_block { - for node in body.iter_mut() { - canonicalize_node_types(node, named); - } - } - }, - StatementNode::For { initialization, condition, increment, body } => { - canonicalize_node_types(initialization, named); - canonicalize_expression_types(condition, named); - canonicalize_expression_types(increment, named); - for node in body { - canonicalize_node_types(node, named); - } - }, - StatementNode::While { condition, body } => { - canonicalize_expression_types(condition, named); - for node in body { - canonicalize_node_types(node, named); - } - }, - StatementNode::Match { value, arms } => { - canonicalize_expression_types(value, named); - for arm in arms { - for node in &mut arm.body { - canonicalize_node_types(node, named); - } - } - }, - StatementNode::Assign { value, .. } => canonicalize_expression_types(value, named), - StatementNode::AsmBlock { inputs, outputs, .. } => { - for (_, expression) in inputs.iter_mut().chain(outputs.iter_mut()) { - canonicalize_expression_types(expression, named); - } - }, - StatementNode::Return(Some(expression)) | StatementNode::Expression(expression) => { - canonicalize_expression_types(expression, named); - }, - StatementNode::Print(_) - | StatementNode::Println(_) - | StatementNode::Variable(_) - | StatementNode::Import(_) - | StatementNode::Break - | StatementNode::Continue - | StatementNode::Return(None) => {}, - } -} - -fn canonicalize_expression_types(expression: &mut Expression, named: &HashMap) { - match expression { - Expression::Located { value, .. } => canonicalize_expression_types(value, named), - Expression::StructLiteral { name, fields } => { - if let Some(ty) = canonical_variant_application(name, named, &mut HashSet::new()) { - *name = display_wave_type(&ty); - } - for (_, value) in fields { - canonicalize_expression_types(value, named); - } - }, - Expression::FunctionCall { type_args, args, .. } => { - for type_argument in type_args { - canonicalize_type(type_argument, named); - } - for argument in args { - canonicalize_expression_types(argument, named); - } - }, - Expression::MethodCall { object, args, type_args, .. } => { - for type_argument in type_args { - canonicalize_type(type_argument, named); - } - canonicalize_expression_types(object, named); - for argument in args { - canonicalize_expression_types(argument, named); - } - }, - Expression::Deref(inner) - | Expression::AddressOf(inner) - | Expression::Await(inner) - | Expression::Grouped(inner) - | Expression::Unary { expr: inner, .. } - | Expression::FieldAccess { object: inner, .. } - | Expression::IncDec { target: inner, .. } => { - canonicalize_expression_types(inner, named); - }, - Expression::Cast { expr, target_type } => { - canonicalize_expression_types(expr, named); - canonicalize_type(target_type, named); - }, - Expression::BinaryExpression { left, right, .. } - | Expression::IndexAccess { target: left, index: right } - | Expression::AssignOperation { target: left, value: right, .. } - | Expression::Assignment { target: left, value: right } => { - canonicalize_expression_types(left, named); - canonicalize_expression_types(right, named); - }, - Expression::ArrayLiteral(values) => { - for value in values { - canonicalize_expression_types(value, named); - } - }, - Expression::AsmBlock { inputs, outputs, .. } => { - for (_, expression) in inputs.iter_mut().chain(outputs.iter_mut()) { - canonicalize_expression_types(expression, named); - } - }, - Expression::Null | Expression::Literal(_) | Expression::Variable(_) => {}, - } -} - -fn walk_patterns_in_nodes(nodes: &[ASTNode], visit: &mut impl FnMut(&MatchPattern)) { - for node in nodes { - match node { - ASTNode::Located { value, .. } => { - walk_patterns_in_nodes(std::slice::from_ref(value), visit) - }, - ASTNode::Function(function) => walk_patterns_in_nodes(&function.body, visit), - ASTNode::Struct(structure) => { - for method in &structure.methods { - walk_patterns_in_nodes(&method.body, visit); - } - }, - ASTNode::ProtoImpl(implementation) => { - for method in &implementation.methods { - walk_patterns_in_nodes(&method.body, visit); - } - }, - ASTNode::Statement(statement) => walk_patterns_in_statement(statement, visit), - ASTNode::ExternFunction(_) - | ASTNode::Program(_) - | ASTNode::Variable(_) - | ASTNode::Expression(_) - | ASTNode::TypeAlias(_) - | ASTNode::Enum(_) - | ASTNode::Variant(_) => {}, - } - } -} - -fn walk_patterns_in_statement(statement: &StatementNode, visit: &mut impl FnMut(&MatchPattern)) { - match statement { - StatementNode::If { body, else_if_blocks, else_block, .. } => { - walk_patterns_in_nodes(body, visit); - if let Some(blocks) = else_if_blocks { - for (_, body) in blocks.iter() { - walk_patterns_in_nodes(body, visit); - } - } - if let Some(body) = else_block { - walk_patterns_in_nodes(body, visit); - } - }, - StatementNode::For { initialization, body, .. } => { - walk_patterns_in_nodes(std::slice::from_ref(initialization.as_ref()), visit); - walk_patterns_in_nodes(body, visit); - }, - StatementNode::While { body, .. } => walk_patterns_in_nodes(body, visit), - StatementNode::Match { arms, .. } => { - for arm in arms { - walk_pattern(&arm.pattern, visit); - walk_patterns_in_nodes(&arm.body, visit); - } - }, - StatementNode::Print(_) - | StatementNode::PrintFormat { .. } - | StatementNode::Println(_) - | StatementNode::PrintlnFormat { .. } - | StatementNode::Input { .. } - | StatementNode::Variable(_) - | StatementNode::Import(_) - | StatementNode::Assign { .. } - | StatementNode::AsmBlock { .. } - | StatementNode::Break - | StatementNode::Continue - | StatementNode::Return(_) - | StatementNode::Expression(_) => {}, - } -} - -fn walk_pattern(pattern: &MatchPattern, visit: &mut impl FnMut(&MatchPattern)) { - visit(pattern); - if let MatchPattern::Variant { payloads, .. } = pattern { - for payload in payloads { - walk_pattern(payload, visit); - } - } -} diff --git a/front/hir/src/traversal.rs b/front/hir/src/traversal.rs new file mode 100644 index 00000000..33217721 --- /dev/null +++ b/front/hir/src/traversal.rs @@ -0,0 +1,92 @@ +// This file is part of the Wave language project. +// Copyright (c) 2024–2026 Wave Foundation +// Copyright (c) 2024–2026 LunaStev and contributors +// +// This Source Code Form is subject to the terms of the +// Mozilla Public License, v. 2.0. +// If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. +// +// 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. +//! Match-pattern traversal in stable source order for HIR identity assignment. + +use parser::ast::{ASTNode, MatchPattern, StatementNode}; + +pub(super) fn walk_patterns_in_nodes(nodes: &[ASTNode], visit: &mut impl FnMut(&MatchPattern)) { + for node in nodes { + match node { + ASTNode::Located { value, .. } => { + walk_patterns_in_nodes(std::slice::from_ref(value), visit) + }, + ASTNode::Function(function) => walk_patterns_in_nodes(&function.body, visit), + ASTNode::Struct(structure) => { + for method in &structure.methods { + walk_patterns_in_nodes(&method.body, visit); + } + }, + ASTNode::ProtoImpl(implementation) => { + for method in &implementation.methods { + walk_patterns_in_nodes(&method.body, visit); + } + }, + ASTNode::Statement(statement) => walk_patterns_in_statement(statement, visit), + ASTNode::ExternFunction(_) + | ASTNode::Program(_) + | ASTNode::Variable(_) + | ASTNode::Expression(_) + | ASTNode::TypeAlias(_) + | ASTNode::Enum(_) + | ASTNode::Variant(_) => {}, + } + } +} + +fn walk_patterns_in_statement(statement: &StatementNode, visit: &mut impl FnMut(&MatchPattern)) { + match statement { + StatementNode::If { body, else_if_blocks, else_block, .. } => { + walk_patterns_in_nodes(body, visit); + if let Some(blocks) = else_if_blocks { + for (_, body) in blocks.iter() { + walk_patterns_in_nodes(body, visit); + } + } + if let Some(body) = else_block { + walk_patterns_in_nodes(body, visit); + } + }, + StatementNode::For { initialization, body, .. } => { + walk_patterns_in_nodes(std::slice::from_ref(initialization.as_ref()), visit); + walk_patterns_in_nodes(body, visit); + }, + StatementNode::While { body, .. } => walk_patterns_in_nodes(body, visit), + StatementNode::Match { arms, .. } => { + for arm in arms { + walk_pattern(&arm.pattern, visit); + walk_patterns_in_nodes(&arm.body, visit); + } + }, + StatementNode::Print(_) + | StatementNode::PrintFormat { .. } + | StatementNode::Println(_) + | StatementNode::PrintlnFormat { .. } + | StatementNode::Input { .. } + | StatementNode::Variable(_) + | StatementNode::Import(_) + | StatementNode::Assign { .. } + | StatementNode::AsmBlock { .. } + | StatementNode::Break + | StatementNode::Continue + | StatementNode::Return(_) + | StatementNode::Expression(_) => {}, + } +} + +fn walk_pattern(pattern: &MatchPattern, visit: &mut impl FnMut(&MatchPattern)) { + visit(pattern); + if let MatchPattern::Variant { payloads, .. } = pattern { + for payload in payloads { + walk_pattern(payload, visit); + } + } +} diff --git a/front/hir/tests/canonicalization_traversal.rs b/front/hir/tests/canonicalization_traversal.rs new file mode 100644 index 00000000..e5ec09f5 --- /dev/null +++ b/front/hir/tests/canonicalization_traversal.rs @@ -0,0 +1,56 @@ +//! The extracted HIR helpers preserve the public target-width and identity contracts. +use hir::{resolve_target_types, TypedProgram}; +use lexer::Lexer; +use parser::ast::{ASTNode, MatchPattern, StatementNode, WaveType}; +use parser::generics::monomorphize_generics; +use parser::{parse_syntax_only, parse_syntax_with_spans}; + +#[test] +fn target_width_resolution_keeps_its_public_api_and_values() { + for (width, signed, unsigned) in [ + (32, WaveType::Int(32), WaveType::Uint(32)), + (64, WaveType::Int(64), WaveType::Uint(64)), + ] { + let tokens = Lexer::new("type Signed = isz; type Unsigned = usz;").tokenize().unwrap(); + let mut nodes = parse_syntax_only(&tokens).unwrap(); + resolve_target_types(&mut nodes, width).unwrap(); + let ASTNode::TypeAlias(s) = nodes[0].unspanned() else { panic!("signed alias") }; + let ASTNode::TypeAlias(u) = nodes[1].unspanned() else { panic!("unsigned alias") }; + assert_eq!(s.target, signed); + assert_eq!(u.target, unsigned); + assert!(resolve_target_types(&mut nodes, 16).is_err()); + } +} + +#[test] +fn nested_patterns_keep_depth_first_ids_and_source_spans() { + let source = + "variant V { A(i32), B } fun f(v: V) { match (v) { V::A(x) => { x; }, V::B => {} } }"; + let tokens = Lexer::new_with_file(source, "patterns.wave").tokenize().unwrap(); + let syntax = parse_syntax_with_spans(&tokens).unwrap(); + let program = TypedProgram::lower(monomorphize_generics(syntax).unwrap()).unwrap(); + + let ASTNode::Function(function) = &program.syntax()[1] else { panic!("function") }; + let ASTNode::Statement(StatementNode::Match { arms, .. }) = &function.body[0] else { + panic!("match"); + }; + let outer = &arms[0].pattern; + let MatchPattern::Variant { payloads, .. } = outer else { panic!("outer pattern") }; + let nested = &payloads[0]; + let sibling = &arms[1].pattern; + + let outer_id = program.pattern_id(outer).unwrap(); + let nested_id = program.pattern_id(nested).unwrap(); + let sibling_id = program.pattern_id(sibling).unwrap(); + assert_eq!(nested_id.index(), outer_id.index() + 1); + assert_eq!(sibling_id.index(), nested_id.index() + 1); + for (id, spelling) in [ + (outer_id, "V::A(x)"), + (nested_id, "x"), + (sibling_id, "V::B"), + ] { + let span = program.pattern_span(id).unwrap(); + assert_eq!(&source[span.start..span.end], spelling); + assert_eq!(span.file, "patterns.wave"); + } +} From 0af90cdf6f009c1f013d4e176aceeed8e43d86f3 Mon Sep 17 00:00:00 2001 From: hiyouwhatsapp <89997383+hiyouwhatsapp@users.noreply.github.com> Date: Wed, 4 Nov 2026 13:36:47 +0200 Subject: [PATCH 2/6] fix(hir): restore numeric checker imports and traversal access --- front/hir/src/numeric_checks.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/front/hir/src/numeric_checks.rs b/front/hir/src/numeric_checks.rs index 73584117..4465faee 100644 --- a/front/hir/src/numeric_checks.rs +++ b/front/hir/src/numeric_checks.rs @@ -2,7 +2,10 @@ //! Diagnose statically invalid checked numeric operations using the same ordered //! HIR conversions as runtime lowering. Mutable bindings are never propagated. use super::{conversions::*, *}; -use parser::ast::{FunctionNode, Literal, Mutability, Operator}; +use parser::ast::visit::{walk_expression, walk_node}; +use parser::ast::{FunctionNode, Literal, Mutability, Operator, StatementNode}; +use std::collections::HashSet; +use super::traversal::walk_pattern; use utils::const_int::ConstInt; /// Fully converted numeric value, independent of backend representation. @@ -498,7 +501,7 @@ impl Checker<'_> { self.inspect(value, scope)?; for arm in arms { let mut scope = scope.clone(); - super::walk_pattern(&arm.pattern, &mut |p| { + walk_pattern(&arm.pattern, &mut |p| { if let MatchPattern::Binding(name) = p { scope.insert(name.clone(), None); } @@ -509,11 +512,11 @@ impl Checker<'_> { _ => { let mut result = Ok(()); let mut visited = HashSet::new(); - super::walk_node(node, &mut |expr| { + walk_node(node, &mut |expr| { let id = self.program.expression_id(expr).unwrap(); if result.is_ok() && !visited.contains(&id) { result = self.inspect(expr, scope); - super::walk_expression(expr, &mut |child| { + walk_expression(expr, &mut |child| { visited.insert(self.program.expression_id(child).unwrap()); }); } @@ -557,7 +560,7 @@ pub(super) fn validate( // errors still follow short-circuiting and whose overflow policy is unchanged. for (index, node) in program.syntax().iter().enumerate() { let mut result = Ok(()); - super::walk_node(node, &mut |expr| { + walk_node(node, &mut |expr| { if result.is_ok() { result = checker.literal_range(expr); } @@ -599,7 +602,7 @@ pub(super) fn validate( checker.globals.insert(name, value); } else if visited.insert(name.clone()) { stack.push((name, true)); - super::walk_expression(expr, &mut |e| { + walk_expression(expr, &mut |e| { if let Expression::Variable(name) = e { if definitions.contains_key(name) { stack.push((name.clone(), false)); From d40b9435eb094dbb1ed7b16eac65eb31f88d09c7 Mon Sep 17 00:00:00 2001 From: hiyouwhatsapp <89997383+hiyouwhatsapp@users.noreply.github.com> Date: Wed, 4 Nov 2026 13:36:50 +0200 Subject: [PATCH 3/6] fix(hir): restore numeric checker imports and traversal access --- front/hir/src/traversal.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front/hir/src/traversal.rs b/front/hir/src/traversal.rs index 33217721..2600ead2 100644 --- a/front/hir/src/traversal.rs +++ b/front/hir/src/traversal.rs @@ -82,7 +82,7 @@ fn walk_patterns_in_statement(statement: &StatementNode, visit: &mut impl FnMut( } } -fn walk_pattern(pattern: &MatchPattern, visit: &mut impl FnMut(&MatchPattern)) { +pub(super) fn walk_pattern(pattern: &MatchPattern, visit: &mut impl FnMut(&MatchPattern)) { visit(pattern); if let MatchPattern::Variant { payloads, .. } = pattern { for payload in payloads { From 048aa20bbba80d4c9dad2f4c686ba1b230b518e8 Mon Sep 17 00:00:00 2001 From: hiyouwhatsapp <89997383+hiyouwhatsapp@users.noreply.github.com> Date: Wed, 4 Nov 2026 13:36:53 +0200 Subject: [PATCH 4/6] fix(hir): restore numeric checker imports and traversal access --- front/hir/tests/canonicalization_traversal.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/front/hir/tests/canonicalization_traversal.rs b/front/hir/tests/canonicalization_traversal.rs index e5ec09f5..5647ebc2 100644 --- a/front/hir/tests/canonicalization_traversal.rs +++ b/front/hir/tests/canonicalization_traversal.rs @@ -14,8 +14,12 @@ fn target_width_resolution_keeps_its_public_api_and_values() { let tokens = Lexer::new("type Signed = isz; type Unsigned = usz;").tokenize().unwrap(); let mut nodes = parse_syntax_only(&tokens).unwrap(); resolve_target_types(&mut nodes, width).unwrap(); - let ASTNode::TypeAlias(s) = nodes[0].unspanned() else { panic!("signed alias") }; - let ASTNode::TypeAlias(u) = nodes[1].unspanned() else { panic!("unsigned alias") }; + let ASTNode::TypeAlias(s) = nodes[0].unspanned() else { + panic!("signed alias") + }; + let ASTNode::TypeAlias(u) = nodes[1].unspanned() else { + panic!("unsigned alias") + }; assert_eq!(s.target, signed); assert_eq!(u.target, unsigned); assert!(resolve_target_types(&mut nodes, 16).is_err()); @@ -30,12 +34,16 @@ fn nested_patterns_keep_depth_first_ids_and_source_spans() { let syntax = parse_syntax_with_spans(&tokens).unwrap(); let program = TypedProgram::lower(monomorphize_generics(syntax).unwrap()).unwrap(); - let ASTNode::Function(function) = &program.syntax()[1] else { panic!("function") }; + let ASTNode::Function(function) = &program.syntax()[1] else { + panic!("function") + }; let ASTNode::Statement(StatementNode::Match { arms, .. }) = &function.body[0] else { panic!("match"); }; let outer = &arms[0].pattern; - let MatchPattern::Variant { payloads, .. } = outer else { panic!("outer pattern") }; + let MatchPattern::Variant { payloads, .. } = outer else { + panic!("outer pattern") + }; let nested = &payloads[0]; let sibling = &arms[1].pattern; From 7b5e27fce1f0e3bfeed037a7acb525c910f4fe23 Mon Sep 17 00:00:00 2001 From: hiyouwhatsapp <89997383+hiyouwhatsapp@users.noreply.github.com> Date: Wed, 4 Nov 2026 13:47:37 +0200 Subject: [PATCH 5/6] style(hir): sort numeric checker imports --- front/hir/src/numeric_checks.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/front/hir/src/numeric_checks.rs b/front/hir/src/numeric_checks.rs index 4465faee..09fb594b 100644 --- a/front/hir/src/numeric_checks.rs +++ b/front/hir/src/numeric_checks.rs @@ -1,11 +1,11 @@ // SPDX-License-Identifier: MPL-2.0 //! Diagnose statically invalid checked numeric operations using the same ordered //! HIR conversions as runtime lowering. Mutable bindings are never propagated. +use super::traversal::walk_pattern; use super::{conversions::*, *}; use parser::ast::visit::{walk_expression, walk_node}; use parser::ast::{FunctionNode, Literal, Mutability, Operator, StatementNode}; use std::collections::HashSet; -use super::traversal::walk_pattern; use utils::const_int::ConstInt; /// Fully converted numeric value, independent of backend representation. From 368fcbbf33c654693a34111e526665057a5f203f Mon Sep 17 00:00:00 2001 From: hiyouwhatsapp <89997383+hiyouwhatsapp@users.noreply.github.com> Date: Thu, 5 Nov 2026 17:42:22 +0200 Subject: [PATCH 6/6] style(hir): apply pinned rustfmt --- front/hir/src/lib.rs | 4 +-- front/hir/tests/canonicalization_traversal.rs | 29 +++++-------------- 2 files changed, 10 insertions(+), 23 deletions(-) diff --git a/front/hir/src/lib.rs b/front/hir/src/lib.rs index 351cc8c6..1a398e32 100644 --- a/front/hir/src/lib.rs +++ b/front/hir/src/lib.rs @@ -23,8 +23,8 @@ #![allow(clippy::result_large_err)] pub mod async_lower; -pub mod conversions; mod canonicalization; +pub mod conversions; mod defaults; pub mod enums; pub use enums::{EnumConversion, EnumType, EnumVariant, TypeId}; @@ -33,9 +33,9 @@ mod traversal; use canonicalization::canonicalize_syntax_types; pub use canonicalization::resolve_target_types; use conversions::{ConversionError, NumericExpressionInfo}; -use traversal::walk_patterns_in_nodes; pub use numeric_checks::{integer_literal_float, ConstantValue}; use parser::ast::visit::walk_nodes; +use traversal::walk_patterns_in_nodes; use parser::ast::{ASTNode, Expression, MatchPattern, WaveType}; use parser::verification::{analyze_semantic_facts, SemanticDiagnostic, SemanticFacts}; diff --git a/front/hir/tests/canonicalization_traversal.rs b/front/hir/tests/canonicalization_traversal.rs index 5647ebc2..6c5c6939 100644 --- a/front/hir/tests/canonicalization_traversal.rs +++ b/front/hir/tests/canonicalization_traversal.rs @@ -7,19 +7,14 @@ use parser::{parse_syntax_only, parse_syntax_with_spans}; #[test] fn target_width_resolution_keeps_its_public_api_and_values() { - for (width, signed, unsigned) in [ - (32, WaveType::Int(32), WaveType::Uint(32)), - (64, WaveType::Int(64), WaveType::Uint(64)), - ] { + for (width, signed, unsigned) in + [(32, WaveType::Int(32), WaveType::Uint(32)), (64, WaveType::Int(64), WaveType::Uint(64))] + { let tokens = Lexer::new("type Signed = isz; type Unsigned = usz;").tokenize().unwrap(); let mut nodes = parse_syntax_only(&tokens).unwrap(); resolve_target_types(&mut nodes, width).unwrap(); - let ASTNode::TypeAlias(s) = nodes[0].unspanned() else { - panic!("signed alias") - }; - let ASTNode::TypeAlias(u) = nodes[1].unspanned() else { - panic!("unsigned alias") - }; + let ASTNode::TypeAlias(s) = nodes[0].unspanned() else { panic!("signed alias") }; + let ASTNode::TypeAlias(u) = nodes[1].unspanned() else { panic!("unsigned alias") }; assert_eq!(s.target, signed); assert_eq!(u.target, unsigned); assert!(resolve_target_types(&mut nodes, 16).is_err()); @@ -34,16 +29,12 @@ fn nested_patterns_keep_depth_first_ids_and_source_spans() { let syntax = parse_syntax_with_spans(&tokens).unwrap(); let program = TypedProgram::lower(monomorphize_generics(syntax).unwrap()).unwrap(); - let ASTNode::Function(function) = &program.syntax()[1] else { - panic!("function") - }; + let ASTNode::Function(function) = &program.syntax()[1] else { panic!("function") }; let ASTNode::Statement(StatementNode::Match { arms, .. }) = &function.body[0] else { panic!("match"); }; let outer = &arms[0].pattern; - let MatchPattern::Variant { payloads, .. } = outer else { - panic!("outer pattern") - }; + let MatchPattern::Variant { payloads, .. } = outer else { panic!("outer pattern") }; let nested = &payloads[0]; let sibling = &arms[1].pattern; @@ -52,11 +43,7 @@ fn nested_patterns_keep_depth_first_ids_and_source_spans() { let sibling_id = program.pattern_id(sibling).unwrap(); assert_eq!(nested_id.index(), outer_id.index() + 1); assert_eq!(sibling_id.index(), nested_id.index() + 1); - for (id, spelling) in [ - (outer_id, "V::A(x)"), - (nested_id, "x"), - (sibling_id, "V::B"), - ] { + for (id, spelling) in [(outer_id, "V::A(x)"), (nested_id, "x"), (sibling_id, "V::B")] { let span = program.pattern_span(id).unwrap(); assert_eq!(&source[span.start..span.end], spelling); assert_eq!(span.file, "patterns.wave");