diff --git a/front/hir/src/lib.rs b/front/hir/src/lib.rs index 44a89a92..2667c13d 100644 --- a/front/hir/src/lib.rs +++ b/front/hir/src/lib.rs @@ -452,12 +452,40 @@ pub fn resolve_target_types(nodes: &mut [ASTNode], pointer_bits: u16) -> Result< } fn canonicalize_syntax_types(nodes: &mut [ASTNode]) { - let named = collect_named_types(nodes); + let mut named = collect_named_types(nodes); + compress_named_alias_chains(&mut named); for node in nodes { canonicalize_node_types(node, &named); } } +// Resolve flat alias chains once rather than rewalking every suffix for +// each declaration. Wrapped and generic types retain the normal canonicalizer. +fn compress_named_alias_chains(named: &mut HashMap) { + let names: Vec<_> = named.keys().cloned().collect(); + for name in names { + let mut path = Vec::new(); + let mut seen = HashSet::new(); + let mut current = name; + loop { + if !seen.insert(current.clone()) { + break; + } + let Some(WaveType::Struct(next)) = named.get(¤t) else { break }; + if !named.contains_key(next) { + break; + } + path.push(current); + current = next.clone(); + } + if let Some(terminal) = named.get(¤t).cloned() { + for alias in path { + named.insert(alias, terminal.clone()); + } + } + } +} + fn collect_named_types(nodes: &[ASTNode]) -> HashMap { let mut named = HashMap::new(); for node in nodes { @@ -845,3 +873,27 @@ fn walk_pattern(pattern: &MatchPattern, visit: &mut impl FnMut(&MatchPattern)) { } } } + +// A long acyclic chain is legal even though it is much deeper than Rust's +// ordinary call stack can handle if each alias triggers a recursive expansion. +#[cfg(test)] +mod alias_chain_tests { + use super::*; + + #[test] + fn compressed_named_aliases_reuse_a_long_resolved_suffix() { + let mut named = HashMap::new(); + for index in 0..16_383 { + named.insert(format!("A{index}"), WaveType::Struct(format!("A{}", index + 1))); + } + named.insert("A16383".into(), WaveType::Int(64)); + compress_named_alias_chains(&mut named); + for index in [0, 1, 4096, 8192, 16_383] { + assert_eq!(named[&format!("A{index}")], WaveType::Int(64)); + } + assert_eq!( + canonical_type(&WaveType::Struct("A0".into()), &named, &mut HashSet::new()), + WaveType::Int(64) + ); + } +} diff --git a/front/hir/tests/alias_chains.rs b/front/hir/tests/alias_chains.rs new file mode 100644 index 00000000..5ae2dc95 --- /dev/null +++ b/front/hir/tests/alias_chains.rs @@ -0,0 +1,54 @@ +//! Long acyclic aliases must not exhaust the Rust stack during frontend lowering. +use hir::TypedProgram; +use lexer::Lexer; +use parser::{ + ast::{ASTNode, WaveType}, + parse_syntax_with_spans, +}; + +fn lower_chain(count: usize, reverse: bool) -> TypedProgram { + let mut source = String::new(); + for index in 0..count { + let declaration = if index == count - 1 { + format!("type A{index} = i32;\n") + } else { + format!("type A{index} = A{};\n", index + 1) + }; + if reverse { + source.insert_str(0, &declaration); + } else { + source.push_str(&declaration); + } + } + source.push_str("fun main() { var value: A0 = 0; }\n"); + let tokens = Lexer::new(&source).tokenize().unwrap(); + TypedProgram::lower(parse_syntax_with_spans(&tokens).unwrap()) + .unwrap_or_else(|e| panic!("{} aliases, reverse={reverse}: {e:?}", count)) +} + +#[test] +fn forward_and_backward_alias_chains_lower_to_concrete_types() { + for reverse in [false, true] { + let program = lower_chain(2048, reverse); + let ASTNode::Function(function) = &program.syntax()[2048] else { + panic!("missing function"); + }; + let ASTNode::Variable(variable) = &function.body[0] else { + panic!("missing variable"); + }; + assert_eq!(variable.type_name, WaveType::Int(32)); + } +} + +#[test] +fn recursive_aliases_keep_a_normal_semantic_error() { + for source in [ + "type A = A; fun main() {}", + "type A = B; type B = A; fun main() {}", + "type A = ptr; type B = A; fun main() {}", + ] { + let tokens = Lexer::new(source).tokenize().unwrap(); + let error = TypedProgram::lower(parse_syntax_with_spans(&tokens).unwrap()).unwrap_err(); + assert!(error.diagnostic().message.contains("cyclic type alias"), "{error:?}"); + } +} diff --git a/front/parser/src/verification.rs b/front/parser/src/verification.rs index a4ec1a2a..38eb85be 100644 --- a/front/parser/src/verification.rs +++ b/front/parser/src/verification.rs @@ -2580,37 +2580,48 @@ fn validate_alias_cycle( 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)); + // An alias declaration has one nested type. Walk the named dependency chain + // without a Rust call frame per alias; the syntax nesting limit still + // bounds Future, pointer and array wrappers within each declaration. + let mut current = alias; + let mut path = Vec::new(); + loop { + if checked.contains(current) { + break; + } + if !active.insert(current.to_string()) { + return Err(format!("cyclic type alias involving `{}`", current)); + } + path.push(current.to_string()); + + let next = program.aliases.get(current).and_then(|target| { + let mut ty = target; + loop { + match ty { + WaveType::Struct(name) if program.aliases.contains_key(name) => { + break Some(name.as_str()); + }, + WaveType::Future(inner) + | WaveType::Pointer(inner) + | WaveType::Array(inner, _) => { + ty = inner; + }, + _ => break None, + } + } + }); + match next { + Some(name) => current = name, + None => break, + } } - if let Some(target) = program.aliases.get(alias) { - validate_alias_type_cycle(target, program, active, checked)?; + for name in path { + active.remove(&name); + checked.insert(name); } - 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::Future(inner) | 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 { @@ -2660,3 +2671,29 @@ fn analyzed_expression_type(program: &ProgramTypes, ty: &ExpressionType) -> Anal ExpressionType::Unknown => AnalyzedExpressionType::Unknown, } } + +#[cfg(test)] +mod alias_cycle_tests { + use super::*; + + #[test] + fn long_alias_dependency_checks_do_not_recurse() { + let mut program = ProgramTypes::default(); + for index in 0..16_383 { + program + .aliases + .insert(format!("A{index}"), WaveType::Struct(format!("A{}", index + 1))); + } + program.aliases.insert("A16383".into(), WaveType::Int(32)); + let mut checked = HashSet::new(); + validate_alias_cycle("A0", &program, &mut HashSet::new(), &mut checked).unwrap(); + assert_eq!(checked.len(), 16_384); + + program + .aliases + .insert("A16383".into(), WaveType::Pointer(Box::new(WaveType::Struct("A0".into())))); + let error = validate_alias_cycle("A0", &program, &mut HashSet::new(), &mut HashSet::new()) + .unwrap_err(); + assert!(error.contains("cyclic type alias"), "{error}"); + } +} diff --git a/front/parser/src/verification/program.rs b/front/parser/src/verification/program.rs index 1353c34e..bfccd0f1 100644 --- a/front/parser/src/verification/program.rs +++ b/front/parser/src/verification/program.rs @@ -458,34 +458,52 @@ impl ProgramTypes { 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(","); - let name = format!("{}<{}>", base, arguments); - if self.variants.contains_key(&base) { - WaveType::Variant(name) - } else { - WaveType::Struct(name) + WaveType::Struct(_) => { + // Common forward-alias chains may be thousands of declarations + // long even though every individual type is syntactically flat. + let mut target = ty; + let mut visited = Vec::new(); + loop { + let WaveType::Struct(name) = target else { break }; + if !seen.insert(name.clone()) { + for name in visited { + seen.remove(&name); + } + return target.clone(); } - } else if self.variants.contains_key(name) { - WaveType::Variant(name.clone()) - } else { - ty.clone() + visited.push(name.clone()); + match self.aliases.get(name).or_else(|| self.enum_reprs.get(name)) { + Some(next) => target = next, + None => break, + } + } + let resolved = match target { + WaveType::Struct(name) => { + 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(","); + let name = format!("{}<{}>", base, arguments); + if self.variants.contains_key(&base) { + WaveType::Variant(name) + } else { + WaveType::Struct(name) + } + } else if self.variants.contains_key(name) { + WaveType::Variant(name.clone()) + } else { + target.clone() + } + }, + _ => self.canonical_type_inner(target, seen), }; - seen.remove(name); + for name in visited { + seen.remove(&name); + } resolved }, WaveType::Future(inner) => { @@ -638,3 +656,24 @@ pub(super) fn substitute_function_type( variadic: signature.variadic, } } + +#[cfg(test)] +mod alias_chain_tests { + use super::*; + + #[test] + fn long_forward_alias_chain_canonicalizes_without_recursive_frames() { + let mut program = ProgramTypes::default(); + for index in 0..16_383 { + program + .aliases + .insert(format!("A{index}"), WaveType::Struct(format!("A{}", index + 1))); + } + program.aliases.insert("A16383".into(), WaveType::Int(32)); + assert_eq!(program.canonical_type(&WaveType::Struct("A0".into())), WaveType::Int(32)); + assert_eq!( + program.canonical_type(&WaveType::Pointer(Box::new(WaveType::Struct("A0".into())))), + WaveType::Pointer(Box::new(WaveType::Int(32))) + ); + } +}