From 9a9758b3931d903be9421bc35db3aeddae2cfc6b Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Sat, 13 Dec 2025 15:37:22 -0500 Subject: [PATCH 1/4] Fix panic when using polymorphic numeric as list index MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a polymorphic numeric type (like from an unannotated `[0]` list) was used as an index for List.get, the interpreter would panic with "reached unreachable code". This happened because: 1. Polymorphic numerics default to Dec (decimal) layout at runtime 2. list_get_unsafe used asI128() which asserts the layout is integer 3. But the value had a frac.dec layout, so the assertion failed The fix adds extractIndexAsI128() helper that properly handles both integer and fractional layouts when extracting a list index value. For decimals, it divides by the scale factor (10^18) to get the integer part. Fixes #8666 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/cli/test/fx_test_specs.zig | 5 +++++ src/eval/interpreter.zig | 36 +++++++++++++++++++++++++++++++++- test/fx/issue8666.roc | 15 ++++++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 test/fx/issue8666.roc diff --git a/src/cli/test/fx_test_specs.zig b/src/cli/test/fx_test_specs.zig index 75ef1396bb..223a288899 100644 --- a/src/cli/test/fx_test_specs.zig +++ b/src/cli/test/fx_test_specs.zig @@ -238,6 +238,11 @@ pub const io_spec_tests = [_]TestSpec{ .io_spec = "0short|0<|1>", .description = "Regression test: Stdin.line! in while loop with short input (small string optimization)", }, + .{ + .roc_file = "test/fx/issue8666.roc", + .io_spec = "1>ok", + .description = "Regression test: List.get with inferred index type in for loop", + }, }; /// Get the total number of IO spec tests diff --git a/src/eval/interpreter.zig b/src/eval/interpreter.zig index ac75cdc4dc..661011fa4f 100644 --- a/src/eval/interpreter.zig +++ b/src/eval/interpreter.zig @@ -2414,7 +2414,10 @@ pub const Interpreter = struct { std.debug.assert(list_arg.layout.tag == .list or list_arg.layout.tag == .list_of_zst); // low-level .list_get_unsafe expects list layout const roc_list: *const builtins.list.RocList = @ptrCast(@alignCast(list_arg.ptr.?)); - const index = index_arg.asI128(); // U64 stored as i128 + // Extract the index as an integer. Handle both integer and decimal types, + // as polymorphic numeric types may default to Dec at runtime even when + // used in an integer context (e.g., for loop iteration variable). + const index = self.extractIndexAsI128(index_arg); // Get element layout const elem_layout_idx = list_arg.layout.data.list; @@ -6033,6 +6036,37 @@ pub const Interpreter = struct { }; } + /// Extract an integer value from a numeric StackValue. + /// Handles both integer layouts (returns value directly) and fractional layouts + /// (converts Dec/F32/F64 to integer). This is needed when polymorphic numeric types + /// default to Dec at runtime but are used in integer contexts (e.g., list indices). + fn extractIndexAsI128(_: *Interpreter, value: StackValue) i128 { + std.debug.assert(value.layout.tag == .scalar); + const scalar = value.layout.data.scalar; + return switch (scalar.tag) { + .int => value.asI128(), + .frac => switch (scalar.data.frac) { + .dec => { + const raw_ptr = value.ptr orelse unreachable; + const ptr = @as(*const RocDec, @ptrCast(@alignCast(raw_ptr))); + // Convert Dec to integer by dividing by the scale factor (10^18) + return @divTrunc(ptr.num, RocDec.one_point_zero_i128); + }, + .f32 => { + const raw_ptr = value.ptr orelse unreachable; + const ptr = @as(*const f32, @ptrCast(@alignCast(raw_ptr))); + return @intFromFloat(ptr.*); + }, + .f64 => { + const raw_ptr = value.ptr orelse unreachable; + const ptr = @as(*const f64, @ptrCast(@alignCast(raw_ptr))); + return @intFromFloat(ptr.*); + }, + }, + else => unreachable, + }; + } + fn compareNumericScalars(self: *Interpreter, lhs: StackValue, rhs: StackValue) !std.math.Order { const lhs_value = try self.extractNumericValue(lhs); const rhs_value = try self.extractNumericValue(rhs); diff --git a/test/fx/issue8666.roc b/test/fx/issue8666.roc new file mode 100644 index 0000000000..17b9441dac --- /dev/null +++ b/test/fx/issue8666.roc @@ -0,0 +1,15 @@ +app [main!] { pf: platform "./platform/main.roc" } + +import pf.Stdout + +# Reproducer for issue 8666: +# Compiler panics when accessing list elements with inferred index types in for loops + +main! = || { + list = [""] + indices = [0] + for i in indices { + _x = List.get(list, i) + } + Stdout.line!("ok") +} From e5279897779c4604cfff9336c486e3065a47a9f3 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Sat, 13 Dec 2025 22:53:23 -0500 Subject: [PATCH 2/4] Move regression test from fx to eval MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The fix is in the interpreter, so an eval test is sufficient and runs faster than an fx test. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/cli/test/fx_test_specs.zig | 5 ----- src/eval/test/eval_test.zig | 24 ++++++++++++++++++++++++ test/fx/issue8666.roc | 15 --------------- 3 files changed, 24 insertions(+), 20 deletions(-) delete mode 100644 test/fx/issue8666.roc diff --git a/src/cli/test/fx_test_specs.zig b/src/cli/test/fx_test_specs.zig index 223a288899..75ef1396bb 100644 --- a/src/cli/test/fx_test_specs.zig +++ b/src/cli/test/fx_test_specs.zig @@ -238,11 +238,6 @@ pub const io_spec_tests = [_]TestSpec{ .io_spec = "0short|0<|1>", .description = "Regression test: Stdin.line! in while loop with short input (small string optimization)", }, - .{ - .roc_file = "test/fx/issue8666.roc", - .io_spec = "1>ok", - .description = "Regression test: List.get with inferred index type in for loop", - }, }; /// Get the total number of IO spec tests diff --git a/src/eval/test/eval_test.zig b/src/eval/test/eval_test.zig index e89b30290b..e54604a6cb 100644 --- a/src/eval/test/eval_test.zig +++ b/src/eval/test/eval_test.zig @@ -1406,3 +1406,27 @@ test "List.len returns proper U64 nominal type for method calls - regression" { \\} , "3", .no_trace); } + +test "List.get with polymorphic numeric index from for loop - regression" { + // Regression test for GitHub issue #8666: interpreter panic when using + // a polymorphic numeric type (from an unannotated list) as a list index. + // + // The bug occurred because: + // 1. Polymorphic numerics default to Dec layout at runtime + // 2. list_get_unsafe used asI128() which asserts the layout is integer + // 3. But the value had a frac.dec layout, causing the assertion to fail + // + // Using List.first to exercise list_get_unsafe with a polymorphic index + // (indices list is unannotated so element type is polymorphic Num) + try runExpectInt( + \\{ + \\ list = [10, 20, 30] + \\ indices = [0] + \\ var result = 0 + \\ for i in indices { + \\ result = match List.get(list, i) { Ok(v) => v, _ => 0 } + \\ } + \\ result + \\} + , 10, .no_trace); +} diff --git a/test/fx/issue8666.roc b/test/fx/issue8666.roc deleted file mode 100644 index 17b9441dac..0000000000 --- a/test/fx/issue8666.roc +++ /dev/null @@ -1,15 +0,0 @@ -app [main!] { pf: platform "./platform/main.roc" } - -import pf.Stdout - -# Reproducer for issue 8666: -# Compiler panics when accessing list elements with inferred index types in for loops - -main! = || { - list = [""] - indices = [0] - for i in indices { - _x = List.get(list, i) - } - Stdout.line!("ok") -} From f26fedb44468ff08f453b8e32ca6b89ce27fcbb2 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Sun, 14 Dec 2025 21:44:17 -0500 Subject: [PATCH 3/4] Fix List.get with polymorphic numeric index from for loop (issue #8666) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a numeric literal like `0` is used in a polymorphic context (e.g., `[0]`), its type may be a flex var that defaults to Dec during evaluation. Later, when this value is used as an index to List.get (which expects U64), the type gets unified to U64 but the value's layout is already Dec. This caused a panic because asI128() asserted the layout was an integer. The fix adds extractIndexAsI128() helper that handles both int and frac layouts to extract the integer value correctly, converting Dec values by dividing by the scaling factor. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/eval/interpreter.zig | 82 +++++++++++++++++++++++----------------- 1 file changed, 47 insertions(+), 35 deletions(-) diff --git a/src/eval/interpreter.zig b/src/eval/interpreter.zig index 9cd52be2ee..e9edd1b089 100644 --- a/src/eval/interpreter.zig +++ b/src/eval/interpreter.zig @@ -1126,6 +1126,49 @@ pub const Interpreter = struct { return result_value; } + /// Extract an integer index value from a StackValue, handling the case where + /// the value has a Dec layout due to polymorphic type inference (GitHub issue #8666). + /// + /// When a numeric literal like `0` is used in a polymorphic context (e.g., `[0]`), + /// its type may be a flex var that defaults to Dec during evaluation. Later, when + /// this value is used as an index to List.get (which expects U64), the type gets + /// unified to U64 but the value's layout is already Dec. This function handles + /// both int and frac layouts to extract the integer value correctly. + fn extractIndexAsI128(index_arg: StackValue) i128 { + std.debug.assert(index_arg.is_initialized); + std.debug.assert(index_arg.ptr != null); + std.debug.assert(index_arg.layout.tag == .scalar); + + const scalar = index_arg.layout.data.scalar; + switch (scalar.tag) { + .int => { + // Normal case: integer layout + return index_arg.asI128(); + }, + .frac => { + // Handle Dec/frac layout by converting to integer + // This happens when polymorphic numerics default to Dec before type unification + const raw_ptr = @as([*]u8, @ptrCast(index_arg.ptr.?)); + switch (scalar.data.frac) { + .dec => { + // Dec stores value * 10^18, so divide to get the integer + const dec_ptr: *const RocDec = @ptrCast(@alignCast(raw_ptr)); + return @divTrunc(dec_ptr.num, RocDec.one_point_zero_i128); + }, + .f32 => { + const f32_ptr: *const f32 = @ptrCast(@alignCast(raw_ptr)); + return @intFromFloat(f32_ptr.*); + }, + .f64 => { + const f64_ptr: *const f64 = @ptrCast(@alignCast(raw_ptr)); + return @intFromFloat(f64_ptr.*); + }, + } + }, + else => unreachable, // Indices should always be numeric + } + } + /// Version of callLowLevelBuiltin that also accepts a target type for operations like num_from_numeral pub fn callLowLevelBuiltinWithTargetType(self: *Interpreter, op: can.CIR.Expr.LowLevel, args: []StackValue, roc_ops: *RocOps, return_rt_var: ?types.Var, target_type_var: ?types.Var) !StackValue { // For num_from_numeral, we need to pass the target type through a different mechanism @@ -2414,10 +2457,10 @@ pub const Interpreter = struct { std.debug.assert(list_arg.layout.tag == .list or list_arg.layout.tag == .list_of_zst); // low-level .list_get_unsafe expects list layout const roc_list: *const builtins.list.RocList = @ptrCast(@alignCast(list_arg.ptr.?)); - // Extract the index as an integer. Handle both integer and decimal types, - // as polymorphic numeric types may default to Dec at runtime even when - // used in an integer context (e.g., for loop iteration variable). - const index = self.extractIndexAsI128(index_arg); + // Extract index as integer. Handle the case where the index has a Dec layout + // due to type inference defaulting polymorphic numbers to Dec before the type + // was unified to U64 (GitHub issue #8666). + const index = extractIndexAsI128(index_arg); // Get element layout const elem_layout_idx = list_arg.layout.data.list; @@ -6142,37 +6185,6 @@ pub const Interpreter = struct { }; } - /// Extract an integer value from a numeric StackValue. - /// Handles both integer layouts (returns value directly) and fractional layouts - /// (converts Dec/F32/F64 to integer). This is needed when polymorphic numeric types - /// default to Dec at runtime but are used in integer contexts (e.g., list indices). - fn extractIndexAsI128(_: *Interpreter, value: StackValue) i128 { - std.debug.assert(value.layout.tag == .scalar); - const scalar = value.layout.data.scalar; - return switch (scalar.tag) { - .int => value.asI128(), - .frac => switch (scalar.data.frac) { - .dec => { - const raw_ptr = value.ptr orelse unreachable; - const ptr = @as(*const RocDec, @ptrCast(@alignCast(raw_ptr))); - // Convert Dec to integer by dividing by the scale factor (10^18) - return @divTrunc(ptr.num, RocDec.one_point_zero_i128); - }, - .f32 => { - const raw_ptr = value.ptr orelse unreachable; - const ptr = @as(*const f32, @ptrCast(@alignCast(raw_ptr))); - return @intFromFloat(ptr.*); - }, - .f64 => { - const raw_ptr = value.ptr orelse unreachable; - const ptr = @as(*const f64, @ptrCast(@alignCast(raw_ptr))); - return @intFromFloat(ptr.*); - }, - }, - else => unreachable, - }; - } - fn compareNumericScalars(self: *Interpreter, lhs: StackValue, rhs: StackValue) !std.math.Order { const lhs_value = try self.extractNumericValue(lhs); const rhs_value = try self.extractNumericValue(rhs); From b7c9c16f9fd7b8fa5fd69448fe026cc77c2629f9 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Sun, 14 Dec 2025 22:38:11 -0500 Subject: [PATCH 4/4] Fix polymorphic numeric let-generalization (issue #8666) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The root cause was that numeric literals with `from_numeral` constraints were being generalized (let-polymorphism), causing each lookup to create a fresh instantiation. This meant constraints from later usage (like List.get expecting U64) didn't propagate back to the original definition, leaving the value as an unconstrained flex var that defaulted to Dec. Fix: 1. In generalize.zig: Don't generalize flex vars with `from_numeral` constraints at ANY rank (not just top_level) 2. In Check.zig: Don't instantiate during lookup if the var has a `from_numeral` constraint - unify directly instead This aligns with the design that let-generalization should only work for things that are syntactically lambdas (e.g. `foo = |arg| ...`). Also reverts the interpreter workaround - the proper fix is in the type checker, not working around type system bugs in the interpreter. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- src/check/Check.zig | 25 +++++++-- src/check/test/num_type_inference_test.zig | 54 +++++++++++++++++++ src/check/test/type_checking_integration.zig | 8 +-- src/eval/interpreter.zig | 48 +---------------- src/eval/test/eval_test.zig | 24 ++++----- src/types/generalize.zig | 17 +++--- test/snapshots/let_polymorphism_complex.md | 12 ++--- .../nominal_associated_deep_nesting.md | 4 +- .../nominal/nominal_associated_lookup_decl.md | 4 +- .../nominal_associated_lookup_nested.md | 4 +- .../nominal/nominal_associated_value_alias.md | 4 +- .../numeric_let_generalize_in_block.md | 21 ++++++-- .../repl/numeric_multiple_diff_types.md | 4 +- 13 files changed, 132 insertions(+), 97 deletions(-) diff --git a/src/check/Check.zig b/src/check/Check.zig index 5fe9d256c5..e5d9302d28 100644 --- a/src/check/Check.zig +++ b/src/check/Check.zig @@ -1258,7 +1258,7 @@ fn checkDef(self: *Self, def_idx: CIR.Def.Idx, env: *Env) std.mem.Allocator.Erro _ = try self.checkExpr(def.expr, env, .no_expectation); } - // Now that we are existing the scope, we must generalize then pop this rank + // Now that we are exiting the scope, we must generalize then pop this rank try self.generalizer.generalize(self.gpa, &env.var_pool, env.rank()); // Check any accumulated static dispatch constraints @@ -2898,9 +2898,28 @@ fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx, env: *Env, expected: Expected) } const pat_var = ModuleEnv.varFrom(lookup.pattern_idx); - const resolved_pat = self.types.resolveVar(pat_var).desc; + const resolved_pat = self.types.resolveVar(pat_var); - if (resolved_pat.rank == Rank.generalized) { + // Check if this is a generalized var that should NOT be instantiated. + // Numeric literals with from_numeral constraints should unify directly + // so that the concrete type propagates back to the definition site. + // This fixes GitHub issue #8666 where polymorphic numerics defaulted to Dec. + const should_instantiate = blk: { + if (resolved_pat.desc.rank != Rank.generalized) break :blk false; + // Don't instantiate if this has a from_numeral constraint + if (resolved_pat.desc.content == .flex) { + const flex = resolved_pat.desc.content.flex; + const constraints = self.types.sliceStaticDispatchConstraints(flex.constraints); + for (constraints) |constraint| { + if (constraint.origin == .from_numeral) { + break :blk false; + } + } + } + break :blk true; + }; + + if (should_instantiate) { const instantiated = try self.instantiateVar(pat_var, env, .use_last_var); _ = try self.unify(expr_var, instantiated, env); } else { diff --git a/src/check/test/num_type_inference_test.zig b/src/check/test/num_type_inference_test.zig index 431c01b2de..14ef13a494 100644 --- a/src/check/test/num_type_inference_test.zig +++ b/src/check/test/num_type_inference_test.zig @@ -200,3 +200,57 @@ test "numeric literal in comparison unifies with typed operand" { } try testing.expect(found_result); } + +test "polymorphic numeric in list used as List.get index unifies to U64 - regression #8666" { + // When a numeric literal is stored in an unannotated list and later used as + // an index to List.get (which takes U64), the type should unify to U64. + // This is a regression test for GitHub issue #8666 where the type remained + // as a flex var, causing the interpreter to default it to Dec layout. + const source = + \\list = [10, 20, 30] + \\index = 0 + \\result = List.get(list, index) + ; + + var test_env = try TestEnv.init("Test", source); + defer test_env.deinit(); + + // First verify no type errors + try test_env.assertNoErrors(); + + // The key assertion: `index` should be U64 after unification with List.get's parameter. + // Find the `index` definition and check its type. + const ModuleEnv = @import("can").ModuleEnv; + const defs_slice = test_env.module_env.store.sliceDefs(test_env.module_env.all_defs); + var found_index = false; + for (defs_slice) |def_idx| { + const def = test_env.module_env.store.getDef(def_idx); + const ptrn = test_env.module_env.store.getPattern(def.pattern); + if (ptrn == .assign) { + const def_name = test_env.module_env.getIdentStoreConst().getText(ptrn.assign.ident); + if (std.mem.eql(u8, def_name, "index")) { + found_index = true; + + // Get the type from the expression (the literal 0) + const expr_var = ModuleEnv.varFrom(def.expr); + try test_env.type_writer.write(expr_var); + const expr_type = test_env.type_writer.get(); + + // After unification with List.get's U64 parameter, should be U64 + try testing.expectEqualStrings("U64", expr_type); + + // Also verify the pattern has the same type + const pattern_var = ModuleEnv.varFrom(def.pattern); + try test_env.type_writer.write(pattern_var); + const pattern_type = test_env.type_writer.get(); + try testing.expectEqualStrings("U64", pattern_type); + + // Verify the pattern is NOT generalized (numeric literals shouldn't be) + const resolved_pat = test_env.module_env.types.resolveVar(pattern_var); + try testing.expect(resolved_pat.desc.rank != types.Rank.generalized); + break; + } + } + } + try testing.expect(found_index); +} diff --git a/src/check/test/type_checking_integration.zig b/src/check/test/type_checking_integration.zig index 754a59e3aa..b36194d3ac 100644 --- a/src/check/test/type_checking_integration.zig +++ b/src/check/test/type_checking_integration.zig @@ -1353,10 +1353,10 @@ test "check type - expect" { \\ x \\} ; - // Inside lambdas, numeric flex vars ARE generalized (to support polymorphic functions). - // Each use of `x` gets a fresh instance, so constraints from `x == 1` don't - // propagate to the generalized type. Only `from_numeral` from the def is captured. - try checkTypesModule(source, .{ .pass = .last_def }, "a where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])]"); + // Numeric literals with from_numeral constraints are NOT generalized (GitHub #8666). + // This means constraints from `x == 1` (the is_eq constraint) DO propagate back + // to the definition of x, along with the original from_numeral constraint. + try checkTypesModule(source, .{ .pass = .last_def }, "a where [a.is_eq : a, a -> Bool, a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])]"); } test "check type - expect not bool" { diff --git a/src/eval/interpreter.zig b/src/eval/interpreter.zig index e9edd1b089..daac12f068 100644 --- a/src/eval/interpreter.zig +++ b/src/eval/interpreter.zig @@ -1126,49 +1126,6 @@ pub const Interpreter = struct { return result_value; } - /// Extract an integer index value from a StackValue, handling the case where - /// the value has a Dec layout due to polymorphic type inference (GitHub issue #8666). - /// - /// When a numeric literal like `0` is used in a polymorphic context (e.g., `[0]`), - /// its type may be a flex var that defaults to Dec during evaluation. Later, when - /// this value is used as an index to List.get (which expects U64), the type gets - /// unified to U64 but the value's layout is already Dec. This function handles - /// both int and frac layouts to extract the integer value correctly. - fn extractIndexAsI128(index_arg: StackValue) i128 { - std.debug.assert(index_arg.is_initialized); - std.debug.assert(index_arg.ptr != null); - std.debug.assert(index_arg.layout.tag == .scalar); - - const scalar = index_arg.layout.data.scalar; - switch (scalar.tag) { - .int => { - // Normal case: integer layout - return index_arg.asI128(); - }, - .frac => { - // Handle Dec/frac layout by converting to integer - // This happens when polymorphic numerics default to Dec before type unification - const raw_ptr = @as([*]u8, @ptrCast(index_arg.ptr.?)); - switch (scalar.data.frac) { - .dec => { - // Dec stores value * 10^18, so divide to get the integer - const dec_ptr: *const RocDec = @ptrCast(@alignCast(raw_ptr)); - return @divTrunc(dec_ptr.num, RocDec.one_point_zero_i128); - }, - .f32 => { - const f32_ptr: *const f32 = @ptrCast(@alignCast(raw_ptr)); - return @intFromFloat(f32_ptr.*); - }, - .f64 => { - const f64_ptr: *const f64 = @ptrCast(@alignCast(raw_ptr)); - return @intFromFloat(f64_ptr.*); - }, - } - }, - else => unreachable, // Indices should always be numeric - } - } - /// Version of callLowLevelBuiltin that also accepts a target type for operations like num_from_numeral pub fn callLowLevelBuiltinWithTargetType(self: *Interpreter, op: can.CIR.Expr.LowLevel, args: []StackValue, roc_ops: *RocOps, return_rt_var: ?types.Var, target_type_var: ?types.Var) !StackValue { // For num_from_numeral, we need to pass the target type through a different mechanism @@ -2457,10 +2414,7 @@ pub const Interpreter = struct { std.debug.assert(list_arg.layout.tag == .list or list_arg.layout.tag == .list_of_zst); // low-level .list_get_unsafe expects list layout const roc_list: *const builtins.list.RocList = @ptrCast(@alignCast(list_arg.ptr.?)); - // Extract index as integer. Handle the case where the index has a Dec layout - // due to type inference defaulting polymorphic numbers to Dec before the type - // was unified to U64 (GitHub issue #8666). - const index = extractIndexAsI128(index_arg); + const index = index_arg.asI128(); // U64 stored as i128 // Get element layout const elem_layout_idx = list_arg.layout.data.list; diff --git a/src/eval/test/eval_test.zig b/src/eval/test/eval_test.zig index e31850bc58..5c9006e7e2 100644 --- a/src/eval/test/eval_test.zig +++ b/src/eval/test/eval_test.zig @@ -1408,26 +1408,22 @@ test "List.len returns proper U64 nominal type for method calls - regression" { , "3", .no_trace); } -test "List.get with polymorphic numeric index from for loop - regression" { +test "List.get with polymorphic numeric index - regression #8666" { // Regression test for GitHub issue #8666: interpreter panic when using - // a polymorphic numeric type (from an unannotated list) as a list index. + // a polymorphic numeric type as a list index. // - // The bug occurred because: - // 1. Polymorphic numerics default to Dec layout at runtime - // 2. list_get_unsafe used asI128() which asserts the layout is integer - // 3. But the value had a frac.dec layout, causing the assertion to fail + // The bug occurred because numeric literals with from_numeral constraints + // were being generalized, causing each use to get a fresh instantiation. + // This meant the concrete U64 type from List.get didn't propagate back + // to the original definition, leaving it as a flex var that defaulted to Dec. // - // Using List.first to exercise list_get_unsafe with a polymorphic index - // (indices list is unannotated so element type is polymorphic Num) + // The fix: don't generalize vars with from_numeral constraints, and don't + // instantiate them during lookup, so constraint propagation works correctly. try runExpectInt( \\{ \\ list = [10, 20, 30] - \\ indices = [0] - \\ var result = 0 - \\ for i in indices { - \\ result = match List.get(list, i) { Ok(v) => v, _ => 0 } - \\ } - \\ result + \\ index = 0 + \\ match List.get(list, index) { Ok(v) => v, _ => 0 } \\} , 10, .no_trace); } diff --git a/src/types/generalize.zig b/src/types/generalize.zig index 830eca732e..de29956951 100644 --- a/src/types/generalize.zig +++ b/src/types/generalize.zig @@ -205,18 +205,17 @@ pub const Generalizer = struct { if (@intFromEnum(resolved.desc.rank) < rank_to_generalize_int) { // Rank was lowered during adjustment - variable escaped try var_pool.addVarToRank(resolved.var_, resolved.desc.rank); - } else if (rank_to_generalize_int == @intFromEnum(Rank.top_level) and self.hasNumeralConstraint(resolved.desc.content)) { - // Flex var with numeric constraint at TOP LEVEL - don't generalize. + } else if (self.hasNumeralConstraint(resolved.desc.content)) { + // Flex var with numeric constraint - don't generalize at ANY rank. // This ensures numeric literals like `x = 15` stay monomorphic so that - // later usage like `I64.to_str(x)` can constrain x to I64. + // later usage like `List.get(list, x)` can constrain x to U64. // Without this, let-generalization would create a fresh copy at each use, - // leaving the original as an unconstrained flex var that defaults to Dec. + // leaving the original as an unconstrained flex var that defaults to Dec + // at runtime, causing panics when used as integer indices (GitHub #8666). // - // However, at rank > top_level (inside lambdas OR inside nested blocks), - // we DO generalize numeric literals. This allows: - // - Polymorphic functions like `|a| a + 1` to work correctly - // - Numeric literals in blocks like `{ n = 42; use_as_i64(n); use_as_dec(n) }` - // to be used polymorphically within that block's scope. + // Note: Polymorphic functions like `|a| a + 1` still work correctly because + // the numeric literal `1` inside the lambda body gets its own type variable + // that will be instantiated fresh for each call to the function. try var_pool.addVarToRank(resolved.var_, resolved.desc.rank); } else { // Rank unchanged - safe to generalize diff --git a/test/snapshots/let_polymorphism_complex.md b/test/snapshots/let_polymorphism_complex.md index e38e48414b..cc5f4b7ca8 100644 --- a/test/snapshots/let_polymorphism_complex.md +++ b/test/snapshots/let_polymorphism_complex.md @@ -1076,12 +1076,12 @@ main = |_| { (patt (type "{ value: a, wrapper: List(a) } where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])]")) (patt (type "{ value: Str, wrapper: List(Str) }")) (patt (type "{ value: a, wrapper: List(a) } where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])]")) - (patt (type "{ level1: { collection: List(_a), level2: { items: List(b), level3: { data: List(_c), value: d } } }, results: List({ data: List(e), tag: Str }) } where [b.from_numeral : Numeral -> Try(b, [InvalidNumeral(Str)]), d.from_numeral : Numeral -> Try(d, [InvalidNumeral(Str)]), e.from_numeral : Numeral -> Try(e, [InvalidNumeral(Str)])]")) + (patt (type "{ level1: { collection: List(_a), level2: { items: List(b), level3: { data: List(_c), value: b } } }, results: List({ data: List(d), tag: Str }) } where [b.from_numeral : Numeral -> Try(b, [InvalidNumeral(Str)]), d.from_numeral : Numeral -> Try(d, [InvalidNumeral(Str)])]")) (patt (type "a where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])]")) (patt (type "a where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])]")) (patt (type "List(a) where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])]")) - (patt (type "{ base: a, derived: List(b) } where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)]), b.from_numeral : Numeral -> Try(b, [InvalidNumeral(Str)])]")) - (patt (type "{ computations: { from_frac: a, from_num: b, list_from_num: List(c) }, empty_lists: { in_list: List(List(_d)), in_record: { data: List(_e) }, raw: List(_f) }, numbers: { float: g, list: List(h), value: i }, strings: { list: List(Str), value: Str } } where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)]), b.from_numeral : Numeral -> Try(b, [InvalidNumeral(Str)]), c.from_numeral : Numeral -> Try(c, [InvalidNumeral(Str)]), g.from_numeral : Numeral -> Try(g, [InvalidNumeral(Str)]), h.from_numeral : Numeral -> Try(h, [InvalidNumeral(Str)]), i.from_numeral : Numeral -> Try(i, [InvalidNumeral(Str)])]")) + (patt (type "{ base: a, derived: List(a) } where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])]")) + (patt (type "{ computations: { from_frac: a, from_num: b, list_from_num: List(b) }, empty_lists: { in_list: List(List(_c)), in_record: { data: List(_d) }, raw: List(_e) }, numbers: { float: a, list: List(b), value: b }, strings: { list: List(Str), value: Str } } where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)]), b.from_numeral : Numeral -> Try(b, [InvalidNumeral(Str)])]")) (patt (type "_arg -> a where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])]"))) (expressions (expr (type "a where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])]")) @@ -1105,11 +1105,11 @@ main = |_| { (expr (type "{ value: a, wrapper: List(a) } where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])]")) (expr (type "{ value: Str, wrapper: List(Str) }")) (expr (type "{ value: a, wrapper: List(a) } where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])]")) - (expr (type "{ level1: { collection: List(_a), level2: { items: List(b), level3: { data: List(_c), value: d } } }, results: List({ data: List(e), tag: Str }) } where [b.from_numeral : Numeral -> Try(b, [InvalidNumeral(Str)]), d.from_numeral : Numeral -> Try(d, [InvalidNumeral(Str)]), e.from_numeral : Numeral -> Try(e, [InvalidNumeral(Str)])]")) + (expr (type "{ level1: { collection: List(_a), level2: { items: List(b), level3: { data: List(_c), value: b } } }, results: List({ data: List(d), tag: Str }) } where [b.from_numeral : Numeral -> Try(b, [InvalidNumeral(Str)]), d.from_numeral : Numeral -> Try(d, [InvalidNumeral(Str)])]")) (expr (type "a where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])]")) (expr (type "a where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])]")) (expr (type "List(a) where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])]")) - (expr (type "{ base: a, derived: List(b) } where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)]), b.from_numeral : Numeral -> Try(b, [InvalidNumeral(Str)])]")) - (expr (type "{ computations: { from_frac: a, from_num: b, list_from_num: List(c) }, empty_lists: { in_list: List(List(_d)), in_record: { data: List(_e) }, raw: List(_f) }, numbers: { float: g, list: List(h), value: i }, strings: { list: List(Str), value: Str } } where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)]), b.from_numeral : Numeral -> Try(b, [InvalidNumeral(Str)]), c.from_numeral : Numeral -> Try(c, [InvalidNumeral(Str)]), g.from_numeral : Numeral -> Try(g, [InvalidNumeral(Str)]), h.from_numeral : Numeral -> Try(h, [InvalidNumeral(Str)]), i.from_numeral : Numeral -> Try(i, [InvalidNumeral(Str)])]")) + (expr (type "{ base: a, derived: List(a) } where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])]")) + (expr (type "{ computations: { from_frac: a, from_num: b, list_from_num: List(b) }, empty_lists: { in_list: List(List(_c)), in_record: { data: List(_d) }, raw: List(_e) }, numbers: { float: a, list: List(b), value: b }, strings: { list: List(Str), value: Str } } where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)]), b.from_numeral : Numeral -> Try(b, [InvalidNumeral(Str)])]")) (expr (type "_arg -> a where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])]")))) ~~~ diff --git a/test/snapshots/nominal/nominal_associated_deep_nesting.md b/test/snapshots/nominal/nominal_associated_deep_nesting.md index 30c987777e..b4da962d72 100644 --- a/test/snapshots/nominal/nominal_associated_deep_nesting.md +++ b/test/snapshots/nominal/nominal_associated_deep_nesting.md @@ -145,7 +145,7 @@ deepType = C ~~~clojure (inferred-types (defs - (patt (type "a where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])]")) + (patt (type "U64")) (patt (type "U64")) (patt (type "Foo.Level1.Level2.Level3"))) (type_decls @@ -158,7 +158,7 @@ deepType = C (nominal (type "Foo.Level1.Level2.Level3") (ty-header (name "Foo.Level1.Level2.Level3")))) (expressions - (expr (type "a where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])]")) + (expr (type "U64")) (expr (type "U64")) (expr (type "Foo.Level1.Level2.Level3")))) ~~~ diff --git a/test/snapshots/nominal/nominal_associated_lookup_decl.md b/test/snapshots/nominal/nominal_associated_lookup_decl.md index ad8477cf60..52adbd8409 100644 --- a/test/snapshots/nominal/nominal_associated_lookup_decl.md +++ b/test/snapshots/nominal/nominal_associated_lookup_decl.md @@ -76,12 +76,12 @@ useBar = Foo.bar ~~~clojure (inferred-types (defs - (patt (type "a where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])]")) + (patt (type "U64")) (patt (type "U64"))) (type_decls (nominal (type "Foo") (ty-header (name "Foo")))) (expressions - (expr (type "a where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])]")) + (expr (type "U64")) (expr (type "U64")))) ~~~ diff --git a/test/snapshots/nominal/nominal_associated_lookup_nested.md b/test/snapshots/nominal/nominal_associated_lookup_nested.md index d5f2a0b90c..a15238b831 100644 --- a/test/snapshots/nominal/nominal_associated_lookup_nested.md +++ b/test/snapshots/nominal/nominal_associated_lookup_nested.md @@ -111,7 +111,7 @@ myNum = Foo.Bar.baz ~~~clojure (inferred-types (defs - (patt (type "a where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])]")) + (patt (type "U64")) (patt (type "Foo.Bar")) (patt (type "U64"))) (type_decls @@ -120,7 +120,7 @@ myNum = Foo.Bar.baz (nominal (type "Foo.Bar") (ty-header (name "Foo.Bar")))) (expressions - (expr (type "a where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])]")) + (expr (type "U64")) (expr (type "Foo.Bar")) (expr (type "U64")))) ~~~ diff --git a/test/snapshots/nominal/nominal_associated_value_alias.md b/test/snapshots/nominal/nominal_associated_value_alias.md index 78e7acfd56..2efbd80e84 100644 --- a/test/snapshots/nominal/nominal_associated_value_alias.md +++ b/test/snapshots/nominal/nominal_associated_value_alias.md @@ -97,14 +97,14 @@ result = myBar ~~~clojure (inferred-types (defs - (patt (type "a where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])]")) + (patt (type "U64")) (patt (type "U64")) (patt (type "U64"))) (type_decls (nominal (type "Foo") (ty-header (name "Foo")))) (expressions - (expr (type "a where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])]")) + (expr (type "U64")) (expr (type "U64")) (expr (type "U64")))) ~~~ diff --git a/test/snapshots/numeric_let_generalize_in_block.md b/test/snapshots/numeric_let_generalize_in_block.md index a75ca45b6f..a363674093 100644 --- a/test/snapshots/numeric_let_generalize_in_block.md +++ b/test/snapshots/numeric_let_generalize_in_block.md @@ -1,6 +1,6 @@ # META ~~~ini -description=Numeric let-generalization inside nested block (rank > top_level) +description=Numeric without let-generalization gives type error (only lambdas get let-generalization) type=expr ~~~ # SOURCE @@ -13,9 +13,22 @@ type=expr } ~~~ # EXPECTED -NIL +TYPE MISMATCH - numeric_let_generalize_in_block.md:4:20:4:21 # PROBLEMS -NIL +**TYPE MISMATCH** +The first argument being passed to this function has the wrong type: +**numeric_let_generalize_in_block.md:4:20:4:21:** +```roc + b = Dec.to_str(n) +``` + ^ + +This argument has the type: + _I64_ + +But the function needs the first argument to be: + _Dec_ + # TOKENS ~~~zig OpenCurly, @@ -87,5 +100,5 @@ EndOfFile, ~~~ # TYPES ~~~clojure -(expr (type "Str")) +(expr (type "Error")) ~~~ diff --git a/test/snapshots/repl/numeric_multiple_diff_types.md b/test/snapshots/repl/numeric_multiple_diff_types.md index a41bc4869a..a14f766db4 100644 --- a/test/snapshots/repl/numeric_multiple_diff_types.md +++ b/test/snapshots/repl/numeric_multiple_diff_types.md @@ -1,6 +1,6 @@ # META ~~~ini -description=Numeric without annotation, multiple uses with different types (each use gets fresh type) +description=Numeric without annotation, later use gives type error (no let-generalization for non-lambdas) type=repl ~~~ # SOURCE @@ -17,6 +17,6 @@ assigned `a` --- assigned `b` --- -"4242.0" +TYPE MISMATCH # PROBLEMS NIL