Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 53 additions & 1 deletion front/hir/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, WaveType>) {
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(&current) else { break };
if !named.contains_key(next) {
break;
}
path.push(current);
current = next.clone();
}
if let Some(terminal) = named.get(&current).cloned() {
for alias in path {
named.insert(alias, terminal.clone());
}
}
}
}

fn collect_named_types(nodes: &[ASTNode]) -> HashMap<String, WaveType> {
let mut named = HashMap::new();
for node in nodes {
Expand Down Expand Up @@ -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)
);
}
}
54 changes: 54 additions & 0 deletions front/hir/tests/alias_chains.rs
Original file line number Diff line number Diff line change
@@ -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<B>; 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:?}");
}
}
89 changes: 63 additions & 26 deletions front/parser/src/verification.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2580,37 +2580,48 @@ fn validate_alias_cycle(
active: &mut HashSet<String>,
checked: &mut HashSet<String>,
) -> 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<String>,
checked: &mut HashSet<String>,
) -> 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 {
Expand Down Expand Up @@ -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}");
}
}
91 changes: 65 additions & 26 deletions front/parser/src/verification/program.rs
Original file line number Diff line number Diff line change
Expand Up @@ -458,34 +458,52 @@ impl ProgramTypes {
seen: &mut HashSet<String>,
) -> 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::<Vec<_>>()
.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::<Vec<_>>()
.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),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P1] The iterative walk ends at a wrapped target and re-enters the recursive canonicalizer here. A chain of 8,192 shallow declarations type A0 = ptr<A1>; ... still aborts during frontend/HIR lowering with stack overflow, while the flat 16,384-alias control passes. The review body provides the complete bounded reproduction. Please cover wrapped alias graphs before treating #1068 as completed; this is a remaining failure, not an alleged new regression.

};
seen.remove(name);
for name in visited {
seen.remove(&name);
}
resolved
},
WaveType::Future(inner) => {
Expand Down Expand Up @@ -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)))
);
}
}
Loading