Convert issue8656 to type-checker test

Move the regression test from a CLI test to a type-checker unit test
since it only tests type checking behavior.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Richard Feldman 2025-12-13 10:30:32 -05:00
parent 930fd0865e
commit d0d53b20ef
No known key found for this signature in database
4 changed files with 92 additions and 85 deletions

View file

@ -43,4 +43,5 @@ test "check tests" {
std.testing.refAllDecls(@import("test/instantiate_tag_union_test.zig"));
std.testing.refAllDecls(@import("test/where_clause_test.zig"));
std.testing.refAllDecls(@import("test/recursive_alias_test.zig"));
std.testing.refAllDecls(@import("test/generalize_redirect_test.zig"));
}

View file

@ -0,0 +1,91 @@
//! Regression test for issue #8656: rank panic when variable redirects to higher-rank variable.
//!
//! The original bug was: when a variable added to the var_pool at rank 1
//! was later redirected (via setVarRedirect) to a variable at rank 2,
//! generalization would try to add the resolved variable at rank 2 to the
//! tmp_var_pool which only goes up to rank 1, causing a panic.
const std = @import("std");
const TestEnv = @import("./TestEnv.zig");
test "nested lambda with higher-rank variables does not panic during generalization" {
// This code structure triggered the bug in the original report.
// It involves nested lambdas that create rank-2 variables, pattern matching
// on tuples, and recursive functions.
//
// The key pattern is:
// 1. A function with a nested lambda (ret) that creates rank-2 type variables
// 2. Pattern matching on tuples that exercises the type checker
// 3. Recursive calls that can cause variable redirects across ranks
const source =
\\{
\\ Maybe(t) : [
\\ Some(t),
\\ None,
\\ ]
\\
\\ TokenContents : [
\\ NewlineToken,
\\ SymbolsToken(Str),
\\ SnakeCaseIdentToken(Str),
\\ EndOfFileToken,
\\ ]
\\
\\ TokenizerResult : (
\\ Try(TokenContents, Str),
\\ U64,
\\ U64,
\\ )
\\
\\ get_next_token : List(U8), U64 -> TokenizerResult
\\ get_next_token = |file, index| {
\\ match List.get(file, index) {
\\ Ok('\n') => (Ok(NewlineToken), index, index + 1)
\\ Err(_) => (Ok(EndOfFileToken), index, index)
\\ }
\\ }
\\
\\ tokenize_identifier = |file, index, acc, start_index| {
\\ char = List.get(file, index)
\\ ret = || {
\\ match Str.from_utf8(acc) {
\\ Ok(str) => (Ok(SnakeCaseIdentToken(str)), start_index, index)
\\ Err(_) => (Err("Invalid UTF8"), start_index, index)
\\ }
\\ }
\\ match char {
\\ Ok(c) => {
\\ if ('a' <= c and c <= 'z') or ('A' <= c and c <= 'Z') or (c == '_') {
\\ tokenize_identifier(file, index + 1, List.append(acc, c), start_index)
\\ } else {
\\ ret()
\\ }
\\ }
\\ _ => ret()
\\ }
\\ }
\\
\\ parse_pattern = |file, tokenizer_result| {
\\ (token, _, index) = tokenizer_result
\\ match token {
\\ Ok(SnakeCaseIdentToken(ident)) => {
\\ match get_next_token(file, index) {
\\ (Ok(SymbolsToken(":")), _, index2) => Ok((ident, Some("type"), index2))
\\ _ => Ok((ident, None, index))
\\ }
\\ }
\\ _ => Err("expected pattern")
\\ }
\\ }
\\
\\ parse_pattern
\\}
;
var test_env = try TestEnv.initExpr("Test", source);
defer test_env.deinit();
// If we get here without panicking, the test passes.
// The bug would cause a panic during type checking with:
// "trying to add var at rank 2, but current rank is 1"
try test_env.assertNoErrors();
}

View file

@ -238,11 +238,6 @@ pub const io_spec_tests = [_]TestSpec{
.io_spec = "0<short|1>short|0<|1>",
.description = "Regression test: Stdin.line! in while loop with short input (small string optimization)",
},
.{
.roc_file = "test/fx/issue8656.roc",
.io_spec = "1>ok",
.description = "Regression test: rank panic when variable redirects to higher-rank variable during generalization",
},
};
/// Get the total number of IO spec tests

View file

@ -1,80 +0,0 @@
app [main!] { pf: platform "./platform/main.roc" }
import pf.Stdout
# Regression test for issue #8656
# Tests that generalization handles variables that redirect to higher-rank
# variables without panicking.
#
# The original bug was: when a variable added to the var_pool at rank 1
# was later redirected (via setVarRedirect) to a variable at rank 2,
# generalization would try to add the resolved variable at rank 2 to the
# tmp_var_pool which only goes up to rank 1, causing a panic.
main! = || {
Stdout.line!("ok")
}
# The following code structure triggered the bug in the original report.
# It involves complex type definitions with nested tuples and recursive functions.
Maybe(t) : [
Some(t),
None,
]
TokenContents : [
NewlineToken,
SymbolsToken(Str),
SnakeCaseIdentToken(Str),
EndOfFileToken,
]
TokenizerResult : (
Try(TokenContents, Str),
U64,
U64,
)
get_next_token : List(U8), U64 -> TokenizerResult
get_next_token = |file, index| {
match List.get(file, index) {
Ok('\n') => (Ok(NewlineToken), index, index + 1)
Err(_) => (Ok(EndOfFileToken), index, index)
}
}
# This function has a nested lambda (ret) that creates rank-2 variables
tokenize_identifier = |file, index, acc, start_index| {
char = List.get(file, index)
ret = || {
match Str.from_utf8(acc) {
Ok(str) => (Ok(SnakeCaseIdentToken(str)), start_index, index)
Err(_) => (Err("Invalid UTF8"), start_index, index)
}
}
match char {
Ok(c) => {
if ('a' <= c and c <= 'z') or ('A' <= c and c <= 'Z') or (c == '_') {
tokenize_identifier(file, index + 1, List.append(acc, c), start_index)
} else {
ret()
}
}
_ => ret()
}
}
# This function with pattern matching on tuples exercises the type checker
parse_pattern = |file, tokenizer_result| {
(token, _, index) = tokenizer_result
match token {
Ok(SnakeCaseIdentToken(ident)) => {
match get_next_token(file, index) {
(Ok(SymbolsToken(":")), _, index2) => Ok((ident, Some("type"), index2))
_ => Ok((ident, None, index))
}
}
_ => Err("expected pattern")
}
}