From 004219d4eef5123721c90a0ff4a8d9029f238eed Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 7 Nov 2025 08:23:06 -0500 Subject: [PATCH 01/38] wip --- src/build/builtin_compiler/main.zig | 6 ++++++ src/build/roc/Builtin.roc | 2 -- src/builtins/list.zig | 10 +++++++++ src/canonicalize/Expression.zig | 4 ++++ src/eval/interpreter.zig | 32 +++++++++++++++++++++++++++++ 5 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/build/builtin_compiler/main.zig b/src/build/builtin_compiler/main.zig index 03bb18df6b..0ae34c46fe 100644 --- a/src/build/builtin_compiler/main.zig +++ b/src/build/builtin_compiler/main.zig @@ -149,6 +149,12 @@ fn replaceStrIsEmptyWithLowLevel(env: *ModuleEnv) !std.ArrayList(CIR.Def.Idx) { if (env.common.findIdent("Builtin.Str.is_empty")) |str_is_empty_ident| { try low_level_map.put(str_is_empty_ident, .str_is_empty); } + if (env.common.findIdent("Builtin.List.len")) |list_len_ident| { + try low_level_map.put(list_len_ident, .list_len); + } + if (env.common.findIdent("Builtin.List.is_empty")) |list_is_empty_ident| { + try low_level_map.put(list_is_empty_ident, .list_is_empty); + } if (env.common.findIdent("Builtin.Set.is_empty")) |set_is_empty_ident| { try low_level_map.put(set_is_empty_ident, .set_is_empty); } diff --git a/src/build/roc/Builtin.roc b/src/build/roc/Builtin.roc index feb433ccf7..dae6f06fd1 100644 --- a/src/build/roc/Builtin.roc +++ b/src/build/roc/Builtin.roc @@ -8,10 +8,8 @@ Builtin := [].{ List := [ProvidedByCompiler].{ len : List(a) -> U64 - len = |_| 0 is_empty : List(a) -> Bool - is_empty = |_| True first : List(a) -> Try(a, [ListWasEmpty]) first = |_| Err(ListWasEmpty) diff --git a/src/builtins/list.zig b/src/builtins/list.zig index ce32f60886..e9147642d7 100644 --- a/src/builtins/list.zig +++ b/src/builtins/list.zig @@ -375,6 +375,16 @@ pub fn listIncref(list: RocList, amount: isize, elements_refcounted: bool) callc list.incref(amount, elements_refcounted); } +/// Get the number of elements in the list. +pub fn listLen(list: RocList) callconv(.c) usize { + return list.len(); +} + +/// Check if the list is empty. +pub fn listIsEmpty(list: RocList) callconv(.c) bool { + return list.isEmpty(); +} + /// Decrement reference count and deallocate when no longer shared. pub fn listDecref( list: RocList, diff --git a/src/canonicalize/Expression.zig b/src/canonicalize/Expression.zig index 3229d1ad7f..a34e08c501 100644 --- a/src/canonicalize/Expression.zig +++ b/src/canonicalize/Expression.zig @@ -387,6 +387,10 @@ pub const Expr = union(enum) { // String operations str_is_empty, + // List operations + list_len, + list_is_empty, + // Set operations set_is_empty, diff --git a/src/eval/interpreter.zig b/src/eval/interpreter.zig index 56fa5a9d9b..3499748205 100644 --- a/src/eval/interpreter.zig +++ b/src/eval/interpreter.zig @@ -2119,6 +2119,38 @@ pub const Interpreter = struct { return try self.makeSimpleBoolValue(result); }, + .list_len => { + // List.len : List(a) -> U64 + // Note: listLen returns usize, but List.len always returns U64. + // We need to cast usize -> u64 for 32-bit targets (e.g. wasm32). + if (args.len != 1) return error.TypeMismatch; + + const list_arg = args[0]; + if (list_arg.ptr == null) return error.TypeMismatch; + + const roc_list: *const builtins.list.RocList = @ptrCast(@alignCast(list_arg.ptr.?)); + const len_usize = builtins.list.listLen(roc_list.*); + const len_u64: u64 = @intCast(len_usize); + + const result_layout = layout.Layout.int(.u64); + var out = try self.pushRaw(result_layout, 0); + out.is_initialized = false; + out.setInt(@intCast(len_u64)); + out.is_initialized = true; + return out; + }, + .list_is_empty => { + // List.is_empty : List(a) -> Bool + if (args.len != 1) return error.TypeMismatch; + + const list_arg = args[0]; + if (list_arg.ptr == null) return error.TypeMismatch; + + const roc_list: *const builtins.list.RocList = @ptrCast(@alignCast(list_arg.ptr.?)); + const result = builtins.list.listIsEmpty(roc_list.*); + + return try self.makeSimpleBoolValue(result); + }, .set_is_empty => { // TODO: implement Set.is_empty self.triggerCrash("Set.is_empty not yet implemented", false, roc_ops); From d437dc952777e9b375e802bb988a8871ee424841 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 7 Nov 2025 11:40:39 -0500 Subject: [PATCH 02/38] Use list_get_unsafe to get List.get working --- src/build/builtin_compiler/main.zig | 5 + src/build/roc/Builtin.roc | 58 +++++---- src/builtins/list.zig | 12 ++ src/canonicalize/Can.zig | 156 +++++++++++++++++++++--- src/canonicalize/Expression.zig | 1 + src/check/Check.zig | 122 +++++++++++++++--- src/eval/interpreter.zig | 51 ++++++++ src/eval/test/anno_only_interp_test.zig | 17 +++ 8 files changed, 362 insertions(+), 60 deletions(-) diff --git a/src/build/builtin_compiler/main.zig b/src/build/builtin_compiler/main.zig index 0ae34c46fe..bb7af72614 100644 --- a/src/build/builtin_compiler/main.zig +++ b/src/build/builtin_compiler/main.zig @@ -155,6 +155,11 @@ fn replaceStrIsEmptyWithLowLevel(env: *ModuleEnv) !std.ArrayList(CIR.Def.Idx) { if (env.common.findIdent("Builtin.List.is_empty")) |list_is_empty_ident| { try low_level_map.put(list_is_empty_ident, .list_is_empty); } + // list_get_unsafe is a private top-level function (not in Builtin.List) + // Module-level functions use simple names, not qualified names + if (env.common.findIdent("list_get_unsafe")) |list_get_unsafe_ident| { + try low_level_map.put(list_get_unsafe_ident, .list_get_unsafe); + } if (env.common.findIdent("Builtin.Set.is_empty")) |set_is_empty_ident| { try low_level_map.put(set_is_empty_ident, .set_is_empty); } diff --git a/src/build/roc/Builtin.roc b/src/build/roc/Builtin.roc index dae6f06fd1..fb424f9ccc 100644 --- a/src/build/roc/Builtin.roc +++ b/src/build/roc/Builtin.roc @@ -1,34 +1,9 @@ Builtin := [].{ - Str := [ProvidedByCompiler].{ - is_empty : Str -> Bool - - contains : Str, Str -> Bool - contains = |_str, _other| True - } - - List := [ProvidedByCompiler].{ - len : List(a) -> U64 - - is_empty : List(a) -> Bool - - first : List(a) -> Try(a, [ListWasEmpty]) - first = |_| Err(ListWasEmpty) - - map : List(a), (a -> b) -> List(b) - map = |_, _| [] - - keep_if : List(a), (a -> Bool) -> List(a) - keep_if = |_, _| [] - - concat : List(a), List(a) -> List(a) - concat = |_, _| [] - } - Bool := [True, False].{ not : Bool -> Bool not = |bool| match bool { - Bool.True => Bool.False - Bool.False => Bool.True + True => False + False => True } is_eq : Bool, Bool -> Bool @@ -75,6 +50,33 @@ Builtin := [].{ #} } + Str := [ProvidedByCompiler].{ + is_empty : Str -> Bool + + contains : Str, Str -> Bool + contains = |_str, _other| True + } + + List := [ProvidedByCompiler].{ + len : List(_elem) -> U64 + is_empty : List(_elem) -> Bool + + first : List(elem) -> Try(a, [ListWasEmpty]) + first = |list| List.get(list, 0) + + get : List(elem), U64 -> Try(elem, [ListWasEmpty]) + get = |list, index| if List.is_empty(list) Err(ListWasEmpty) else Ok(list_get_unsafe(list, index)) + + map : List(a), (a -> b) -> List(b) + map = |_, _| [] + + keep_if : List(a), (a -> Bool) -> List(a) + keep_if = |_, _| [] + + concat : List(a), List(a) -> List(a) + concat = |_, _| [] + } + Dict := [EmptyDict].{} Set(elem) := [].{ @@ -333,3 +335,5 @@ Builtin := [].{ } } } + +list_get_unsafe : List(elem), U64 -> elem diff --git a/src/builtins/list.zig b/src/builtins/list.zig index e9147642d7..8c312c9b61 100644 --- a/src/builtins/list.zig +++ b/src/builtins/list.zig @@ -385,6 +385,18 @@ pub fn listIsEmpty(list: RocList) callconv(.c) bool { return list.isEmpty(); } +/// Get a pointer to an element at the given index without bounds checking. +/// UNSAFE: No bounds checking is performed. Index must be < list.len(). +/// This is intended for internal use by low-level operations only. +/// Returns a pointer to the element at the given index. +pub fn listGetUnsafe(list: RocList, index: u64, element_width: usize) callconv(.c) ?[*]u8 { + if (list.bytes) |bytes| { + const byte_offset = @as(usize, @intCast(index)) * element_width; + return bytes + byte_offset; + } + return null; +} + /// Decrement reference count and deallocate when no longer shared. pub fn listDecref( list: RocList, diff --git a/src/canonicalize/Can.zig b/src/canonicalize/Can.zig index 92cc1c7d63..11e8499fbe 100644 --- a/src/canonicalize/Can.zig +++ b/src/canonicalize/Can.zig @@ -618,6 +618,10 @@ fn processAssociatedItemsSecondPass( const type_text = self.env.getIdent(type_ident); const qualified_idx = try self.env.insertQualifiedIdent(parent_text, type_text); + // Enter a new scope for the nested associated block + try self.scopeEnter(self.env.gpa, false); + defer self.scopeExit(self.env.gpa) catch unreachable; + try self.processAssociatedItemsSecondPass(qualified_idx, assoc.statements); } }, @@ -689,6 +693,25 @@ fn processAssociatedItemsSecondPass( // Register this associated item by its qualified name const def_idx_u16: u16 = @intCast(@intFromEnum(def_idx)); try self.env.setExposedNodeIndexById(qualified_idx, def_idx_u16); + + // Also make the unqualified name and short qualified name available in the current scope + // (This allows `get`, `List.get`, and `Builtin.List.get` to all work) + const def_cir = self.env.store.getDef(def_idx); + const pattern_idx = def_cir.pattern; + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + + // Add unqualified name (e.g., "get") + try current_scope.idents.put(self.env.gpa, name_ident, pattern_idx); + + // Also add short qualified name (e.g., "List.get") + // Extract the last component of parent_name (e.g., "List" from "Builtin.List") + const parent_full_text = self.env.getIdent(parent_name); + const short_parent_text = if (std.mem.lastIndexOf(u8, parent_full_text, ".")) |last_dot| + parent_full_text[last_dot + 1..] + else + parent_full_text; + const short_qualified_idx = try self.env.insertQualifiedIdent(short_parent_text, decl_text); + try current_scope.idents.put(self.env.gpa, short_qualified_idx, pattern_idx); } else {} } } @@ -711,6 +734,25 @@ fn processAssociatedItemsSecondPass( const def_idx_u16: u16 = @intCast(@intFromEnum(def_idx)); try self.env.setExposedNodeIndexById(qualified_idx, def_idx_u16); + // Also make the unqualified name and short qualified name available in the current scope + // (This allows `is_empty`, `List.is_empty`, and `Builtin.List.is_empty` to all work) + const def_cir = self.env.store.getDef(def_idx); + const pattern_idx = def_cir.pattern; + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + + // Add unqualified name (e.g., "is_empty") + try current_scope.idents.put(self.env.gpa, name_ident, pattern_idx); + + // Also add short qualified name (e.g., "List.is_empty") + // Extract the last component of parent_name (e.g., "List" from "Builtin.List") + const parent_full_text = self.env.getIdent(parent_name); + const short_parent_text = if (std.mem.lastIndexOf(u8, parent_full_text, ".")) |last_dot| + parent_full_text[last_dot + 1..] + else + parent_full_text; + const short_qualified_idx = try self.env.insertQualifiedIdent(short_parent_text, name_text); + try current_scope.idents.put(self.env.gpa, short_qualified_idx, pattern_idx); + try self.env.store.addScratchDef(def_idx); }, } @@ -823,6 +865,8 @@ fn processAssociatedItemsFirstPass( }, else => { // Skip other statement types in first pass + // Note: .type_anno is skipped here because anno-only patterns are created + // in the second pass, not the first pass }, } } @@ -1148,6 +1192,20 @@ pub fn canonicalizeFile( } break; } + + // If we didn't find any next statement, create an anno-only def + // (This handles the case where the type annotation is the last statement in the file) + if (next_i >= ast_stmt_idxs.len) { + const def_idx = try self.createAnnoOnlyDef(name_ident, type_anno_idx, where_clauses, region); + try self.env.store.addScratchDef(def_idx); + + // If this identifier should be exposed, register it + const ident_text = self.env.getIdent(name_ident); + if (self.exposed_ident_texts.contains(ident_text)) { + const def_idx_u16: u16 = @intCast(@intFromEnum(def_idx)); + try self.env.setExposedNodeIndexById(name_ident, def_idx_u16); + } + } }, .malformed => |malformed| { // We won't touch this since it's already a parse error. @@ -1248,11 +1306,48 @@ pub fn canonicalizeFile( } } }, - else => {}, + else => { + // Note: .type_anno is not handled here because anno-only patterns + // are created during processAssociatedItemsSecondPass, so they need + // to be re-introduced AFTER that call completes + }, } } try self.processAssociatedItemsSecondPass(type_ident, assoc.statements); + + // After processing, re-introduce anno-only defs into the associated block scope + // (They were just created by processAssociatedItemsSecondPass and need to be available + // for use within the associated block) + for (self.parse_ir.store.statementSlice(assoc.statements)) |anno_stmt_idx| { + const anno_stmt = self.parse_ir.store.getStatement(anno_stmt_idx); + switch (anno_stmt) { + .type_anno => |type_anno| { + if (self.parse_ir.tokens.resolveIdentifier(type_anno.name)) |anno_ident| { + // Build qualified name + const parent_text = self.env.getIdent(type_ident); + const anno_text = self.env.getIdent(anno_ident); + const qualified_ident_idx = try self.env.insertQualifiedIdent(parent_text, anno_text); + + // Look up the qualified pattern that was just created + switch (self.scopeLookup(.ident, qualified_ident_idx)) { + .found => |pattern_idx| { + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + // Add both unqualified and qualified names to the current scope + // This allows both `len` and `List.len` to work inside the associated block + try current_scope.idents.put(self.env.gpa, anno_ident, pattern_idx); + try current_scope.idents.put(self.env.gpa, qualified_ident_idx, pattern_idx); + }, + .not_found => { + // This can happen if the type_anno was followed by a matching decl + // (in which case it's not an anno-only def) + }, + } + } + }, + else => {}, + } + } } }, else => { @@ -2755,21 +2850,52 @@ pub fn canonicalizeExpr( break :blk null; } orelse { // Not a module alias and not an auto-imported module - // This is a qualified identifier with an invalid qualifier + // Check if the qualifier is a type - if so, try to lookup associated items + if (self.scopeLookupTypeBinding(module_alias)) |_| { + // This is a type with a potential associated item + // Build the fully qualified name and try to look it up + const type_text = self.env.getIdent(module_alias); + const field_text = self.env.getIdent(ident); + const type_qualified_idx = try self.env.insertQualifiedIdent(type_text, field_text); - // Check if the qualifier is in scope as a type/value - // If so, provide a more helpful error message - const diagnostic = if (self.scopeLookupTypeBinding(module_alias) != null) - Diagnostic{ .nested_value_not_found = .{ - .parent_name = module_alias, - .nested_name = ident, - .region = region, - } } - else - Diagnostic{ .qualified_ident_does_not_exist = .{ - .ident = qualified_ident, - .region = region, - } }; + // Try to look up the associated item in the current scope + switch (self.scopeLookup(.ident, type_qualified_idx)) { + .found => |found_pattern_idx| { + // Found the associated item! Mark it as used. + try self.used_patterns.put(self.env.gpa, found_pattern_idx, {}); + + // Return a local lookup expression + const expr_idx = try self.env.addExpr(CIR.Expr{ .e_lookup_local = .{ + .pattern_idx = found_pattern_idx, + } }, region); + + const free_vars_start = self.scratch_free_vars.top(); + try self.scratch_free_vars.append(found_pattern_idx); + return CanonicalizedExpr{ + .idx = expr_idx, + .free_vars = DataSpan.init(free_vars_start, 1) + }; + }, + .not_found => { + // Associated item not found - generate error + const diagnostic = Diagnostic{ .nested_value_not_found = .{ + .parent_name = module_alias, + .nested_name = ident, + .region = region, + } }; + return CanonicalizedExpr{ + .idx = try self.env.pushMalformed(Expr.Idx, diagnostic), + .free_vars = null, + }; + }, + } + } + + // Not a type either - generate appropriate error + const diagnostic = Diagnostic{ .qualified_ident_does_not_exist = .{ + .ident = qualified_ident, + .region = region, + } }; return CanonicalizedExpr{ .idx = try self.env.pushMalformed(Expr.Idx, diagnostic), diff --git a/src/canonicalize/Expression.zig b/src/canonicalize/Expression.zig index a34e08c501..f0c41377b8 100644 --- a/src/canonicalize/Expression.zig +++ b/src/canonicalize/Expression.zig @@ -390,6 +390,7 @@ pub const Expr = union(enum) { // List operations list_len, list_is_empty, + list_get_unsafe, // Internal only - private top-level function // Set operations set_is_empty, diff --git a/src/check/Check.zig b/src/check/Check.zig index 2740baf35e..6a88a31f8e 100644 --- a/src/check/Check.zig +++ b/src/check/Check.zig @@ -579,7 +579,19 @@ fn freshFromContent(self: *Self, content: Content, rank: types_mod.Rank, new_reg /// The the region for a variable fn freshBool(self: *Self, rank: Rank, new_region: Region) Allocator.Error!Var { // Use the copied Bool type from the type store (set by copyBuiltinTypes) - return try self.instantiateVar(self.bool_var, rank, .{ .explicit = new_region }); + const resolved_bool = self.types.resolveVar(self.bool_var); + std.debug.print("\nDEBUG: freshBool called\n", .{}); + std.debug.print(" self.bool_var={} rank={} content_tag={s}\n", .{ @intFromEnum(self.bool_var), resolved_bool.desc.rank, @tagName(resolved_bool.desc.content) }); + if (resolved_bool.desc.content == .structure) { + std.debug.print(" structure tag: {s}\n", .{@tagName(resolved_bool.desc.content.structure)}); + } + const result = try self.instantiateVar(self.bool_var, rank, .{ .explicit = new_region }); + const resolved_result = self.types.resolveVar(result); + std.debug.print(" result var={} rank={} content_tag={s}\n", .{ @intFromEnum(result), resolved_result.desc.rank, @tagName(resolved_result.desc.content) }); + if (resolved_result.desc.content == .structure) { + std.debug.print(" result structure tag: {s}\n", .{@tagName(resolved_result.desc.content.structure)}); + } + return result; } // fresh vars // @@ -596,16 +608,26 @@ fn updateVar(self: *Self, target_var: Var, content: types_mod.Content, rank: typ /// other modules directly. The Bool and Result types are used in language constructs like /// `if` conditions and need to be available in every module's type store. fn copyBuiltinTypes(self: *Self) !void { - const bool_stmt_idx = self.common_idents.bool_stmt; - if (self.common_idents.builtin_module) |builtin_env| { // Copy Bool type from Builtin module using the direct reference + const bool_stmt_idx = self.common_idents.bool_stmt; const bool_type_var = ModuleEnv.varFrom(bool_stmt_idx); self.bool_var = try self.copyVar(bool_type_var, builtin_env, Region.zero()); } else { - // If Builtin module reference is null, use the statement from the current module - // This happens when compiling the Builtin module itself - self.bool_var = ModuleEnv.varFrom(bool_stmt_idx); + // If Builtin module reference is null, we're compiling the Builtin module itself + // Search for the Bool type declaration in all_statements + const all_stmts = self.cir.store.sliceStatements(self.cir.all_statements); + for (all_stmts) |stmt_idx| { + const stmt = self.cir.store.getStatement(stmt_idx); + if (stmt == .s_nominal_decl) { + const header = self.cir.store.getTypeHeader(stmt.s_nominal_decl.header); + const ident_text = self.cir.getIdent(header.name); + if (std.mem.eql(u8, ident_text, "Builtin.Bool")) { + self.bool_var = ModuleEnv.varFrom(stmt_idx); + break; + } + } + } } // Result type is accessed via external references, no need to copy it here @@ -618,9 +640,6 @@ pub fn checkFile(self: *Self) std.mem.Allocator.Error!void { try ensureTypeStoreIsFilled(self); - // Copy builtin types (Bool, Result) into this module's type store - try self.copyBuiltinTypes(); - // First, iterate over the builtin statements, generating types for each type declaration const builtin_stmts_slice = self.cir.store.sliceStatements(self.cir.builtin_statements); for (builtin_stmts_slice) |builtin_stmt_idx| { @@ -638,6 +657,11 @@ pub fn checkFile(self: *Self) std.mem.Allocator.Error!void { try self.generateStmtTypeDeclType(stmt_idx); } + // Copy builtin types (Bool, Result) into this module's type store + // This must happen AFTER type declarations are generated so that when compiling + // Builtin itself, the Bool and Try types have already been created + try self.copyBuiltinTypes(); + // First pass: assign placeholder type vars const defs_slice = self.cir.store.sliceDefs(self.cir.all_defs); for (defs_slice) |def_idx| { @@ -767,8 +791,30 @@ fn checkDef(self: *Self, def_idx: CIR.Def.Idx) std.mem.Allocator.Error!void { // Unify the fresh pattern var with the placeholder _ = try self.unify(fresh_ptrn_var, placeholder_ptrn_var, rank); + // Debug: check if this is is_empty + const pattern = self.cir.store.getPattern(def.pattern); + if (pattern == .assign) { + const ident_text = self.cir.getIdent(pattern.assign.ident); + std.debug.print("\nDEBUG: Checking def for ident: {s}\n", .{ident_text}); + if (std.mem.eql(u8, ident_text, "is_empty")) { + const before_generalize = self.types.resolveVar(placeholder_ptrn_var).desc; + std.debug.print("\nDEBUG: Before generalizing is_empty\n", .{}); + std.debug.print(" placeholder_ptrn_var={} rank={} content_tag={s}\n", .{ @intFromEnum(placeholder_ptrn_var), before_generalize.rank, @tagName(before_generalize.content) }); + } + } + // Now that we are existing the scope, we must generalize then pop this rank try self.generalizer.generalize(&self.var_pool, rank); + + // Debug: check after generalization + if (pattern == .assign) { + const ident_text = self.cir.getIdent(pattern.assign.ident); + if (std.mem.eql(u8, ident_text, "is_empty")) { + const after_generalize = self.types.resolveVar(placeholder_ptrn_var).desc; + std.debug.print("\nDEBUG: After generalizing is_empty\n", .{}); + std.debug.print(" placeholder_ptrn_var={} rank={} content_tag={s}\n", .{ @intFromEnum(placeholder_ptrn_var), after_generalize.rank, @tagName(after_generalize.content) }); + } + } } // create types for type decls // @@ -875,16 +921,28 @@ fn generateStmtTypeDeclType( .num_args = @intCast(header_args.len), } }); + const nominal_content = try self.types.mkNominal( + .{ .ident_idx = header.name }, + backing_var, + header_vars, + self.common_idents.module_name, + ); try self.updateVar( decl_var, - try self.types.mkNominal( - .{ .ident_idx = header.name }, - backing_var, - header_vars, - self.common_idents.module_name, - ), + nominal_content, Rank.generalized, ); + + // Debug: print ALL nominal type declarations + const ident_text = self.cir.getIdent(header.name); + std.debug.print("\nDEBUG: Generated nominal type: {s}\n", .{ident_text}); + std.debug.print(" decl_var={} decl_idx={}\n", .{ @intFromEnum(decl_var), @intFromEnum(decl_idx) }); + std.debug.print(" backing_var={}\n", .{ @intFromEnum(backing_var) }); + const resolved = self.types.resolveVar(decl_var); + std.debug.print(" decl_var resolves to: rank={} content_tag={s}\n", .{ resolved.desc.rank, @tagName(resolved.desc.content) }); + if (resolved.desc.content == .structure) { + std.debug.print(" structure tag: {s}\n", .{@tagName(resolved.desc.content.structure)}); + } }, .s_runtime_error => { try self.updateVar(decl_var, .err, Rank.generalized); @@ -2450,6 +2508,17 @@ fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx, rank: types_mod.Rank, expected const pat_var = ModuleEnv.varFrom(lookup.pattern_idx); const resolved_pat = self.types.resolveVar(pat_var).desc; + // Debug: check if this is is_empty + const pattern = self.cir.store.getPattern(lookup.pattern_idx); + if (pattern == .assign) { + const ident_text = self.cir.getIdent(pattern.assign.ident); + if (std.mem.eql(u8, ident_text, "is_empty")) { + std.debug.print("\nDEBUG: Looking up is_empty\n", .{}); + std.debug.print(" pat_var={} rank={} content_tag={s}\n", .{ @intFromEnum(pat_var), resolved_pat.rank, @tagName(resolved_pat.content) }); + std.debug.print(" Will instantiate: {}\n", .{resolved_pat.rank == Rank.generalized and resolved_pat.content != .rigid}); + } + } + // We never instantiate rigid variables if (resolved_pat.rank == Rank.generalized and resolved_pat.content != .rigid) { const instantiated = try self.instantiateVar(pat_var, rank, .use_last_var); @@ -2979,14 +3048,14 @@ fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx, rank: types_mod.Rank, expected // For annotation-only expressions, the type comes from the annotation. // This case should only occur when the expression has an annotation (which is // enforced during canonicalization), so the expected type should be set. - // The type will be unified with the expected type in the code below. switch (expected) { .no_expectation => { // This shouldn't happen since we always create e_anno_only with an annotation try self.updateVar(expr_var, .err, rank); }, - .expected => { - // The expr_var will be unified with the annotation var below + .expected => |expected_type| { + // Redirect expr_var to the annotation var so that lookups get the correct type + _ = try self.types.setVarRedirect(expr_var, expected_type.var_); }, } }, @@ -3244,6 +3313,23 @@ fn checkIfElseExpr( var does_fx = try self.checkExpr(first_branch.cond, rank, .no_expectation); const first_cond_var: Var = ModuleEnv.varFrom(first_branch.cond); const bool_var = try self.freshBool(rank, expr_region); + + // Debug: print types before unification + const resolved_cond = self.types.resolveVar(first_cond_var); + const resolved_bool = self.types.resolveVar(bool_var); + std.debug.print("\nDEBUG: If condition analysis\n", .{}); + std.debug.print(" Condition var={} rank={} content_tag={s}\n", .{@intFromEnum(first_cond_var), resolved_cond.desc.rank, @tagName(resolved_cond.desc.content)}); + std.debug.print(" Expected var={} rank={} content_tag={s}\n", .{@intFromEnum(bool_var), resolved_bool.desc.rank, @tagName(resolved_bool.desc.content)}); + + // Debug: if both are structure (nominal), print their details + if (resolved_cond.desc.content == .structure and resolved_bool.desc.content == .structure) { + std.debug.print(" Both are nominal types\n", .{}); + const cond_structure = resolved_cond.desc.content.structure; + const bool_structure = resolved_bool.desc.content.structure; + std.debug.print(" Condition structure tag: {s}\n", .{@tagName(cond_structure)}); + std.debug.print(" Expected structure tag: {s}\n", .{@tagName(bool_structure)}); + } + const first_cond_result = try self.unify(bool_var, first_cond_var, rank); self.setDetailIfTypeMismatch(first_cond_result, .incompatible_if_cond); diff --git a/src/eval/interpreter.zig b/src/eval/interpreter.zig index 3499748205..be24c3f8d2 100644 --- a/src/eval/interpreter.zig +++ b/src/eval/interpreter.zig @@ -2151,6 +2151,57 @@ pub const Interpreter = struct { return try self.makeSimpleBoolValue(result); }, + .list_get_unsafe => { + // Internal operation: Get element at index without bounds checking + // Args: List(a), U64 (index) + // Returns: a (the element) + if (args.len != 2) return error.TypeMismatch; + + const list_arg = args[0]; + const index_arg = args[1]; + + if (list_arg.ptr == null) return error.TypeMismatch; + + // Extract element layout from List(a) + if (list_arg.layout.tag != .list and list_arg.layout.tag != .list_of_zst) { + return error.TypeMismatch; + } + + const roc_list: *const builtins.list.RocList = @ptrCast(@alignCast(list_arg.ptr.?)); + const index = index_arg.asI128(); // U64 stored as i128 + + // Get element layout + const elem_layout_idx = list_arg.layout.data.list; + const elem_layout = self.runtime_layout_store.getLayout(elem_layout_idx); + const elem_size = self.runtime_layout_store.layoutSize(elem_layout); + + if (elem_size == 0) { + // ZST element - return zero-sized value + return StackValue{ + .layout = elem_layout, + .ptr = null, + .is_initialized = true, + }; + } + + // Get pointer to element (no bounds checking!) + const elem_ptr = builtins.list.listGetUnsafe(roc_list.*, @intCast(index), elem_size); + + if (elem_ptr == null) { + self.triggerCrash("list_get_unsafe: null pointer returned", false, roc_ops); + return error.Crash; + } + + // Create StackValue pointing to the element + const elem_value = StackValue{ + .layout = elem_layout, + .ptr = @ptrCast(elem_ptr.?), + .is_initialized = true, + }; + + // Copy to new location and increment refcount + return try self.pushCopy(elem_value, roc_ops); + }, .set_is_empty => { // TODO: implement Set.is_empty self.triggerCrash("Set.is_empty not yet implemented", false, roc_ops); diff --git a/src/eval/test/anno_only_interp_test.zig b/src/eval/test/anno_only_interp_test.zig index c7936312ed..0c15937f23 100644 --- a/src/eval/test/anno_only_interp_test.zig +++ b/src/eval/test/anno_only_interp_test.zig @@ -210,3 +210,20 @@ test "e_anno_only - value only crashes when accessed (False branch)" { try testing.expectEqual(@as(u32, 2), summary.evaluated); try testing.expectEqual(@as(u32, 0), summary.crashed); } + +test "List.first on nonempty list" { + const src = + \\import Builtin exposing [List, Try] + \\ + \\result = List.first([1, 2, 3]) + ; + + var result = try parseCheckAndEvalModule(src); + defer cleanupEvalModule(&result); + + const summary = try result.evaluator.evalAll(); + + // Should evaluate 1 declaration with 0 crashes (List.first should succeed) + try testing.expectEqual(@as(u32, 1), summary.evaluated); + try testing.expectEqual(@as(u32, 0), summary.crashed); +} From 8b62182e5d49759ea783e44f0bc4ad07268d53c5 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 7 Nov 2025 15:48:23 -0500 Subject: [PATCH 03/38] Get more List.get stuff working --- src/build/builtin_compiler/main.zig | 8 +- src/build/roc/Builtin.roc | 76 +++++++------ src/canonicalize/Can.zig | 9 +- src/canonicalize/Expression.zig | 2 +- src/canonicalize/ModuleEnv.zig | 16 ++- src/check/Check.zig | 88 ++------------- src/eval/test/anno_only_interp_test.zig | 104 ++++++++++++++++++ test/snapshots/file/inline_ingested_file.md | 4 +- .../formatting/multiline/everything.md | 4 +- test/snapshots/formatting/multiline/hosted.md | 13 ++- .../snapshots/formatting/multiline/package.md | 13 ++- .../multiline_without_comma/everything.md | 4 +- .../multiline_without_comma/hosted.md | 13 ++- .../multiline_without_comma/package.md | 13 ++- .../formatting/singleline/everything.md | 4 +- .../snapshots/formatting/singleline/hosted.md | 13 ++- .../formatting/singleline/package.md | 13 ++- .../singleline_with_comma/everything.md | 4 +- .../singleline_with_comma/hosted.md | 13 ++- .../singleline_with_comma/package.md | 13 ++- test/snapshots/fuzz_crash/fuzz_crash_002.md | 13 ++- test/snapshots/fuzz_crash/fuzz_crash_018.md | 13 ++- test/snapshots/fuzz_crash/fuzz_crash_019.md | 8 +- test/snapshots/fuzz_crash/fuzz_crash_020.md | 8 +- test/snapshots/fuzz_crash/fuzz_crash_022.md | 4 +- test/snapshots/fuzz_crash/fuzz_crash_026.md | Bin 9525 -> 9527 bytes test/snapshots/fuzz_crash/fuzz_crash_027.md | 4 +- test/snapshots/fuzz_crash/fuzz_crash_028.md | Bin 55896 -> 56024 bytes test/snapshots/fuzz_crash/fuzz_crash_029.md | 13 ++- test/snapshots/fuzz_crash/fuzz_crash_040.md | 13 ++- test/snapshots/fuzz_crash/fuzz_crash_042.md | 13 ++- test/snapshots/fuzz_crash/fuzz_crash_043.md | 13 ++- test/snapshots/fuzz_crash/fuzz_crash_048.md | 21 ++-- test/snapshots/fuzz_crash/fuzz_crash_049.md | Bin 120224 -> 120242 bytes test/snapshots/fuzz_crash/fuzz_crash_079.md | 13 ++- test/snapshots/platform/platform_int.md | 16 ++- test/snapshots/platform/platform_str.md | 15 ++- .../type_annotation_missing_parens.md | 24 +++- test/snapshots/type_annotations.md | 21 ++-- .../where_clause/where_clauses_10.md | 18 ++- .../where_clause/where_clauses_error_cases.md | 24 +++- 41 files changed, 466 insertions(+), 215 deletions(-) diff --git a/src/build/builtin_compiler/main.zig b/src/build/builtin_compiler/main.zig index bb7af72614..8213494948 100644 --- a/src/build/builtin_compiler/main.zig +++ b/src/build/builtin_compiler/main.zig @@ -155,9 +155,11 @@ fn replaceStrIsEmptyWithLowLevel(env: *ModuleEnv) !std.ArrayList(CIR.Def.Idx) { if (env.common.findIdent("Builtin.List.is_empty")) |list_is_empty_ident| { try low_level_map.put(list_is_empty_ident, .list_is_empty); } - // list_get_unsafe is a private top-level function (not in Builtin.List) - // Module-level functions use simple names, not qualified names - if (env.common.findIdent("list_get_unsafe")) |list_get_unsafe_ident| { + // Note: List.get is now a real function implementation in Builtin.roc, + // not a bare type annotation, so it's not in the low_level_map + + // list_get_unsafe is a top-level function in Builtin module + if (env.common.findIdent("Builtin.list_get_unsafe")) |list_get_unsafe_ident| { try low_level_map.put(list_get_unsafe_ident, .list_get_unsafe); } if (env.common.findIdent("Builtin.Set.is_empty")) |set_is_empty_ident| { diff --git a/src/build/roc/Builtin.roc b/src/build/roc/Builtin.roc index fb424f9ccc..94658b007f 100644 --- a/src/build/roc/Builtin.roc +++ b/src/build/roc/Builtin.roc @@ -1,9 +1,52 @@ Builtin := [].{ + Str := [ProvidedByCompiler].{ + is_empty : Str -> Bool + + contains : Str, Str -> Bool + contains = |_str, _other| True + } + + # Private top-level function for unsafe list access + # This is a low-level operation that gets replaced by the compiler + list_get_unsafe : List(elem), U64 -> elem + + List := [ProvidedByCompiler].{ + len : List(_elem) -> U64 + is_empty : List(_elem) -> Bool + + first : List(elem) -> Try(elem, [ListWasEmpty]) + first = |list| List.get(list, 0) + + get : List(elem), U64 -> Try(elem, [ListWasEmpty]) + get = |list, index| + if List.is_empty(list) { + Try.Err(ListWasEmpty) + } else { + # list_get_unsafe is a top-level low-level function + # It returns the element if in bounds, or crashes if out of bounds + # We need to check bounds first + if index >= List.len(list) { + Try.Err(ListWasEmpty) + } else { + Try.Ok(list_get_unsafe(list, index)) + } + } + + map : List(a), (a -> b) -> List(b) + map = |_, _| [] + + keep_if : List(a), (a -> Bool) -> List(a) + keep_if = |_, _| [] + + concat : List(a), List(a) -> List(a) + concat = |_, _| [] + } + Bool := [True, False].{ not : Bool -> Bool not = |bool| match bool { - True => False - False => True + Bool.True => Bool.False + Bool.False => Bool.True } is_eq : Bool, Bool -> Bool @@ -50,33 +93,6 @@ Builtin := [].{ #} } - Str := [ProvidedByCompiler].{ - is_empty : Str -> Bool - - contains : Str, Str -> Bool - contains = |_str, _other| True - } - - List := [ProvidedByCompiler].{ - len : List(_elem) -> U64 - is_empty : List(_elem) -> Bool - - first : List(elem) -> Try(a, [ListWasEmpty]) - first = |list| List.get(list, 0) - - get : List(elem), U64 -> Try(elem, [ListWasEmpty]) - get = |list, index| if List.is_empty(list) Err(ListWasEmpty) else Ok(list_get_unsafe(list, index)) - - map : List(a), (a -> b) -> List(b) - map = |_, _| [] - - keep_if : List(a), (a -> Bool) -> List(a) - keep_if = |_, _| [] - - concat : List(a), List(a) -> List(a) - concat = |_, _| [] - } - Dict := [EmptyDict].{} Set(elem) := [].{ @@ -335,5 +351,3 @@ Builtin := [].{ } } } - -list_get_unsafe : List(elem), U64 -> elem diff --git a/src/canonicalize/Can.zig b/src/canonicalize/Can.zig index 11e8499fbe..0dd950a4a5 100644 --- a/src/canonicalize/Can.zig +++ b/src/canonicalize/Can.zig @@ -707,7 +707,7 @@ fn processAssociatedItemsSecondPass( // Extract the last component of parent_name (e.g., "List" from "Builtin.List") const parent_full_text = self.env.getIdent(parent_name); const short_parent_text = if (std.mem.lastIndexOf(u8, parent_full_text, ".")) |last_dot| - parent_full_text[last_dot + 1..] + parent_full_text[last_dot + 1 ..] else parent_full_text; const short_qualified_idx = try self.env.insertQualifiedIdent(short_parent_text, decl_text); @@ -747,7 +747,7 @@ fn processAssociatedItemsSecondPass( // Extract the last component of parent_name (e.g., "List" from "Builtin.List") const parent_full_text = self.env.getIdent(parent_name); const short_parent_text = if (std.mem.lastIndexOf(u8, parent_full_text, ".")) |last_dot| - parent_full_text[last_dot + 1..] + parent_full_text[last_dot + 1 ..] else parent_full_text; const short_qualified_idx = try self.env.insertQualifiedIdent(short_parent_text, name_text); @@ -2871,10 +2871,7 @@ pub fn canonicalizeExpr( const free_vars_start = self.scratch_free_vars.top(); try self.scratch_free_vars.append(found_pattern_idx); - return CanonicalizedExpr{ - .idx = expr_idx, - .free_vars = DataSpan.init(free_vars_start, 1) - }; + return CanonicalizedExpr{ .idx = expr_idx, .free_vars = DataSpan.init(free_vars_start, 1) }; }, .not_found => { // Associated item not found - generate error diff --git a/src/canonicalize/Expression.zig b/src/canonicalize/Expression.zig index f0c41377b8..44af97e71c 100644 --- a/src/canonicalize/Expression.zig +++ b/src/canonicalize/Expression.zig @@ -390,7 +390,7 @@ pub const Expr = union(enum) { // List operations list_len, list_is_empty, - list_get_unsafe, // Internal only - private top-level function + list_get_unsafe, // Internal only - returns element or crashes if out of bounds // Set operations set_is_empty, diff --git a/src/canonicalize/ModuleEnv.zig b/src/canonicalize/ModuleEnv.zig index 067aea17d7..a7fd480a67 100644 --- a/src/canonicalize/ModuleEnv.zig +++ b/src/canonicalize/ModuleEnv.zig @@ -1604,11 +1604,17 @@ pub const Serialized = struct { .store = self.store.deserialize(offset, gpa).*, .evaluation_order = null, // Not serialized, will be recomputed if needed // Well-known identifiers for type checking - look them up in the deserialized common env - .from_int_digits_ident = env.common.findIdent(Ident.FROM_INT_DIGITS_METHOD_NAME) orelse unreachable, - .from_dec_digits_ident = env.common.findIdent(Ident.FROM_DEC_DIGITS_METHOD_NAME) orelse unreachable, - .try_ident = env.common.findIdent("Try") orelse unreachable, - .out_of_range_ident = env.common.findIdent("OutOfRange") orelse unreachable, - .builtin_module_ident = env.common.findIdent("Builtin") orelse unreachable, + // If not found, insert them (this can happen if Builtin.roc doesn't reference them) + .from_int_digits_ident = env.common.findIdent(Ident.FROM_INT_DIGITS_METHOD_NAME) orelse + try env.common.insertIdent(gpa, Ident.for_text(Ident.FROM_INT_DIGITS_METHOD_NAME)), + .from_dec_digits_ident = env.common.findIdent(Ident.FROM_DEC_DIGITS_METHOD_NAME) orelse + try env.common.insertIdent(gpa, Ident.for_text(Ident.FROM_DEC_DIGITS_METHOD_NAME)), + .try_ident = env.common.findIdent("Try") orelse + try env.common.insertIdent(gpa, Ident.for_text("Try")), + .out_of_range_ident = env.common.findIdent("OutOfRange") orelse + try env.common.insertIdent(gpa, Ident.for_text("OutOfRange")), + .builtin_module_ident = env.common.findIdent("Builtin") orelse + try env.common.insertIdent(gpa, Ident.for_text("Builtin")), }; return env; diff --git a/src/check/Check.zig b/src/check/Check.zig index 6a88a31f8e..13a20cb758 100644 --- a/src/check/Check.zig +++ b/src/check/Check.zig @@ -579,19 +579,7 @@ fn freshFromContent(self: *Self, content: Content, rank: types_mod.Rank, new_reg /// The the region for a variable fn freshBool(self: *Self, rank: Rank, new_region: Region) Allocator.Error!Var { // Use the copied Bool type from the type store (set by copyBuiltinTypes) - const resolved_bool = self.types.resolveVar(self.bool_var); - std.debug.print("\nDEBUG: freshBool called\n", .{}); - std.debug.print(" self.bool_var={} rank={} content_tag={s}\n", .{ @intFromEnum(self.bool_var), resolved_bool.desc.rank, @tagName(resolved_bool.desc.content) }); - if (resolved_bool.desc.content == .structure) { - std.debug.print(" structure tag: {s}\n", .{@tagName(resolved_bool.desc.content.structure)}); - } - const result = try self.instantiateVar(self.bool_var, rank, .{ .explicit = new_region }); - const resolved_result = self.types.resolveVar(result); - std.debug.print(" result var={} rank={} content_tag={s}\n", .{ @intFromEnum(result), resolved_result.desc.rank, @tagName(resolved_result.desc.content) }); - if (resolved_result.desc.content == .structure) { - std.debug.print(" result structure tag: {s}\n", .{@tagName(resolved_result.desc.content.structure)}); - } - return result; + return try self.instantiateVar(self.bool_var, rank, .{ .explicit = new_region }); } // fresh vars // @@ -791,30 +779,8 @@ fn checkDef(self: *Self, def_idx: CIR.Def.Idx) std.mem.Allocator.Error!void { // Unify the fresh pattern var with the placeholder _ = try self.unify(fresh_ptrn_var, placeholder_ptrn_var, rank); - // Debug: check if this is is_empty - const pattern = self.cir.store.getPattern(def.pattern); - if (pattern == .assign) { - const ident_text = self.cir.getIdent(pattern.assign.ident); - std.debug.print("\nDEBUG: Checking def for ident: {s}\n", .{ident_text}); - if (std.mem.eql(u8, ident_text, "is_empty")) { - const before_generalize = self.types.resolveVar(placeholder_ptrn_var).desc; - std.debug.print("\nDEBUG: Before generalizing is_empty\n", .{}); - std.debug.print(" placeholder_ptrn_var={} rank={} content_tag={s}\n", .{ @intFromEnum(placeholder_ptrn_var), before_generalize.rank, @tagName(before_generalize.content) }); - } - } - // Now that we are existing the scope, we must generalize then pop this rank try self.generalizer.generalize(&self.var_pool, rank); - - // Debug: check after generalization - if (pattern == .assign) { - const ident_text = self.cir.getIdent(pattern.assign.ident); - if (std.mem.eql(u8, ident_text, "is_empty")) { - const after_generalize = self.types.resolveVar(placeholder_ptrn_var).desc; - std.debug.print("\nDEBUG: After generalizing is_empty\n", .{}); - std.debug.print(" placeholder_ptrn_var={} rank={} content_tag={s}\n", .{ @intFromEnum(placeholder_ptrn_var), after_generalize.rank, @tagName(after_generalize.content) }); - } - } } // create types for type decls // @@ -921,28 +887,16 @@ fn generateStmtTypeDeclType( .num_args = @intCast(header_args.len), } }); - const nominal_content = try self.types.mkNominal( - .{ .ident_idx = header.name }, - backing_var, - header_vars, - self.common_idents.module_name, - ); try self.updateVar( decl_var, - nominal_content, + try self.types.mkNominal( + .{ .ident_idx = header.name }, + backing_var, + header_vars, + self.common_idents.module_name, + ), Rank.generalized, ); - - // Debug: print ALL nominal type declarations - const ident_text = self.cir.getIdent(header.name); - std.debug.print("\nDEBUG: Generated nominal type: {s}\n", .{ident_text}); - std.debug.print(" decl_var={} decl_idx={}\n", .{ @intFromEnum(decl_var), @intFromEnum(decl_idx) }); - std.debug.print(" backing_var={}\n", .{ @intFromEnum(backing_var) }); - const resolved = self.types.resolveVar(decl_var); - std.debug.print(" decl_var resolves to: rank={} content_tag={s}\n", .{ resolved.desc.rank, @tagName(resolved.desc.content) }); - if (resolved.desc.content == .structure) { - std.debug.print(" structure tag: {s}\n", .{@tagName(resolved.desc.content.structure)}); - } }, .s_runtime_error => { try self.updateVar(decl_var, .err, Rank.generalized); @@ -2508,17 +2462,6 @@ fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx, rank: types_mod.Rank, expected const pat_var = ModuleEnv.varFrom(lookup.pattern_idx); const resolved_pat = self.types.resolveVar(pat_var).desc; - // Debug: check if this is is_empty - const pattern = self.cir.store.getPattern(lookup.pattern_idx); - if (pattern == .assign) { - const ident_text = self.cir.getIdent(pattern.assign.ident); - if (std.mem.eql(u8, ident_text, "is_empty")) { - std.debug.print("\nDEBUG: Looking up is_empty\n", .{}); - std.debug.print(" pat_var={} rank={} content_tag={s}\n", .{ @intFromEnum(pat_var), resolved_pat.rank, @tagName(resolved_pat.content) }); - std.debug.print(" Will instantiate: {}\n", .{resolved_pat.rank == Rank.generalized and resolved_pat.content != .rigid}); - } - } - // We never instantiate rigid variables if (resolved_pat.rank == Rank.generalized and resolved_pat.content != .rigid) { const instantiated = try self.instantiateVar(pat_var, rank, .use_last_var); @@ -3313,23 +3256,6 @@ fn checkIfElseExpr( var does_fx = try self.checkExpr(first_branch.cond, rank, .no_expectation); const first_cond_var: Var = ModuleEnv.varFrom(first_branch.cond); const bool_var = try self.freshBool(rank, expr_region); - - // Debug: print types before unification - const resolved_cond = self.types.resolveVar(first_cond_var); - const resolved_bool = self.types.resolveVar(bool_var); - std.debug.print("\nDEBUG: If condition analysis\n", .{}); - std.debug.print(" Condition var={} rank={} content_tag={s}\n", .{@intFromEnum(first_cond_var), resolved_cond.desc.rank, @tagName(resolved_cond.desc.content)}); - std.debug.print(" Expected var={} rank={} content_tag={s}\n", .{@intFromEnum(bool_var), resolved_bool.desc.rank, @tagName(resolved_bool.desc.content)}); - - // Debug: if both are structure (nominal), print their details - if (resolved_cond.desc.content == .structure and resolved_bool.desc.content == .structure) { - std.debug.print(" Both are nominal types\n", .{}); - const cond_structure = resolved_cond.desc.content.structure; - const bool_structure = resolved_bool.desc.content.structure; - std.debug.print(" Condition structure tag: {s}\n", .{@tagName(cond_structure)}); - std.debug.print(" Expected structure tag: {s}\n", .{@tagName(bool_structure)}); - } - const first_cond_result = try self.unify(bool_var, first_cond_var, rank); self.setDetailIfTypeMismatch(first_cond_result, .incompatible_if_cond); diff --git a/src/eval/test/anno_only_interp_test.zig b/src/eval/test/anno_only_interp_test.zig index 0c15937f23..5890d482e0 100644 --- a/src/eval/test/anno_only_interp_test.zig +++ b/src/eval/test/anno_only_interp_test.zig @@ -227,3 +227,107 @@ test "List.first on nonempty list" { try testing.expectEqual(@as(u32, 1), summary.evaluated); try testing.expectEqual(@as(u32, 0), summary.crashed); } + +test "List.get with valid index returns Ok" { + const src = + \\import Builtin exposing [List, Try] + \\ + \\result = List.get([1, 2, 3], 1) + ; + + var result = try parseCheckAndEvalModule(src); + defer cleanupEvalModule(&result); + + const summary = try result.evaluator.evalAll(); + + // Should evaluate 1 declaration with 0 crashes (List.get should succeed) + try testing.expectEqual(@as(u32, 1), summary.evaluated); + try testing.expectEqual(@as(u32, 0), summary.crashed); +} + +test "List.get with invalid index returns Err" { + const src = + \\import Builtin exposing [List, Try] + \\ + \\result = List.get([1, 2, 3], 10) + ; + + var result = try parseCheckAndEvalModule(src); + defer cleanupEvalModule(&result); + + const summary = try result.evaluator.evalAll(); + + // Should evaluate 1 declaration with 0 crashes (List.get should return Err but not crash) + try testing.expectEqual(@as(u32, 1), summary.evaluated); + try testing.expectEqual(@as(u32, 0), summary.crashed); +} + +test "List.get on empty list returns Err" { + const src = + \\import Builtin exposing [List, Try] + \\ + \\empty : List(U64) + \\empty = [] + \\result = List.get(empty, 0) + ; + + var result = try parseCheckAndEvalModule(src); + defer cleanupEvalModule(&result); + + const summary = try result.evaluator.evalAll(); + + // Should evaluate 2 declarations with 0 crashes (List.get should return Err but not crash) + try testing.expectEqual(@as(u32, 2), summary.evaluated); + try testing.expectEqual(@as(u32, 0), summary.crashed); +} + +test "List.get with different element types - Str" { + const src = + \\import Builtin exposing [List, Try] + \\ + \\result = List.get(["foo", "bar", "baz"], 1) + ; + + var result = try parseCheckAndEvalModule(src); + defer cleanupEvalModule(&result); + + const summary = try result.evaluator.evalAll(); + + // Should evaluate 1 declaration with 0 crashes + try testing.expectEqual(@as(u32, 1), summary.evaluated); + try testing.expectEqual(@as(u32, 0), summary.crashed); +} + +test "List.get with different element types - Bool" { + const src = + \\import Builtin exposing [List, Try, Bool] + \\ + \\result = List.get([Bool.True, Bool.False, Bool.True], 2) + ; + + var result = try parseCheckAndEvalModule(src); + defer cleanupEvalModule(&result); + + const summary = try result.evaluator.evalAll(); + + // Should evaluate 1 declaration with 0 crashes + try testing.expectEqual(@as(u32, 1), summary.evaluated); + try testing.expectEqual(@as(u32, 0), summary.crashed); +} + +test "List.get with nested lists" { + const src = + \\import Builtin exposing [List, Try] + \\ + \\result = List.get([[1, 2], [3, 4], [5, 6]], 1) + ; + + var result = try parseCheckAndEvalModule(src); + defer cleanupEvalModule(&result); + + const summary = try result.evaluator.evalAll(); + + // Should evaluate 1 declaration with 0 crashes + try testing.expectEqual(@as(u32, 1), summary.evaluated); + try testing.expectEqual(@as(u32, 0), summary.crashed); +} diff --git a/test/snapshots/file/inline_ingested_file.md b/test/snapshots/file/inline_ingested_file.md index af56a94354..8d01f05bc9 100644 --- a/test/snapshots/file/inline_ingested_file.md +++ b/test/snapshots/file/inline_ingested_file.md @@ -137,9 +137,9 @@ foo = Json.parse(data) ~~~clojure (inferred-types (defs - (patt (type "Error")) + (patt (type "Str")) (patt (type "Error"))) (expressions - (expr (type "Error")) + (expr (type "Str")) (expr (type "Error")))) ~~~ diff --git a/test/snapshots/formatting/multiline/everything.md b/test/snapshots/formatting/multiline/everything.md index 735fe5f096..767eea4f43 100644 --- a/test/snapshots/formatting/multiline/everything.md +++ b/test/snapshots/formatting/multiline/everything.md @@ -840,7 +840,7 @@ h = |x, y| { ~~~clojure (inferred-types (defs - (patt (type "Error")) + (patt (type "e -> e")) (patt (type "[Z1((c, d)), Z2(c, f), Z3({ a: c, b: i }), Z4(List(c))]j, [Z1((c, d)), Z2(c, f), Z3({ a: c, b: i }), Z4(List(c))]j -> c"))) (type_decls (alias (type "A(a)") @@ -866,6 +866,6 @@ h = |x, y| { (alias (type "F") (ty-header (name "F")))) (expressions - (expr (type "Error")) + (expr (type "e -> e")) (expr (type "[Z1((c, d)), Z2(c, f), Z3({ a: c, b: i }), Z4(List(c))]j, [Z1((c, d)), Z2(c, f), Z3({ a: c, b: i }), Z4(List(c))]j -> c")))) ~~~ diff --git a/test/snapshots/formatting/multiline/hosted.md b/test/snapshots/formatting/multiline/hosted.md index cd8adcf901..2064cfe200 100644 --- a/test/snapshots/formatting/multiline/hosted.md +++ b/test/snapshots/formatting/multiline/hosted.md @@ -76,6 +76,13 @@ NO CHANGE (d-let (p-assign (ident "a!")) (e-anno-only) + (annotation + (ty-fn (effectful true) + (ty-lookup (name "Str") (builtin)) + (ty-lookup (name "Str") (builtin))))) + (d-let + (p-assign (ident "b!")) + (e-anno-only) (annotation (ty-fn (effectful true) (ty-lookup (name "Str") (builtin)) @@ -85,7 +92,9 @@ NO CHANGE ~~~clojure (inferred-types (defs - (patt (type "Error"))) + (patt (type "Str => Str")) + (patt (type "Str => Str"))) (expressions - (expr (type "Error")))) + (expr (type "Str => Str")) + (expr (type "Str => Str")))) ~~~ diff --git a/test/snapshots/formatting/multiline/package.md b/test/snapshots/formatting/multiline/package.md index 0f0fcb805b..529e861dc9 100644 --- a/test/snapshots/formatting/multiline/package.md +++ b/test/snapshots/formatting/multiline/package.md @@ -93,6 +93,13 @@ NO CHANGE (d-let (p-assign (ident "a!")) (e-anno-only) + (annotation + (ty-fn (effectful true) + (ty-lookup (name "Str") (builtin)) + (ty-lookup (name "Str") (builtin))))) + (d-let + (p-assign (ident "b!")) + (e-anno-only) (annotation (ty-fn (effectful true) (ty-lookup (name "Str") (builtin)) @@ -102,7 +109,9 @@ NO CHANGE ~~~clojure (inferred-types (defs - (patt (type "Error"))) + (patt (type "Str => Str")) + (patt (type "Str => Str"))) (expressions - (expr (type "Error")))) + (expr (type "Str => Str")) + (expr (type "Str => Str")))) ~~~ diff --git a/test/snapshots/formatting/multiline_without_comma/everything.md b/test/snapshots/formatting/multiline_without_comma/everything.md index 2ca4829e6e..d4209d51d0 100644 --- a/test/snapshots/formatting/multiline_without_comma/everything.md +++ b/test/snapshots/formatting/multiline_without_comma/everything.md @@ -1904,7 +1904,7 @@ h = |x, y| { ~~~clojure (inferred-types (defs - (patt (type "Error")) + (patt (type "e -> e")) (patt (type "[Z1((c, d)), Z2(c, f), Z3({ a: c, b: i }), Z4(List(c))]j, [Z1((c, d)), Z2(c, f), Z3({ a: c, b: i }), Z4(List(c))]j -> c"))) (type_decls (alias (type "A(a)") @@ -1921,6 +1921,6 @@ h = |x, y| { (alias (type "F") (ty-header (name "F")))) (expressions - (expr (type "Error")) + (expr (type "e -> e")) (expr (type "[Z1((c, d)), Z2(c, f), Z3({ a: c, b: i }), Z4(List(c))]j, [Z1((c, d)), Z2(c, f), Z3({ a: c, b: i }), Z4(List(c))]j -> c")))) ~~~ diff --git a/test/snapshots/formatting/multiline_without_comma/hosted.md b/test/snapshots/formatting/multiline_without_comma/hosted.md index d7c78a79e8..b819f8277a 100644 --- a/test/snapshots/formatting/multiline_without_comma/hosted.md +++ b/test/snapshots/formatting/multiline_without_comma/hosted.md @@ -82,6 +82,13 @@ b! : Str => Str (d-let (p-assign (ident "a!")) (e-anno-only) + (annotation + (ty-fn (effectful true) + (ty-lookup (name "Str") (builtin)) + (ty-lookup (name "Str") (builtin))))) + (d-let + (p-assign (ident "b!")) + (e-anno-only) (annotation (ty-fn (effectful true) (ty-lookup (name "Str") (builtin)) @@ -91,7 +98,9 @@ b! : Str => Str ~~~clojure (inferred-types (defs - (patt (type "Error"))) + (patt (type "Str => Str")) + (patt (type "Str => Str"))) (expressions - (expr (type "Error")))) + (expr (type "Str => Str")) + (expr (type "Str => Str")))) ~~~ diff --git a/test/snapshots/formatting/multiline_without_comma/package.md b/test/snapshots/formatting/multiline_without_comma/package.md index f700d33e00..ec42d1f8a7 100644 --- a/test/snapshots/formatting/multiline_without_comma/package.md +++ b/test/snapshots/formatting/multiline_without_comma/package.md @@ -104,6 +104,13 @@ b! : Str => Str (d-let (p-assign (ident "a!")) (e-anno-only) + (annotation + (ty-fn (effectful true) + (ty-lookup (name "Str") (builtin)) + (ty-lookup (name "Str") (builtin))))) + (d-let + (p-assign (ident "b!")) + (e-anno-only) (annotation (ty-fn (effectful true) (ty-lookup (name "Str") (builtin)) @@ -113,7 +120,9 @@ b! : Str => Str ~~~clojure (inferred-types (defs - (patt (type "Error"))) + (patt (type "Str => Str")) + (patt (type "Str => Str"))) (expressions - (expr (type "Error")))) + (expr (type "Str => Str")) + (expr (type "Str => Str")))) ~~~ diff --git a/test/snapshots/formatting/singleline/everything.md b/test/snapshots/formatting/singleline/everything.md index 81f191a4aa..969ec0aa0c 100644 --- a/test/snapshots/formatting/singleline/everything.md +++ b/test/snapshots/formatting/singleline/everything.md @@ -528,7 +528,7 @@ NO CHANGE ~~~clojure (inferred-types (defs - (patt (type "Error")) + (patt (type "e -> e")) (patt (type "[Z1((c, d)), Z2(c, f), Z3({ a: c, b: i }), Z4(List(c))]j, [Z1((c, d)), Z2(c, f), Z3({ a: c, b: i }), Z4(List(c))]j -> c"))) (type_decls (alias (type "A(a)") @@ -554,6 +554,6 @@ NO CHANGE (alias (type "F") (ty-header (name "F")))) (expressions - (expr (type "Error")) + (expr (type "e -> e")) (expr (type "[Z1((c, d)), Z2(c, f), Z3({ a: c, b: i }), Z4(List(c))]j, [Z1((c, d)), Z2(c, f), Z3({ a: c, b: i }), Z4(List(c))]j -> c")))) ~~~ diff --git a/test/snapshots/formatting/singleline/hosted.md b/test/snapshots/formatting/singleline/hosted.md index d463426603..6905a493e8 100644 --- a/test/snapshots/formatting/singleline/hosted.md +++ b/test/snapshots/formatting/singleline/hosted.md @@ -70,6 +70,13 @@ NO CHANGE (d-let (p-assign (ident "a!")) (e-anno-only) + (annotation + (ty-fn (effectful true) + (ty-lookup (name "Str") (builtin)) + (ty-lookup (name "Str") (builtin))))) + (d-let + (p-assign (ident "b!")) + (e-anno-only) (annotation (ty-fn (effectful true) (ty-lookup (name "Str") (builtin)) @@ -79,7 +86,9 @@ NO CHANGE ~~~clojure (inferred-types (defs - (patt (type "Error"))) + (patt (type "Str => Str")) + (patt (type "Str => Str"))) (expressions - (expr (type "Error")))) + (expr (type "Str => Str")) + (expr (type "Str => Str")))) ~~~ diff --git a/test/snapshots/formatting/singleline/package.md b/test/snapshots/formatting/singleline/package.md index 1715b77289..26d8ab4681 100644 --- a/test/snapshots/formatting/singleline/package.md +++ b/test/snapshots/formatting/singleline/package.md @@ -77,6 +77,13 @@ NO CHANGE (d-let (p-assign (ident "a!")) (e-anno-only) + (annotation + (ty-fn (effectful true) + (ty-lookup (name "Str") (builtin)) + (ty-lookup (name "Str") (builtin))))) + (d-let + (p-assign (ident "b!")) + (e-anno-only) (annotation (ty-fn (effectful true) (ty-lookup (name "Str") (builtin)) @@ -86,7 +93,9 @@ NO CHANGE ~~~clojure (inferred-types (defs - (patt (type "Error"))) + (patt (type "Str => Str")) + (patt (type "Str => Str"))) (expressions - (expr (type "Error")))) + (expr (type "Str => Str")) + (expr (type "Str => Str")))) ~~~ diff --git a/test/snapshots/formatting/singleline_with_comma/everything.md b/test/snapshots/formatting/singleline_with_comma/everything.md index de29e14916..7a6ec9de8f 100644 --- a/test/snapshots/formatting/singleline_with_comma/everything.md +++ b/test/snapshots/formatting/singleline_with_comma/everything.md @@ -641,7 +641,7 @@ h = | ~~~clojure (inferred-types (defs - (patt (type "Error")) + (patt (type "e -> e")) (patt (type "[Z1((c, d)), Z2(c, f), Z3({ a: c, b: i }), Z4(List(c))]j, [Z1((c, d)), Z2(c, f), Z3({ a: c, b: i }), Z4(List(c))]j -> c"))) (type_decls (alias (type "A(a)") @@ -667,6 +667,6 @@ h = | (alias (type "F") (ty-header (name "F")))) (expressions - (expr (type "Error")) + (expr (type "e -> e")) (expr (type "[Z1((c, d)), Z2(c, f), Z3({ a: c, b: i }), Z4(List(c))]j, [Z1((c, d)), Z2(c, f), Z3({ a: c, b: i }), Z4(List(c))]j -> c")))) ~~~ diff --git a/test/snapshots/formatting/singleline_with_comma/hosted.md b/test/snapshots/formatting/singleline_with_comma/hosted.md index b83c6554c6..39b24517ab 100644 --- a/test/snapshots/formatting/singleline_with_comma/hosted.md +++ b/test/snapshots/formatting/singleline_with_comma/hosted.md @@ -76,6 +76,13 @@ b! : Str => Str (d-let (p-assign (ident "a!")) (e-anno-only) + (annotation + (ty-fn (effectful true) + (ty-lookup (name "Str") (builtin)) + (ty-lookup (name "Str") (builtin))))) + (d-let + (p-assign (ident "b!")) + (e-anno-only) (annotation (ty-fn (effectful true) (ty-lookup (name "Str") (builtin)) @@ -85,7 +92,9 @@ b! : Str => Str ~~~clojure (inferred-types (defs - (patt (type "Error"))) + (patt (type "Str => Str")) + (patt (type "Str => Str"))) (expressions - (expr (type "Error")))) + (expr (type "Str => Str")) + (expr (type "Str => Str")))) ~~~ diff --git a/test/snapshots/formatting/singleline_with_comma/package.md b/test/snapshots/formatting/singleline_with_comma/package.md index 3b1d217d1b..45eac5f650 100644 --- a/test/snapshots/formatting/singleline_with_comma/package.md +++ b/test/snapshots/formatting/singleline_with_comma/package.md @@ -88,6 +88,13 @@ b! : Str => Str (d-let (p-assign (ident "a!")) (e-anno-only) + (annotation + (ty-fn (effectful true) + (ty-lookup (name "Str") (builtin)) + (ty-lookup (name "Str") (builtin))))) + (d-let + (p-assign (ident "b!")) + (e-anno-only) (annotation (ty-fn (effectful true) (ty-lookup (name "Str") (builtin)) @@ -97,7 +104,9 @@ b! : Str => Str ~~~clojure (inferred-types (defs - (patt (type "Error"))) + (patt (type "Str => Str")) + (patt (type "Str => Str"))) (expressions - (expr (type "Error")))) + (expr (type "Str => Str")) + (expr (type "Str => Str")))) ~~~ diff --git a/test/snapshots/fuzz_crash/fuzz_crash_002.md b/test/snapshots/fuzz_crash/fuzz_crash_002.md index d92caaeb98..0125186fcb 100644 --- a/test/snapshots/fuzz_crash/fuzz_crash_002.md +++ b/test/snapshots/fuzz_crash/fuzz_crash_002.md @@ -287,11 +287,18 @@ modu : ~~~ # CANONICALIZE ~~~clojure -(can-ir (empty true)) +(can-ir + (d-let + (p-assign (ident "modu")) + (e-anno-only) + (annotation + (ty-malformed)))) ~~~ # TYPES ~~~clojure (inferred-types - (defs) - (expressions)) + (defs + (patt (type "Error"))) + (expressions + (expr (type "Error")))) ~~~ diff --git a/test/snapshots/fuzz_crash/fuzz_crash_018.md b/test/snapshots/fuzz_crash/fuzz_crash_018.md index bd1c7b9016..b457375506 100644 --- a/test/snapshots/fuzz_crash/fuzz_crash_018.md +++ b/test/snapshots/fuzz_crash/fuzz_crash_018.md @@ -83,11 +83,18 @@ b : S ~~~ # CANONICALIZE ~~~clojure -(can-ir (empty true)) +(can-ir + (d-let + (p-assign (ident "b")) + (e-anno-only) + (annotation + (ty-malformed)))) ~~~ # TYPES ~~~clojure (inferred-types - (defs) - (expressions)) + (defs + (patt (type "Error"))) + (expressions + (expr (type "Error")))) ~~~ diff --git a/test/snapshots/fuzz_crash/fuzz_crash_019.md b/test/snapshots/fuzz_crash/fuzz_crash_019.md index a77b3cd864..187aa279f0 100644 --- a/test/snapshots/fuzz_crash/fuzz_crash_019.md +++ b/test/snapshots/fuzz_crash/fuzz_crash_019.md @@ -2007,14 +2007,14 @@ expect { ~~~clojure (inferred-types (defs - (patt (type "Error")) + (patt (type "()")) (patt (type "Bool -> num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) (patt (type "Error")) (patt (type "Bool -> Error")) (patt (type "[Blue]_others, [Tb]_others2 -> Error")) (patt (type "Error")) (patt (type "_arg -> [Stdo!(Error)]_others")) - (patt (type "Error")) + (patt (type "{ }")) (patt (type "{}")) (patt (type "Error"))) (type_decls @@ -2044,14 +2044,14 @@ expect { (ty-args (ty-rigid-var (name "a")))))) (expressions - (expr (type "Error")) + (expr (type "()")) (expr (type "Bool -> num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) (expr (type "Error")) (expr (type "Bool -> Error")) (expr (type "[Blue]_others, [Tb]_others2 -> Error")) (expr (type "Error")) (expr (type "_arg -> [Stdo!(Error)]_others")) - (expr (type "Error")) + (expr (type "{ }")) (expr (type "{}")) (expr (type "Error")))) ~~~ diff --git a/test/snapshots/fuzz_crash/fuzz_crash_020.md b/test/snapshots/fuzz_crash/fuzz_crash_020.md index 0922812a64..2840a7649f 100644 --- a/test/snapshots/fuzz_crash/fuzz_crash_020.md +++ b/test/snapshots/fuzz_crash/fuzz_crash_020.md @@ -1986,14 +1986,14 @@ expect { ~~~clojure (inferred-types (defs - (patt (type "Error")) + (patt (type "()")) (patt (type "Bool -> num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) (patt (type "Error")) (patt (type "[Rum]_others -> Error")) (patt (type "[Blue]_others -> Error")) (patt (type "Error")) (patt (type "_arg -> [Stdo!(Error)]_others")) - (patt (type "Error")) + (patt (type "{ }")) (patt (type "{}")) (patt (type "Error"))) (type_decls @@ -2023,14 +2023,14 @@ expect { (ty-args (ty-rigid-var (name "a")))))) (expressions - (expr (type "Error")) + (expr (type "()")) (expr (type "Bool -> num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) (expr (type "Error")) (expr (type "[Rum]_others -> Error")) (expr (type "[Blue]_others -> Error")) (expr (type "Error")) (expr (type "_arg -> [Stdo!(Error)]_others")) - (expr (type "Error")) + (expr (type "{ }")) (expr (type "{}")) (expr (type "Error")))) ~~~ diff --git a/test/snapshots/fuzz_crash/fuzz_crash_022.md b/test/snapshots/fuzz_crash/fuzz_crash_022.md index 701d19372d..fb5d884dc6 100644 --- a/test/snapshots/fuzz_crash/fuzz_crash_022.md +++ b/test/snapshots/fuzz_crash/fuzz_crash_022.md @@ -245,7 +245,7 @@ ain! = |_| getUser(900) (inferred-types (defs (patt (type "Error")) - (patt (type "Error")) + (patt (type "UserId -> Str")) (patt (type "_arg -> Error")) (patt (type "_arg -> Error"))) (type_decls @@ -253,7 +253,7 @@ ain! = |_| getUser(900) (ty-header (name "UserId")))) (expressions (expr (type "Error")) - (expr (type "Error")) + (expr (type "UserId -> Str")) (expr (type "_arg -> Error")) (expr (type "_arg -> Error")))) ~~~ diff --git a/test/snapshots/fuzz_crash/fuzz_crash_026.md b/test/snapshots/fuzz_crash/fuzz_crash_026.md index 31ff2b29709f174ca0746799fb660b2e991e5ca8..6ae9223b5d1cdbeb79df1b8438c7ede7ba16c7df 100644 GIT binary patch delta 36 ncmdn$wcTrjhYDM0acYrg%4A=ag`5zklBVY5dny`} num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) (patt (type "Num(Int(Unsigned64)) -> Num(Int(Unsigned64))")) (patt (type "[Red, Blue]_others, _arg -> Error")) @@ -2286,7 +2286,7 @@ expect { (ty-args (ty-rigid-var (name "a")))))) (expressions - (expr (type "Error")) + (expr (type "(Error, Error)")) (expr (type "Bool -> num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) (expr (type "Num(Int(Unsigned64)) -> Num(Int(Unsigned64))")) (expr (type "[Red, Blue]_others, _arg -> Error")) diff --git a/test/snapshots/fuzz_crash/fuzz_crash_028.md b/test/snapshots/fuzz_crash/fuzz_crash_028.md index 2611ecf986900d3718eeabb45ff0631745b1abe4..564b8d8700c2a514e5156a16897f165e749d4022 100644 GIT binary patch delta 224 zcmcbyh55!-<_*SI_%vLLit>we6u^|`pIbo@$lH9Yf5G(z)=Gt=`@Q_M^>H5GL2 z6tIZ{HLMl{Nrn_vYE&yI)aoe2r)W-&x@yb<;%%OE)qok@j*B;iC*QgePntbYO_Mj= Uw8UZ`NcPW71+bly6>pgX002@_mH+?% delta 92 zcmcbymHEaN<_*SICg)z2m>hB?9!>{dwVYgZRe18cs|p~o%}=k&GEcU-1yU3|+2fYr fWXD^=P&UtGg R.a.E # CANONICALIZE ~~~clojure (can-ir + (d-let + (p-assign (ident "g")) + (e-anno-only) + (annotation + (ty-fn (effectful false) + (ty-rigid-var (name "r")) + (ty-malformed)))) (s-import (module "u.R") (exposes))) ~~~ # TYPES ~~~clojure (inferred-types - (defs) - (expressions)) + (defs + (patt (type "r -> Error"))) + (expressions + (expr (type "r -> Error")))) ~~~ diff --git a/test/snapshots/fuzz_crash/fuzz_crash_043.md b/test/snapshots/fuzz_crash/fuzz_crash_043.md index 0be33a2e45..0643763136 100644 --- a/test/snapshots/fuzz_crash/fuzz_crash_043.md +++ b/test/snapshots/fuzz_crash/fuzz_crash_043.md @@ -102,11 +102,18 @@ o : ~~~ # CANONICALIZE ~~~clojure -(can-ir (empty true)) +(can-ir + (d-let + (p-assign (ident "o")) + (e-anno-only) + (annotation + (ty-malformed)))) ~~~ # TYPES ~~~clojure (inferred-types - (defs) - (expressions)) + (defs + (patt (type "Error"))) + (expressions + (expr (type "Error")))) ~~~ diff --git a/test/snapshots/fuzz_crash/fuzz_crash_048.md b/test/snapshots/fuzz_crash/fuzz_crash_048.md index 7539b8ba9c..5ac1049a26 100644 --- a/test/snapshots/fuzz_crash/fuzz_crash_048.md +++ b/test/snapshots/fuzz_crash/fuzz_crash_048.md @@ -157,21 +157,28 @@ tag_tuple : Value((a, b, c)) (ty-malformed)) (ty-apply (name "Result") (builtin) (ty-record) - (ty-underscore)))))) + (ty-underscore))))) + (d-let + (p-assign (ident "tag_tuple")) + (e-anno-only) + (annotation + (ty-malformed)))) ~~~ # TYPES ~~~clojure (inferred-types (defs + (patt (type "Num(Int(Unsigned64))")) (patt (type "Error")) - (patt (type "Error")) - (patt (type "Error")) - (patt (type "Error")) + (patt (type "(a, b, c)")) + (patt (type "Num(Int(Unsigned8)), Num(Int(Unsigned16)) -> Num(Int(Unsigned32))")) + (patt (type "List(Error) -> Try({ }, _d)")) (patt (type "Error"))) (expressions + (expr (type "Num(Int(Unsigned64))")) (expr (type "Error")) - (expr (type "Error")) - (expr (type "Error")) - (expr (type "Error")) + (expr (type "(a, b, c)")) + (expr (type "Num(Int(Unsigned8)), Num(Int(Unsigned16)) -> Num(Int(Unsigned32))")) + (expr (type "List(Error) -> Try({ }, _d)")) (expr (type "Error")))) ~~~ diff --git a/test/snapshots/fuzz_crash/fuzz_crash_049.md b/test/snapshots/fuzz_crash/fuzz_crash_049.md index 7c318353aab8aa23e7641034760ab6ba9ed694b7..e6029d0eea5ec22680747201f04df40fc788596e 100644 GIT binary patch delta 72 zcmZ3mntju1_J%Et?8o^uT#JhGi*yvgl;-q>CmDse%O7Xl#?Ff(GyTE^Mv>_OR~cER PD<5a{-|ldpF@X&L){Gj7 delta 33 pcmdnAntj1)_J%Et?8m2bpJ0^Ou5p}k8~gNA=Yh=s=NLD$0RZk14k!Qs diff --git a/test/snapshots/fuzz_crash/fuzz_crash_079.md b/test/snapshots/fuzz_crash/fuzz_crash_079.md index 763bd83f8a..5d1c7229bb 100644 --- a/test/snapshots/fuzz_crash/fuzz_crash_079.md +++ b/test/snapshots/fuzz_crash/fuzz_crash_079.md @@ -45,11 +45,18 @@ b : r ~~~ # CANONICALIZE ~~~clojure -(can-ir (empty true)) +(can-ir + (d-let + (p-assign (ident "b")) + (e-anno-only) + (annotation + (ty-rigid-var (name "r"))))) ~~~ # TYPES ~~~clojure (inferred-types - (defs) - (expressions)) + (defs + (patt (type "r"))) + (expressions + (expr (type "r")))) ~~~ diff --git a/test/snapshots/platform/platform_int.md b/test/snapshots/platform/platform_int.md index 10b4934bed..ab5254cd77 100644 --- a/test/snapshots/platform/platform_int.md +++ b/test/snapshots/platform/platform_int.md @@ -63,11 +63,21 @@ multiplyInts : I64, I64 -> I64 ~~~ # CANONICALIZE ~~~clojure -(can-ir (empty true)) +(can-ir + (d-let + (p-assign (ident "multiplyInts")) + (e-anno-only) + (annotation + (ty-fn (effectful false) + (ty-lookup (name "I64") (builtin)) + (ty-lookup (name "I64") (builtin)) + (ty-lookup (name "I64") (builtin)))))) ~~~ # TYPES ~~~clojure (inferred-types - (defs) - (expressions)) + (defs + (patt (type "Num(Int(Signed64)), Num(Int(Signed64)) -> Num(Int(Signed64))"))) + (expressions + (expr (type "Num(Int(Signed64)), Num(Int(Signed64)) -> Num(Int(Signed64))")))) ~~~ diff --git a/test/snapshots/platform/platform_str.md b/test/snapshots/platform/platform_str.md index b95e1d501f..94120c53ca 100644 --- a/test/snapshots/platform/platform_str.md +++ b/test/snapshots/platform/platform_str.md @@ -61,11 +61,20 @@ processString : Str -> Str ~~~ # CANONICALIZE ~~~clojure -(can-ir (empty true)) +(can-ir + (d-let + (p-assign (ident "processString")) + (e-anno-only) + (annotation + (ty-fn (effectful false) + (ty-lookup (name "Str") (builtin)) + (ty-lookup (name "Str") (builtin)))))) ~~~ # TYPES ~~~clojure (inferred-types - (defs) - (expressions)) + (defs + (patt (type "Str -> Str"))) + (expressions + (expr (type "Str -> Str")))) ~~~ diff --git a/test/snapshots/type_annotation_missing_parens.md b/test/snapshots/type_annotation_missing_parens.md index 6156718772..7b3824382a 100644 --- a/test/snapshots/type_annotation_missing_parens.md +++ b/test/snapshots/type_annotation_missing_parens.md @@ -9,6 +9,7 @@ nums : List U8 ~~~ # EXPECTED PARSE ERROR - type_annotation_missing_parens.md:2:1:2:1 +TOO FEW ARGS - type_annotation_missing_parens.md:1:8:1:12 # PROBLEMS **PARSE ERROR** Type applications require parentheses around their type arguments. @@ -33,6 +34,16 @@ Other valid examples: ^ +**TOO FEW ARGS** +The type _List_ expects argument, but got instead. +**type_annotation_missing_parens.md:1:8:1:12:** +```roc +nums : List U8 +``` + ^^^^ + + + # TOKENS ~~~zig LowerIdent,OpColon,UpperIdent,UpperIdent, @@ -53,11 +64,18 @@ nums : List ~~~ # CANONICALIZE ~~~clojure -(can-ir (empty true)) +(can-ir + (d-let + (p-assign (ident "nums")) + (e-anno-only) + (annotation + (ty-lookup (name "List") (builtin))))) ~~~ # TYPES ~~~clojure (inferred-types - (defs) - (expressions)) + (defs + (patt (type "Error"))) + (expressions + (expr (type "Error")))) ~~~ diff --git a/test/snapshots/type_annotations.md b/test/snapshots/type_annotations.md index c690c20373..5084ca529a 100644 --- a/test/snapshots/type_annotations.md +++ b/test/snapshots/type_annotations.md @@ -143,21 +143,28 @@ NO CHANGE (ty-malformed)) (ty-apply (name "Result") (builtin) (ty-record) - (ty-underscore)))))) + (ty-underscore))))) + (d-let + (p-assign (ident "tag_tuple")) + (e-anno-only) + (annotation + (ty-malformed)))) ~~~ # TYPES ~~~clojure (inferred-types (defs + (patt (type "Num(Int(Unsigned64))")) (patt (type "Error")) - (patt (type "Error")) - (patt (type "Error")) - (patt (type "Error")) + (patt (type "(_a, _b, _c)")) + (patt (type "Num(Int(Unsigned8)), Num(Int(Unsigned16)) -> Num(Int(Unsigned32))")) + (patt (type "List(Error) -> Try({ }, _a)")) (patt (type "Error"))) (expressions + (expr (type "Num(Int(Unsigned64))")) (expr (type "Error")) - (expr (type "Error")) - (expr (type "Error")) - (expr (type "Error")) + (expr (type "(_a, _b, _c)")) + (expr (type "Num(Int(Unsigned8)), Num(Int(Unsigned16)) -> Num(Int(Unsigned32))")) + (expr (type "List(Error) -> Try({ }, _a)")) (expr (type "Error")))) ~~~ diff --git a/test/snapshots/where_clause/where_clauses_10.md b/test/snapshots/where_clause/where_clauses_10.md index ccafb9e033..82afe21de1 100644 --- a/test/snapshots/where_clause/where_clauses_10.md +++ b/test/snapshots/where_clause/where_clauses_10.md @@ -71,6 +71,18 @@ decode_things # After member name # CANONICALIZE ~~~clojure (can-ir + (d-let + (p-assign (ident "decode_things")) + (e-anno-only) + (annotation + (ty-fn (effectful false) + (ty-apply (name "List") (builtin) + (ty-apply (name "List") (builtin) + (ty-lookup (name "U8") (builtin)))) + (ty-apply (name "List") (builtin) + (ty-rigid-var (name "a")))) + (where + (alias (ty-rigid-var-lookup (ty-rigid-var (name "a"))) (name "Decode"))))) (s-import (module "Decode") (exposes (exposed (name "Decode") (wildcard false))))) @@ -78,6 +90,8 @@ decode_things # After member name # TYPES ~~~clojure (inferred-types - (defs) - (expressions)) + (defs + (patt (type "List(List(Num(Int(Unsigned8)))) -> List(a)"))) + (expressions + (expr (type "List(List(Num(Int(Unsigned8)))) -> List(a)")))) ~~~ diff --git a/test/snapshots/where_clause/where_clauses_error_cases.md b/test/snapshots/where_clause/where_clauses_error_cases.md index d4490b8753..3ef4e7ec2f 100644 --- a/test/snapshots/where_clause/where_clauses_error_cases.md +++ b/test/snapshots/where_clause/where_clauses_error_cases.md @@ -194,15 +194,29 @@ broken_fn3 : a -> b (ty-rigid-var (name "a")) (ty-rigid-var (name "b"))) (where - (malformed))))) + (malformed)))) + (d-let + (p-assign (ident "broken_fn3")) + (e-anno-only) + (annotation + (ty-fn (effectful false) + (ty-rigid-var (name "a")) + (ty-rigid-var (name "b"))) + (where + (method (ty-rigid-var (name "c")) (name "method") + (args + (ty-rigid-var-lookup (ty-rigid-var (name "c")))) + (ty-rigid-var (name "d"))))))) ~~~ # TYPES ~~~clojure (inferred-types (defs - (patt (type "Error")) - (patt (type "Error"))) + (patt (type "a -> b")) + (patt (type "a -> b")) + (patt (type "a -> b"))) (expressions - (expr (type "Error")) - (expr (type "Error")))) + (expr (type "a -> b")) + (expr (type "a -> b")) + (expr (type "a -> b")))) ~~~ From 2da6c7d1b28f73f3bccbbe4e1e8e81145a085425 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 7 Nov 2025 16:28:57 -0500 Subject: [PATCH 04/38] Move list_get_unsafe to top level --- src/build/builtin_compiler/main.zig | 4 ++-- src/build/roc/Builtin.roc | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/build/builtin_compiler/main.zig b/src/build/builtin_compiler/main.zig index 8213494948..7710f0c8a7 100644 --- a/src/build/builtin_compiler/main.zig +++ b/src/build/builtin_compiler/main.zig @@ -158,8 +158,8 @@ fn replaceStrIsEmptyWithLowLevel(env: *ModuleEnv) !std.ArrayList(CIR.Def.Idx) { // Note: List.get is now a real function implementation in Builtin.roc, // not a bare type annotation, so it's not in the low_level_map - // list_get_unsafe is a top-level function in Builtin module - if (env.common.findIdent("Builtin.list_get_unsafe")) |list_get_unsafe_ident| { + // list_get_unsafe is a top-level function outside the Builtin type + if (env.common.findIdent("list_get_unsafe")) |list_get_unsafe_ident| { try low_level_map.put(list_get_unsafe_ident, .list_get_unsafe); } if (env.common.findIdent("Builtin.Set.is_empty")) |set_is_empty_ident| { diff --git a/src/build/roc/Builtin.roc b/src/build/roc/Builtin.roc index 94658b007f..0cc111a9bd 100644 --- a/src/build/roc/Builtin.roc +++ b/src/build/roc/Builtin.roc @@ -1,3 +1,7 @@ +# Private top-level function for unsafe list access +# This is a low-level operation that gets replaced by the compiler +list_get_unsafe : List(elem), U64 -> elem + Builtin := [].{ Str := [ProvidedByCompiler].{ is_empty : Str -> Bool @@ -6,10 +10,6 @@ Builtin := [].{ contains = |_str, _other| True } - # Private top-level function for unsafe list access - # This is a low-level operation that gets replaced by the compiler - list_get_unsafe : List(elem), U64 -> elem - List := [ProvidedByCompiler].{ len : List(_elem) -> U64 is_empty : List(_elem) -> Bool From 858f32137459ebc3bad21a4090a0aa8515991f80 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 7 Nov 2025 19:52:03 -0500 Subject: [PATCH 05/38] Clean up some stuff --- src/build/builtin_compiler/main.zig | 6 ------ src/build/roc/Builtin.roc | 18 +++++------------- 2 files changed, 5 insertions(+), 19 deletions(-) diff --git a/src/build/builtin_compiler/main.zig b/src/build/builtin_compiler/main.zig index 7710f0c8a7..d874098cac 100644 --- a/src/build/builtin_compiler/main.zig +++ b/src/build/builtin_compiler/main.zig @@ -155,18 +155,12 @@ fn replaceStrIsEmptyWithLowLevel(env: *ModuleEnv) !std.ArrayList(CIR.Def.Idx) { if (env.common.findIdent("Builtin.List.is_empty")) |list_is_empty_ident| { try low_level_map.put(list_is_empty_ident, .list_is_empty); } - // Note: List.get is now a real function implementation in Builtin.roc, - // not a bare type annotation, so it's not in the low_level_map - - // list_get_unsafe is a top-level function outside the Builtin type if (env.common.findIdent("list_get_unsafe")) |list_get_unsafe_ident| { try low_level_map.put(list_get_unsafe_ident, .list_get_unsafe); } if (env.common.findIdent("Builtin.Set.is_empty")) |set_is_empty_ident| { try low_level_map.put(set_is_empty_ident, .set_is_empty); } - - // Bool operations if (env.common.findIdent("Builtin.Bool.is_eq")) |bool_is_eq_ident| { try low_level_map.put(bool_is_eq_ident, .bool_is_eq); } diff --git a/src/build/roc/Builtin.roc b/src/build/roc/Builtin.roc index 0cc111a9bd..5e2becd68c 100644 --- a/src/build/roc/Builtin.roc +++ b/src/build/roc/Builtin.roc @@ -18,19 +18,11 @@ Builtin := [].{ first = |list| List.get(list, 0) get : List(elem), U64 -> Try(elem, [ListWasEmpty]) - get = |list, index| - if List.is_empty(list) { - Try.Err(ListWasEmpty) - } else { - # list_get_unsafe is a top-level low-level function - # It returns the element if in bounds, or crashes if out of bounds - # We need to check bounds first - if index >= List.len(list) { - Try.Err(ListWasEmpty) - } else { - Try.Ok(list_get_unsafe(list, index)) - } - } + get = |list, index| if index < List.len(list) { + Try.Ok(list_get_unsafe(list, index)) + } else { + Try.Err(ListWasEmpty) + } map : List(a), (a -> b) -> List(b) map = |_, _| [] From dede58b2f96963022c815bf21f75542a9b28989f Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 7 Nov 2025 19:54:06 -0500 Subject: [PATCH 06/38] Move list_get_unsafe to end of file --- src/build/roc/Builtin.roc | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/build/roc/Builtin.roc b/src/build/roc/Builtin.roc index 5e2becd68c..0ec1278c6d 100644 --- a/src/build/roc/Builtin.roc +++ b/src/build/roc/Builtin.roc @@ -1,7 +1,3 @@ -# Private top-level function for unsafe list access -# This is a low-level operation that gets replaced by the compiler -list_get_unsafe : List(elem), U64 -> elem - Builtin := [].{ Str := [ProvidedByCompiler].{ is_empty : Str -> Bool @@ -343,3 +339,7 @@ Builtin := [].{ } } } + +# Private top-level function for unsafe list access +# This is a low-level operation that gets replaced by the compiler +list_get_unsafe : List(elem), U64 -> elem From 6aab4764f51e90cbe6c139c3107045c8dedf3d38 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 7 Nov 2025 19:56:20 -0500 Subject: [PATCH 07/38] Add everything to scope as appropriate --- src/canonicalize/Can.zig | 69 +++++++++++++++++++++++----------------- 1 file changed, 39 insertions(+), 30 deletions(-) diff --git a/src/canonicalize/Can.zig b/src/canonicalize/Can.zig index 0dd950a4a5..7b2a79e684 100644 --- a/src/canonicalize/Can.zig +++ b/src/canonicalize/Can.zig @@ -155,6 +155,39 @@ const TypeBindingLocationConst = struct { binding: *const Scope.TypeBinding, }; +/// Add all qualified variants of a name to the current scope. +/// For a name like "Builtin.List.get", this adds: +/// - "get" (unqualified) +/// - "List.get" (1 level of qualification) +/// - "Builtin.List.get" (full qualification) +/// Works for any depth of nesting. +fn addAllQualifiedVariants( + self: *Self, + parent_full_text: []const u8, + name_text: []const u8, + pattern_idx: Pattern.Idx, +) !void { + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + + // Add unqualified name (e.g., "get") + const name_ident = try self.env.insertIdent(base.Ident.for_text(name_text)); + try current_scope.idents.put(self.env.gpa, name_ident, pattern_idx); + + // Add all qualified variants by iterating through dots + // For "Builtin.List", add "List.get" and "Builtin.List.get" + var start: usize = 0; + while (std.mem.indexOfScalarPos(u8, parent_full_text, start, '.')) |dot_pos| { + const prefix = parent_full_text[dot_pos + 1..]; + const qualified_idx = try self.env.insertQualifiedIdent(prefix, name_text); + try current_scope.idents.put(self.env.gpa, qualified_idx, pattern_idx); + start = dot_pos + 1; + } + + // Add the full qualified name (e.g., "Builtin.List.get") + const full_qualified_idx = try self.env.insertQualifiedIdent(parent_full_text, name_text); + try current_scope.idents.put(self.env.gpa, full_qualified_idx, pattern_idx); +} + /// Deinitialize canonicalizer resources pub fn deinit( self: *Self, @@ -694,24 +727,12 @@ fn processAssociatedItemsSecondPass( const def_idx_u16: u16 = @intCast(@intFromEnum(def_idx)); try self.env.setExposedNodeIndexById(qualified_idx, def_idx_u16); - // Also make the unqualified name and short qualified name available in the current scope - // (This allows `get`, `List.get`, and `Builtin.List.get` to all work) + // Also make all qualified variants available in the current scope + // (This allows `get`, `List.get`, `Builtin.List.get`, etc. to all work) const def_cir = self.env.store.getDef(def_idx); const pattern_idx = def_cir.pattern; - const current_scope = &self.scopes.items[self.scopes.items.len - 1]; - - // Add unqualified name (e.g., "get") - try current_scope.idents.put(self.env.gpa, name_ident, pattern_idx); - - // Also add short qualified name (e.g., "List.get") - // Extract the last component of parent_name (e.g., "List" from "Builtin.List") const parent_full_text = self.env.getIdent(parent_name); - const short_parent_text = if (std.mem.lastIndexOf(u8, parent_full_text, ".")) |last_dot| - parent_full_text[last_dot + 1 ..] - else - parent_full_text; - const short_qualified_idx = try self.env.insertQualifiedIdent(short_parent_text, decl_text); - try current_scope.idents.put(self.env.gpa, short_qualified_idx, pattern_idx); + try self.addAllQualifiedVariants(parent_full_text, decl_text, pattern_idx); } else {} } } @@ -734,24 +755,12 @@ fn processAssociatedItemsSecondPass( const def_idx_u16: u16 = @intCast(@intFromEnum(def_idx)); try self.env.setExposedNodeIndexById(qualified_idx, def_idx_u16); - // Also make the unqualified name and short qualified name available in the current scope - // (This allows `is_empty`, `List.is_empty`, and `Builtin.List.is_empty` to all work) + // Also make all qualified variants available in the current scope + // (This allows `is_empty`, `List.is_empty`, `Builtin.List.is_empty`, etc. to all work) const def_cir = self.env.store.getDef(def_idx); const pattern_idx = def_cir.pattern; - const current_scope = &self.scopes.items[self.scopes.items.len - 1]; - - // Add unqualified name (e.g., "is_empty") - try current_scope.idents.put(self.env.gpa, name_ident, pattern_idx); - - // Also add short qualified name (e.g., "List.is_empty") - // Extract the last component of parent_name (e.g., "List" from "Builtin.List") const parent_full_text = self.env.getIdent(parent_name); - const short_parent_text = if (std.mem.lastIndexOf(u8, parent_full_text, ".")) |last_dot| - parent_full_text[last_dot + 1 ..] - else - parent_full_text; - const short_qualified_idx = try self.env.insertQualifiedIdent(short_parent_text, name_text); - try current_scope.idents.put(self.env.gpa, short_qualified_idx, pattern_idx); + try self.addAllQualifiedVariants(parent_full_text, name_text, pattern_idx); try self.env.store.addScratchDef(def_idx); }, From 11940b1db8d11260060d5a44035b10590b8262b4 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 7 Nov 2025 19:56:37 -0500 Subject: [PATCH 08/38] Remove an unnecessary comment --- src/canonicalize/Expression.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/canonicalize/Expression.zig b/src/canonicalize/Expression.zig index 44af97e71c..36d15ef5aa 100644 --- a/src/canonicalize/Expression.zig +++ b/src/canonicalize/Expression.zig @@ -390,7 +390,7 @@ pub const Expr = union(enum) { // List operations list_len, list_is_empty, - list_get_unsafe, // Internal only - returns element or crashes if out of bounds + list_get_unsafe, // Set operations set_is_empty, From 23551a96e50cfd4380a03194a80191790c680100 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 7 Nov 2025 19:56:55 -0500 Subject: [PATCH 09/38] zig fmt --- src/canonicalize/Can.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/canonicalize/Can.zig b/src/canonicalize/Can.zig index 7b2a79e684..c42ce03009 100644 --- a/src/canonicalize/Can.zig +++ b/src/canonicalize/Can.zig @@ -177,7 +177,7 @@ fn addAllQualifiedVariants( // For "Builtin.List", add "List.get" and "Builtin.List.get" var start: usize = 0; while (std.mem.indexOfScalarPos(u8, parent_full_text, start, '.')) |dot_pos| { - const prefix = parent_full_text[dot_pos + 1..]; + const prefix = parent_full_text[dot_pos + 1 ..]; const qualified_idx = try self.env.insertQualifiedIdent(prefix, name_text); try current_scope.idents.put(self.env.gpa, qualified_idx, pattern_idx); start = dot_pos + 1; From 80a15a27e33fe7804b37bddce40ff2b1eb365cd0 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 7 Nov 2025 20:51:58 -0500 Subject: [PATCH 10/38] Fix some deserialization --- src/canonicalize/Can.zig | 4 +++- src/canonicalize/ModuleEnv.zig | 21 +++++++++------------ src/check/Check.zig | 6 +++--- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/canonicalize/Can.zig b/src/canonicalize/Can.zig index c42ce03009..6eee00f3b0 100644 --- a/src/canonicalize/Can.zig +++ b/src/canonicalize/Can.zig @@ -170,7 +170,9 @@ fn addAllQualifiedVariants( const current_scope = &self.scopes.items[self.scopes.items.len - 1]; // Add unqualified name (e.g., "get") - const name_ident = try self.env.insertIdent(base.Ident.for_text(name_text)); + // Use findIdent first to reuse existing ident if present, otherwise insert + const name_ident = self.env.common.findIdent(name_text) orelse + try self.env.insertIdent(base.Ident.for_text(name_text)); try current_scope.idents.put(self.env.gpa, name_ident, pattern_idx); // Add all qualified variants by iterating through dots diff --git a/src/canonicalize/ModuleEnv.zig b/src/canonicalize/ModuleEnv.zig index a7fd480a67..56c3de04cd 100644 --- a/src/canonicalize/ModuleEnv.zig +++ b/src/canonicalize/ModuleEnv.zig @@ -1587,9 +1587,12 @@ pub const Serialized = struct { // Overwrite ourself with the deserialized version, and return our pointer after casting it to Self. const env = @as(*Self, @ptrFromInt(@intFromPtr(self))); + // Deserialize common env first so we can look up identifiers + const common = self.common.deserialize(offset, source).*; + env.* = Self{ .gpa = gpa, - .common = self.common.deserialize(offset, source).*, + .common = common, .types = self.types.deserialize(offset, gpa).*, .module_kind = self.module_kind, .all_defs = self.all_defs, @@ -1604,17 +1607,11 @@ pub const Serialized = struct { .store = self.store.deserialize(offset, gpa).*, .evaluation_order = null, // Not serialized, will be recomputed if needed // Well-known identifiers for type checking - look them up in the deserialized common env - // If not found, insert them (this can happen if Builtin.roc doesn't reference them) - .from_int_digits_ident = env.common.findIdent(Ident.FROM_INT_DIGITS_METHOD_NAME) orelse - try env.common.insertIdent(gpa, Ident.for_text(Ident.FROM_INT_DIGITS_METHOD_NAME)), - .from_dec_digits_ident = env.common.findIdent(Ident.FROM_DEC_DIGITS_METHOD_NAME) orelse - try env.common.insertIdent(gpa, Ident.for_text(Ident.FROM_DEC_DIGITS_METHOD_NAME)), - .try_ident = env.common.findIdent("Try") orelse - try env.common.insertIdent(gpa, Ident.for_text("Try")), - .out_of_range_ident = env.common.findIdent("OutOfRange") orelse - try env.common.insertIdent(gpa, Ident.for_text("OutOfRange")), - .builtin_module_ident = env.common.findIdent("Builtin") orelse - try env.common.insertIdent(gpa, Ident.for_text("Builtin")), + .from_int_digits_ident = common.findIdent(Ident.FROM_INT_DIGITS_METHOD_NAME) orelse unreachable, + .from_dec_digits_ident = common.findIdent(Ident.FROM_DEC_DIGITS_METHOD_NAME) orelse unreachable, + .try_ident = common.findIdent("Try") orelse unreachable, + .out_of_range_ident = common.findIdent("OutOfRange") orelse unreachable, + .builtin_module_ident = common.findIdent("Builtin") orelse unreachable, }; return env; diff --git a/src/check/Check.zig b/src/check/Check.zig index 13a20cb758..ff22566c48 100644 --- a/src/check/Check.zig +++ b/src/check/Check.zig @@ -591,7 +591,7 @@ fn updateVar(self: *Self, target_var: Var, content: types_mod.Content, rank: typ // file // /// Check the types for all defs -/// Copy builtin types (Bool, Result) from their modules into the current module's type store +/// Copy builtin types from their modules into the current module's type store /// This is necessary because type variables are module-specific - we can't use Vars from /// other modules directly. The Bool and Result types are used in language constructs like /// `if` conditions and need to be available in every module's type store. @@ -645,7 +645,7 @@ pub fn checkFile(self: *Self) std.mem.Allocator.Error!void { try self.generateStmtTypeDeclType(stmt_idx); } - // Copy builtin types (Bool, Result) into this module's type store + // Copy builtin types into this module's type store // This must happen AFTER type declarations are generated so that when compiling // Builtin itself, the Bool and Try types have already been created try self.copyBuiltinTypes(); @@ -701,7 +701,7 @@ pub fn checkFile(self: *Self) std.mem.Allocator.Error!void { pub fn checkExprRepl(self: *Self, expr_idx: CIR.Expr.Idx) std.mem.Allocator.Error!void { try ensureTypeStoreIsFilled(self); - // Copy builtin types (Bool, Result) into this module's type store + // Copy builtin types into this module's type store try self.copyBuiltinTypes(); // First, iterate over the statements, generating types for each type declaration From fbac5abcf47174b05586c62146d9d903b83d04a8 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 7 Nov 2025 21:18:19 -0500 Subject: [PATCH 11/38] Change some things to debug assertions --- src/eval/interpreter.zig | 54 +++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/src/eval/interpreter.zig b/src/eval/interpreter.zig index be24c3f8d2..834613333d 100644 --- a/src/eval/interpreter.zig +++ b/src/eval/interpreter.zig @@ -2109,10 +2109,10 @@ pub const Interpreter = struct { switch (op) { .str_is_empty => { // Str.is_empty : Str -> Bool - if (args.len != 1) return error.TypeMismatch; + std.debug.assert(args.len == 1); // low-level .str_is_empty expects 1 argument const str_arg = args[0]; - if (str_arg.ptr == null) return error.TypeMismatch; + std.debug.assert(str_arg.ptr != null); // low-level .str_is_empty expects non-null string pointer const roc_str: *const RocStr = @ptrCast(@alignCast(str_arg.ptr.?)); const result = builtins.str.isEmpty(roc_str.*); @@ -2123,10 +2123,10 @@ pub const Interpreter = struct { // List.len : List(a) -> U64 // Note: listLen returns usize, but List.len always returns U64. // We need to cast usize -> u64 for 32-bit targets (e.g. wasm32). - if (args.len != 1) return error.TypeMismatch; + std.debug.assert(args.len == 1); // low-level .list_len expects 1 argument const list_arg = args[0]; - if (list_arg.ptr == null) return error.TypeMismatch; + std.debug.assert(list_arg.ptr != null); // low-level .list_len expects non-null list pointer const roc_list: *const builtins.list.RocList = @ptrCast(@alignCast(list_arg.ptr.?)); const len_usize = builtins.list.listLen(roc_list.*); @@ -2141,10 +2141,10 @@ pub const Interpreter = struct { }, .list_is_empty => { // List.is_empty : List(a) -> Bool - if (args.len != 1) return error.TypeMismatch; + std.debug.assert(args.len == 1); // low-level .list_is_empty expects 1 argument const list_arg = args[0]; - if (list_arg.ptr == null) return error.TypeMismatch; + std.debug.assert(list_arg.ptr != null); // low-level .list_is_empty expects non-null list pointer const roc_list: *const builtins.list.RocList = @ptrCast(@alignCast(list_arg.ptr.?)); const result = builtins.list.listIsEmpty(roc_list.*); @@ -2155,17 +2155,15 @@ pub const Interpreter = struct { // Internal operation: Get element at index without bounds checking // Args: List(a), U64 (index) // Returns: a (the element) - if (args.len != 2) return error.TypeMismatch; + std.debug.assert(args.len == 2); // low-level .list_get_unsafe expects 2 arguments const list_arg = args[0]; const index_arg = args[1]; - if (list_arg.ptr == null) return error.TypeMismatch; + std.debug.assert(list_arg.ptr != null); // low-level .list_get_unsafe expects non-null list pointer // Extract element layout from List(a) - if (list_arg.layout.tag != .list and list_arg.layout.tag != .list_of_zst) { - return error.TypeMismatch; - } + 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 @@ -2211,7 +2209,7 @@ pub const Interpreter = struct { // Bool operations .bool_is_eq => { // Bool.is_eq : Bool, Bool -> Bool - if (args.len != 2) return error.TypeMismatch; + std.debug.assert(args.len == 2); // low-level .bool_is_eq expects 2 arguments const lhs = args[0].asBool(); const rhs = args[1].asBool(); const result = lhs == rhs; @@ -2219,7 +2217,7 @@ pub const Interpreter = struct { }, .bool_is_ne => { // Bool.is_ne : Bool, Bool -> Bool - if (args.len != 2) return error.TypeMismatch; + std.debug.assert(args.len == 2); // low-level .bool_is_ne expects 2 arguments const lhs = args[0].asBool(); const rhs = args[1].asBool(); const result = lhs != rhs; @@ -2229,7 +2227,7 @@ pub const Interpreter = struct { // Numeric type checking operations .num_is_zero => { // num.is_zero : num -> Bool - if (args.len != 1) return error.TypeMismatch; + std.debug.assert(args.len == 1); // low-level .num_is_zero expects 1 argument const num_val = try self.extractNumericValue(args[0]); const result = switch (num_val) { .int => |i| i == 0, @@ -2241,7 +2239,7 @@ pub const Interpreter = struct { }, .num_is_negative => { // num.is_negative : num -> Bool (signed types only) - if (args.len != 1) return error.TypeMismatch; + std.debug.assert(args.len == 1); // low-level .num_is_negative expects 1 argument const num_val = try self.extractNumericValue(args[0]); const result = switch (num_val) { .int => |i| i < 0, @@ -2253,7 +2251,7 @@ pub const Interpreter = struct { }, .num_is_positive => { // num.is_positive : num -> Bool (signed types only) - if (args.len != 1) return error.TypeMismatch; + std.debug.assert(args.len == 1); // low-level .num_is_positive expects 1 argument const num_val = try self.extractNumericValue(args[0]); const result = switch (num_val) { .int => |i| i > 0, @@ -2267,7 +2265,7 @@ pub const Interpreter = struct { // Numeric comparison operations .num_is_eq => { // num.is_eq : num, num -> Bool (all integer types + Dec, NOT F32/F64) - if (args.len != 2) return error.TypeMismatch; + std.debug.assert(args.len == 2); // low-level .num_is_eq expects 2 arguments const lhs = try self.extractNumericValue(args[0]); const rhs = try self.extractNumericValue(args[1]); const result = switch (lhs) { @@ -2282,7 +2280,7 @@ pub const Interpreter = struct { }, .num_is_ne => { // num.is_ne : num, num -> Bool (Dec only) - if (args.len != 2) return error.TypeMismatch; + std.debug.assert(args.len == 2); // low-level .num_is_ne expects 2 arguments const lhs = try self.extractNumericValue(args[0]); const rhs = try self.extractNumericValue(args[1]); const result = switch (lhs) { @@ -2296,7 +2294,7 @@ pub const Interpreter = struct { }, .num_is_gt => { // num.is_gt : num, num -> Bool - if (args.len != 2) return error.TypeMismatch; + std.debug.assert(args.len == 2); // low-level .num_is_gt expects 2 arguments const lhs = try self.extractNumericValue(args[0]); const rhs = try self.extractNumericValue(args[1]); const result = switch (lhs) { @@ -2309,7 +2307,7 @@ pub const Interpreter = struct { }, .num_is_gte => { // num.is_gte : num, num -> Bool - if (args.len != 2) return error.TypeMismatch; + std.debug.assert(args.len == 2); // low-level .num_is_gte expects 2 arguments const lhs = try self.extractNumericValue(args[0]); const rhs = try self.extractNumericValue(args[1]); const result = switch (lhs) { @@ -2322,7 +2320,7 @@ pub const Interpreter = struct { }, .num_is_lt => { // num.is_lt : num, num -> Bool - if (args.len != 2) return error.TypeMismatch; + std.debug.assert(args.len == 2); // low-level .num_is_lt expects 2 arguments const lhs = try self.extractNumericValue(args[0]); const rhs = try self.extractNumericValue(args[1]); const result = switch (lhs) { @@ -2335,7 +2333,7 @@ pub const Interpreter = struct { }, .num_is_lte => { // num.is_lte : num, num -> Bool - if (args.len != 2) return error.TypeMismatch; + std.debug.assert(args.len == 2); // low-level .num_is_lte expects 2 arguments const lhs = try self.extractNumericValue(args[0]); const rhs = try self.extractNumericValue(args[1]); const result = switch (lhs) { @@ -2350,7 +2348,7 @@ pub const Interpreter = struct { // Numeric arithmetic operations .num_negate => { // num.negate : num -> num (signed types only) - if (args.len != 1) return error.TypeMismatch; + std.debug.assert(args.len == 1); // low-level .num_negate expects 1 argument const num_val = try self.extractNumericValue(args[0]); const result_layout = args[0].layout; @@ -2367,7 +2365,7 @@ pub const Interpreter = struct { return out; }, .num_plus => { - if (args.len != 2) return error.TypeMismatch; + std.debug.assert(args.len == 2); // low-level .num_plus expects 2 arguments const lhs = try self.extractNumericValue(args[0]); const rhs = try self.extractNumericValue(args[1]); const result_layout = args[0].layout; @@ -2385,7 +2383,7 @@ pub const Interpreter = struct { return out; }, .num_minus => { - if (args.len != 2) return error.TypeMismatch; + std.debug.assert(args.len == 2); // low-level .num_minus expects 2 arguments const lhs = try self.extractNumericValue(args[0]); const rhs = try self.extractNumericValue(args[1]); const result_layout = args[0].layout; @@ -2403,7 +2401,7 @@ pub const Interpreter = struct { return out; }, .num_times => { - if (args.len != 2) return error.TypeMismatch; + std.debug.assert(args.len == 2); // low-level .num_times expects 2 arguments const lhs = try self.extractNumericValue(args[0]); const rhs = try self.extractNumericValue(args[1]); const result_layout = args[0].layout; @@ -2421,7 +2419,7 @@ pub const Interpreter = struct { return out; }, .num_div_by => { - if (args.len != 2) return error.TypeMismatch; + std.debug.assert(args.len == 2); // low-level .num_div_by expects 2 arguments const lhs = try self.extractNumericValue(args[0]); const rhs = try self.extractNumericValue(args[1]); const result_layout = args[0].layout; @@ -2452,7 +2450,7 @@ pub const Interpreter = struct { return out; }, .num_rem_by => { - if (args.len != 2) return error.TypeMismatch; + std.debug.assert(args.len == 2); // low-level .num_rem_by expects 2 arguments const lhs = try self.extractNumericValue(args[0]); const rhs = try self.extractNumericValue(args[1]); const result_layout = args[0].layout; From 99b2032059ad62fb63e47a2c03bbf4dea5180a9a Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 7 Nov 2025 21:24:47 -0500 Subject: [PATCH 12/38] Drop some comments --- src/canonicalize/Can.zig | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/canonicalize/Can.zig b/src/canonicalize/Can.zig index 6eee00f3b0..e98d098bf4 100644 --- a/src/canonicalize/Can.zig +++ b/src/canonicalize/Can.zig @@ -876,8 +876,6 @@ fn processAssociatedItemsFirstPass( }, else => { // Skip other statement types in first pass - // Note: .type_anno is skipped here because anno-only patterns are created - // in the second pass, not the first pass }, } } From b217547e67ead80cd600325a69ef57fbe4e22903 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 7 Nov 2025 22:21:07 -0500 Subject: [PATCH 13/38] Reduce canonicalization passes --- src/canonicalize/Can.zig | 282 +++++++++++++++++++-------------------- 1 file changed, 137 insertions(+), 145 deletions(-) diff --git a/src/canonicalize/Can.zig b/src/canonicalize/Can.zig index e98d098bf4..7f2d4b7d25 100644 --- a/src/canonicalize/Can.zig +++ b/src/canonicalize/Can.zig @@ -543,11 +543,145 @@ fn processTypeDeclFirstPass( const type_text = self.env.getIdent(type_header.name); _ = self.exposed_type_texts.remove(type_text); - // Process associated items recursively in the first pass to introduce names - // Aliases are introduced in the current scope (not a nested scope) during first pass - // They will be available when we process the associated block in the second pass + // Process associated items completely (both symbol introduction and canonicalization) + // This eliminates the need for a separate third pass if (type_decl.associated) |assoc| { + // First, introduce placeholder patterns for all associated items try self.processAssociatedItemsFirstPass(qualified_name_idx, assoc.statements); + + // Now enter a new scope for the associated block where both qualified and unqualified names work + try self.scopeEnter(self.env.gpa, false); // false = not a function boundary + defer self.scopeExit(self.env.gpa) catch unreachable; + + // First, introduce the parent type itself into this scope so it can be referenced by its unqualified name + // For example, if we're processing MyBool's associated items, we need "MyBool" to resolve to "Test.MyBool" + if (self.scopeLookupTypeDecl(qualified_name_idx)) |parent_type_decl_idx| { + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + try current_scope.introduceTypeAlias(self.env.gpa, type_header.name, parent_type_decl_idx); + } + + // Re-introduce the aliases from first pass into this scope + // (We need to rebuild them since we're in a new scope) + for (self.parse_ir.store.statementSlice(assoc.statements)) |assoc_stmt_idx| { + const assoc_stmt = self.parse_ir.store.getStatement(assoc_stmt_idx); + switch (assoc_stmt) { + .type_decl => |nested_type_decl| { + const nested_header = self.parse_ir.store.getTypeHeader(nested_type_decl.header) catch continue; + const unqualified_ident = self.parse_ir.tokens.resolveIdentifier(nested_header.name) orelse continue; + + // Build qualified name + const parent_text = self.env.getIdent(qualified_name_idx); + const nested_type_text = self.env.getIdent(unqualified_ident); + const qualified_ident_idx = try self.env.insertQualifiedIdent(parent_text, nested_type_text); + + // Look up and alias + if (self.scopeLookupTypeDecl(qualified_ident_idx)) |qualified_type_decl_idx| { + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + try current_scope.introduceTypeAlias(self.env.gpa, unqualified_ident, qualified_type_decl_idx); + } + + // Also re-introduce associated items of this nested type + // so that both `my_not` and `MyBool.my_not` work within the associated block + if (nested_type_decl.associated) |nested_assoc| { + for (self.parse_ir.store.statementSlice(nested_assoc.statements)) |nested_assoc_stmt_idx| { + const nested_assoc_stmt = self.parse_ir.store.getStatement(nested_assoc_stmt_idx); + if (nested_assoc_stmt == .decl) { + const nested_decl = nested_assoc_stmt.decl; + const nested_pattern = self.parse_ir.store.getPattern(nested_decl.pattern); + if (nested_pattern == .ident) { + const nested_pattern_ident_tok = nested_pattern.ident.ident_tok; + if (self.parse_ir.tokens.resolveIdentifier(nested_pattern_ident_tok)) |nested_decl_ident| { + // Build fully qualified name (e.g., "Test.MyBool.my_not") + const qualified_text = self.env.getIdent(qualified_ident_idx); + const nested_decl_text = self.env.getIdent(nested_decl_ident); + const full_qualified_ident_idx = try self.env.insertQualifiedIdent(qualified_text, nested_decl_text); + + // Look up the fully qualified pattern + switch (self.scopeLookup(.ident, full_qualified_ident_idx)) { + .found => |pattern_idx| { + const scope = &self.scopes.items[self.scopes.items.len - 1]; + // Add unqualified name (e.g., "my_not") + try scope.idents.put(self.env.gpa, nested_decl_ident, pattern_idx); + // Also add name qualified with nested type (e.g., "MyBool.my_not") + const nested_decl_text2 = self.env.getIdent(nested_decl_ident); + const nested_qualified_ident_idx = try self.env.insertQualifiedIdent(nested_type_text, nested_decl_text2); + try scope.idents.put(self.env.gpa, nested_qualified_ident_idx, pattern_idx); + }, + .not_found => {}, + } + } + } + } + } + } + }, + .decl => |decl| { + const pattern = self.parse_ir.store.getPattern(decl.pattern); + if (pattern == .ident) { + const pattern_ident_tok = pattern.ident.ident_tok; + if (self.parse_ir.tokens.resolveIdentifier(pattern_ident_tok)) |decl_ident| { + // Build qualified name + const parent_text = self.env.getIdent(qualified_name_idx); + const decl_text = self.env.getIdent(decl_ident); + const qualified_ident_idx = try self.env.insertQualifiedIdent(parent_text, decl_text); + + // Look up the qualified pattern + switch (self.scopeLookup(.ident, qualified_ident_idx)) { + .found => |pattern_idx| { + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + // Add both unqualified and qualified names to the current scope + // This allows both `my_not` and `MyBool.my_not` to work inside the associated block + try current_scope.idents.put(self.env.gpa, decl_ident, pattern_idx); + try current_scope.idents.put(self.env.gpa, qualified_ident_idx, pattern_idx); + }, + .not_found => {}, + } + } + } + }, + else => { + // Note: .type_anno is not handled here because anno-only patterns + // are created during processAssociatedItemsSecondPass, so they need + // to be re-introduced AFTER that call completes + }, + } + } + + // Process the associated items (canonicalize their bodies) + try self.processAssociatedItemsSecondPass(qualified_name_idx, assoc.statements); + + // After processing, re-introduce anno-only defs into the associated block scope + // (They were just created by processAssociatedItemsSecondPass and need to be available + // for use within the associated block) + for (self.parse_ir.store.statementSlice(assoc.statements)) |anno_stmt_idx| { + const anno_stmt = self.parse_ir.store.getStatement(anno_stmt_idx); + switch (anno_stmt) { + .type_anno => |type_anno| { + if (self.parse_ir.tokens.resolveIdentifier(type_anno.name)) |anno_ident| { + // Build qualified name + const parent_text = self.env.getIdent(qualified_name_idx); + const anno_text = self.env.getIdent(anno_ident); + const qualified_ident_idx = try self.env.insertQualifiedIdent(parent_text, anno_text); + + // Look up the qualified pattern that was just created + switch (self.scopeLookup(.ident, qualified_ident_idx)) { + .found => |pattern_idx| { + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + // Add both unqualified and qualified names to the current scope + // This allows both `len` and `List.len` to work inside the associated block + try current_scope.idents.put(self.env.gpa, anno_ident, pattern_idx); + try current_scope.idents.put(self.env.gpa, qualified_ident_idx, pattern_idx); + }, + .not_found => { + // This can happen if the type_anno was followed by a matching decl + // (in which case it's not an anno-only def) + }, + } + } + }, + else => {}, + } + } } } @@ -1223,148 +1357,6 @@ pub fn canonicalizeFile( } } - // Third pass: Process associated items in type declarations - for (self.parse_ir.store.statementSlice(file.statements)) |stmt_id| { - const stmt = self.parse_ir.store.getStatement(stmt_id); - switch (stmt) { - .type_decl => |type_decl| { - if (type_decl.associated) |assoc| { - const type_header = self.parse_ir.store.getTypeHeader(type_decl.header) catch continue; - const type_ident = self.parse_ir.tokens.resolveIdentifier(type_header.name) orelse continue; - - // Enter a new scope for the associated block - try self.scopeEnter(self.env.gpa, false); // false = not a function boundary - defer self.scopeExit(self.env.gpa) catch unreachable; - - // Re-introduce the aliases from first pass - // (We need to rebuild them since we're in a new scope) - for (self.parse_ir.store.statementSlice(assoc.statements)) |assoc_stmt_idx| { - const assoc_stmt = self.parse_ir.store.getStatement(assoc_stmt_idx); - switch (assoc_stmt) { - .type_decl => |nested_type_decl| { - const nested_header = self.parse_ir.store.getTypeHeader(nested_type_decl.header) catch continue; - const unqualified_ident = self.parse_ir.tokens.resolveIdentifier(nested_header.name) orelse continue; - - // Build qualified name - const parent_text = self.env.getIdent(type_ident); - const type_text = self.env.getIdent(unqualified_ident); - const qualified_ident_idx = try self.env.insertQualifiedIdent(parent_text, type_text); - - // Look up and alias - if (self.scopeLookupTypeDecl(qualified_ident_idx)) |qualified_type_decl_idx| { - const current_scope = &self.scopes.items[self.scopes.items.len - 1]; - try current_scope.introduceTypeAlias(self.env.gpa, unqualified_ident, qualified_type_decl_idx); - } - - // Also re-introduce associated items of this nested type - // so that both `my_not` and `MyBool.my_not` work within the associated block - if (nested_type_decl.associated) |nested_assoc| { - for (self.parse_ir.store.statementSlice(nested_assoc.statements)) |nested_assoc_stmt_idx| { - const nested_assoc_stmt = self.parse_ir.store.getStatement(nested_assoc_stmt_idx); - if (nested_assoc_stmt == .decl) { - const nested_decl = nested_assoc_stmt.decl; - const nested_pattern = self.parse_ir.store.getPattern(nested_decl.pattern); - if (nested_pattern == .ident) { - const nested_pattern_ident_tok = nested_pattern.ident.ident_tok; - if (self.parse_ir.tokens.resolveIdentifier(nested_pattern_ident_tok)) |nested_decl_ident| { - // Build fully qualified name (e.g., "Test.MyBool.my_not") - const qualified_text = self.env.getIdent(qualified_ident_idx); - const nested_decl_text = self.env.getIdent(nested_decl_ident); - const full_qualified_ident_idx = try self.env.insertQualifiedIdent(qualified_text, nested_decl_text); - - // Look up the fully qualified pattern - switch (self.scopeLookup(.ident, full_qualified_ident_idx)) { - .found => |pattern_idx| { - const scope = &self.scopes.items[self.scopes.items.len - 1]; - // Add unqualified name (e.g., "my_not") - try scope.idents.put(self.env.gpa, nested_decl_ident, pattern_idx); - // Also add name qualified with nested type (e.g., "MyBool.my_not") - const nested_decl_text2 = self.env.getIdent(nested_decl_ident); - const nested_qualified_ident_idx = try self.env.insertQualifiedIdent(type_text, nested_decl_text2); - try scope.idents.put(self.env.gpa, nested_qualified_ident_idx, pattern_idx); - }, - .not_found => {}, - } - } - } - } - } - } - }, - .decl => |decl| { - const pattern = self.parse_ir.store.getPattern(decl.pattern); - if (pattern == .ident) { - const pattern_ident_tok = pattern.ident.ident_tok; - if (self.parse_ir.tokens.resolveIdentifier(pattern_ident_tok)) |decl_ident| { - // Build qualified name - const parent_text = self.env.getIdent(type_ident); - const decl_text = self.env.getIdent(decl_ident); - const qualified_ident_idx = try self.env.insertQualifiedIdent(parent_text, decl_text); - - // Look up the qualified pattern - switch (self.scopeLookup(.ident, qualified_ident_idx)) { - .found => |pattern_idx| { - const current_scope = &self.scopes.items[self.scopes.items.len - 1]; - // Add both unqualified and qualified names to the current scope - // This allows both `my_not` and `MyBool.my_not` to work inside the associated block - try current_scope.idents.put(self.env.gpa, decl_ident, pattern_idx); - try current_scope.idents.put(self.env.gpa, qualified_ident_idx, pattern_idx); - }, - .not_found => {}, - } - } - } - }, - else => { - // Note: .type_anno is not handled here because anno-only patterns - // are created during processAssociatedItemsSecondPass, so they need - // to be re-introduced AFTER that call completes - }, - } - } - - try self.processAssociatedItemsSecondPass(type_ident, assoc.statements); - - // After processing, re-introduce anno-only defs into the associated block scope - // (They were just created by processAssociatedItemsSecondPass and need to be available - // for use within the associated block) - for (self.parse_ir.store.statementSlice(assoc.statements)) |anno_stmt_idx| { - const anno_stmt = self.parse_ir.store.getStatement(anno_stmt_idx); - switch (anno_stmt) { - .type_anno => |type_anno| { - if (self.parse_ir.tokens.resolveIdentifier(type_anno.name)) |anno_ident| { - // Build qualified name - const parent_text = self.env.getIdent(type_ident); - const anno_text = self.env.getIdent(anno_ident); - const qualified_ident_idx = try self.env.insertQualifiedIdent(parent_text, anno_text); - - // Look up the qualified pattern that was just created - switch (self.scopeLookup(.ident, qualified_ident_idx)) { - .found => |pattern_idx| { - const current_scope = &self.scopes.items[self.scopes.items.len - 1]; - // Add both unqualified and qualified names to the current scope - // This allows both `len` and `List.len` to work inside the associated block - try current_scope.idents.put(self.env.gpa, anno_ident, pattern_idx); - try current_scope.idents.put(self.env.gpa, qualified_ident_idx, pattern_idx); - }, - .not_found => { - // This can happen if the type_anno was followed by a matching decl - // (in which case it's not an anno-only def) - }, - } - } - }, - else => {}, - } - } - } - }, - else => { - // Skip non-type-declaration statements in third pass - }, - } - } - // Check for exposed but not implemented items try self.checkExposedButNotImplemented(); From bb97ff26992107e0cc60206a4bb1291575f24383 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 7 Nov 2025 22:29:36 -0500 Subject: [PATCH 14/38] Drop some unnecessary reintroductions --- src/canonicalize/Can.zig | 58 +++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/src/canonicalize/Can.zig b/src/canonicalize/Can.zig index 7f2d4b7d25..5b99412d15 100644 --- a/src/canonicalize/Can.zig +++ b/src/canonicalize/Can.zig @@ -560,8 +560,10 @@ fn processTypeDeclFirstPass( try current_scope.introduceTypeAlias(self.env.gpa, type_header.name, parent_type_decl_idx); } - // Re-introduce the aliases from first pass into this scope - // (We need to rebuild them since we're in a new scope) + // Introduce aliases into this scope so associated items can reference each other + // We only add unqualified and type-qualified names; fully qualified names are + // already in the parent scope and accessible via scope nesting + const parent_type_text = self.env.getIdent(type_header.name); for (self.parse_ir.store.statementSlice(assoc.statements)) |assoc_stmt_idx| { const assoc_stmt = self.parse_ir.store.getStatement(assoc_stmt_idx); switch (assoc_stmt) { @@ -569,19 +571,18 @@ fn processTypeDeclFirstPass( const nested_header = self.parse_ir.store.getTypeHeader(nested_type_decl.header) catch continue; const unqualified_ident = self.parse_ir.tokens.resolveIdentifier(nested_header.name) orelse continue; - // Build qualified name + // Build fully qualified name (e.g., "Test.MyBool") const parent_text = self.env.getIdent(qualified_name_idx); const nested_type_text = self.env.getIdent(unqualified_ident); const qualified_ident_idx = try self.env.insertQualifiedIdent(parent_text, nested_type_text); - // Look up and alias + // Introduce unqualified type alias (fully qualified is already in parent scope) if (self.scopeLookupTypeDecl(qualified_ident_idx)) |qualified_type_decl_idx| { const current_scope = &self.scopes.items[self.scopes.items.len - 1]; try current_scope.introduceTypeAlias(self.env.gpa, unqualified_ident, qualified_type_decl_idx); } - // Also re-introduce associated items of this nested type - // so that both `my_not` and `MyBool.my_not` work within the associated block + // Introduce associated items of nested types if (nested_type_decl.associated) |nested_assoc| { for (self.parse_ir.store.statementSlice(nested_assoc.statements)) |nested_assoc_stmt_idx| { const nested_assoc_stmt = self.parse_ir.store.getStatement(nested_assoc_stmt_idx); @@ -596,16 +597,15 @@ fn processTypeDeclFirstPass( const nested_decl_text = self.env.getIdent(nested_decl_ident); const full_qualified_ident_idx = try self.env.insertQualifiedIdent(qualified_text, nested_decl_text); - // Look up the fully qualified pattern + // Look up the fully qualified pattern (from parent scope via nesting) switch (self.scopeLookup(.ident, full_qualified_ident_idx)) { .found => |pattern_idx| { const scope = &self.scopes.items[self.scopes.items.len - 1]; // Add unqualified name (e.g., "my_not") try scope.idents.put(self.env.gpa, nested_decl_ident, pattern_idx); - // Also add name qualified with nested type (e.g., "MyBool.my_not") - const nested_decl_text2 = self.env.getIdent(nested_decl_ident); - const nested_qualified_ident_idx = try self.env.insertQualifiedIdent(nested_type_text, nested_decl_text2); - try scope.idents.put(self.env.gpa, nested_qualified_ident_idx, pattern_idx); + // Add type-qualified name (e.g., "MyBool.my_not") + const type_qualified_ident_idx = try self.env.insertQualifiedIdent(nested_type_text, nested_decl_text); + try scope.idents.put(self.env.gpa, type_qualified_ident_idx, pattern_idx); }, .not_found => {}, } @@ -620,19 +620,20 @@ fn processTypeDeclFirstPass( if (pattern == .ident) { const pattern_ident_tok = pattern.ident.ident_tok; if (self.parse_ir.tokens.resolveIdentifier(pattern_ident_tok)) |decl_ident| { - // Build qualified name + // Build fully qualified name (e.g., "Test.MyBool.my_not") const parent_text = self.env.getIdent(qualified_name_idx); const decl_text = self.env.getIdent(decl_ident); - const qualified_ident_idx = try self.env.insertQualifiedIdent(parent_text, decl_text); + const fully_qualified_ident_idx = try self.env.insertQualifiedIdent(parent_text, decl_text); - // Look up the qualified pattern - switch (self.scopeLookup(.ident, qualified_ident_idx)) { + // Look up the fully qualified pattern (from parent scope via nesting) + switch (self.scopeLookup(.ident, fully_qualified_ident_idx)) { .found => |pattern_idx| { const current_scope = &self.scopes.items[self.scopes.items.len - 1]; - // Add both unqualified and qualified names to the current scope - // This allows both `my_not` and `MyBool.my_not` to work inside the associated block + // Add unqualified name (e.g., "my_not") try current_scope.idents.put(self.env.gpa, decl_ident, pattern_idx); - try current_scope.idents.put(self.env.gpa, qualified_ident_idx, pattern_idx); + // Add type-qualified name (e.g., "MyBool.my_not") + const type_qualified_ident_idx = try self.env.insertQualifiedIdent(parent_type_text, decl_text); + try current_scope.idents.put(self.env.gpa, type_qualified_ident_idx, pattern_idx); }, .not_found => {}, } @@ -650,27 +651,28 @@ fn processTypeDeclFirstPass( // Process the associated items (canonicalize their bodies) try self.processAssociatedItemsSecondPass(qualified_name_idx, assoc.statements); - // After processing, re-introduce anno-only defs into the associated block scope - // (They were just created by processAssociatedItemsSecondPass and need to be available - // for use within the associated block) + // After processing, introduce anno-only defs into the associated block scope + // (They were just created by processAssociatedItemsSecondPass) + // We only add unqualified and type-qualified names; fully qualified is in parent scope for (self.parse_ir.store.statementSlice(assoc.statements)) |anno_stmt_idx| { const anno_stmt = self.parse_ir.store.getStatement(anno_stmt_idx); switch (anno_stmt) { .type_anno => |type_anno| { if (self.parse_ir.tokens.resolveIdentifier(type_anno.name)) |anno_ident| { - // Build qualified name + // Build fully qualified name (e.g., "Test.MyBool.len") const parent_text = self.env.getIdent(qualified_name_idx); const anno_text = self.env.getIdent(anno_ident); - const qualified_ident_idx = try self.env.insertQualifiedIdent(parent_text, anno_text); + const fully_qualified_ident_idx = try self.env.insertQualifiedIdent(parent_text, anno_text); - // Look up the qualified pattern that was just created - switch (self.scopeLookup(.ident, qualified_ident_idx)) { + // Look up the fully qualified pattern (from parent scope via nesting) + switch (self.scopeLookup(.ident, fully_qualified_ident_idx)) { .found => |pattern_idx| { const current_scope = &self.scopes.items[self.scopes.items.len - 1]; - // Add both unqualified and qualified names to the current scope - // This allows both `len` and `List.len` to work inside the associated block + // Add unqualified name (e.g., "len") try current_scope.idents.put(self.env.gpa, anno_ident, pattern_idx); - try current_scope.idents.put(self.env.gpa, qualified_ident_idx, pattern_idx); + // Add type-qualified name (e.g., "List.len") + const type_qualified_ident_idx = try self.env.insertQualifiedIdent(parent_type_text, anno_text); + try current_scope.idents.put(self.env.gpa, type_qualified_ident_idx, pattern_idx); }, .not_found => { // This can happen if the type_anno was followed by a matching decl From 8b2ca640e2a49fa67e050e00425c5905a81515fe Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 7 Nov 2025 22:39:28 -0500 Subject: [PATCH 15/38] Update tests --- .../nominal_associated_alias_within_block.md | 12 +- .../nominal/nominal_associated_decls.md | 5 + .../nominal_associated_deep_nesting.md | 29 ++- .../nominal/nominal_associated_lookup_decl.md | 6 +- .../nominal_associated_lookup_mixed.md | 26 +-- .../nominal_associated_lookup_nested.md | 15 +- .../nominal_associated_self_reference.md | 16 +- .../nominal/nominal_associated_value_alias.md | 6 +- .../nominal/nominal_deeply_nested_types.md | 30 ++++ .../snapshots/nominal/nominal_nested_types.md | 5 + .../type_module_associated_items_exposed.md | 5 + test/snapshots/static_dispatch/Adv.md | 168 +++++++++--------- test/snapshots/static_dispatch/Basic.md | 70 ++++---- test/snapshots/static_dispatch/BasicNoAnno.md | 54 +++--- .../static_dispatch/MethodDispatch.md | 94 +++++----- 15 files changed, 303 insertions(+), 238 deletions(-) diff --git a/test/snapshots/nominal/nominal_associated_alias_within_block.md b/test/snapshots/nominal/nominal_associated_alias_within_block.md index 5fa33c4dec..6e1b60b4a5 100644 --- a/test/snapshots/nominal/nominal_associated_alias_within_block.md +++ b/test/snapshots/nominal/nominal_associated_alias_within_block.md @@ -84,18 +84,18 @@ external = Foo.defaultBaz # CANONICALIZE ~~~clojure (can-ir - (d-let - (p-assign (ident "external")) - (e-lookup-local - (p-assign (ident "Foo.defaultBaz"))) - (annotation - (ty-lookup (name "Foo.Baz") (local)))) (d-let (p-assign (ident "Foo.defaultBaz")) (e-nominal (nominal "Foo.Bar") (e-tag (name "X"))) (annotation (ty-lookup (name "Foo.Baz") (local)))) + (d-let + (p-assign (ident "external")) + (e-lookup-local + (p-assign (ident "Foo.defaultBaz"))) + (annotation + (ty-lookup (name "Foo.Baz") (local)))) (s-nominal-decl (ty-header (name "Foo")) (ty-tag-union diff --git a/test/snapshots/nominal/nominal_associated_decls.md b/test/snapshots/nominal/nominal_associated_decls.md index 83900f6730..5f00517caa 100644 --- a/test/snapshots/nominal/nominal_associated_decls.md +++ b/test/snapshots/nominal/nominal_associated_decls.md @@ -64,6 +64,9 @@ Foo := [Whatever].{ # CANONICALIZE ~~~clojure (can-ir + (d-let + (p-assign (ident "Foo.Bar.baz")) + (e-num (value "5"))) (d-let (p-assign (ident "Foo.Bar.baz")) (e-num (value "5"))) @@ -83,6 +86,7 @@ Foo := [Whatever].{ ~~~clojure (inferred-types (defs + (patt (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) (patt (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) (patt (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]"))) (type_decls @@ -91,6 +95,7 @@ Foo := [Whatever].{ (nominal (type "Foo.Bar") (ty-header (name "Foo.Bar")))) (expressions + (expr (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) (expr (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) (expr (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")))) ~~~ diff --git a/test/snapshots/nominal/nominal_associated_deep_nesting.md b/test/snapshots/nominal/nominal_associated_deep_nesting.md index 80a32c5034..9ced393c6b 100644 --- a/test/snapshots/nominal/nominal_associated_deep_nesting.md +++ b/test/snapshots/nominal/nominal_associated_deep_nesting.md @@ -110,6 +110,18 @@ deepType = C # CANONICALIZE ~~~clojure (can-ir + (d-let + (p-assign (ident "Foo.Level1.Level2.Level3.value")) + (e-num (value "42"))) + (d-let + (p-assign (ident "Foo.Level1.Level2.Level3.value")) + (e-num (value "42"))) + (d-let + (p-assign (ident "Foo.Level1.Level2.Level3.value")) + (e-num (value "42"))) + (d-let + (p-assign (ident "Foo.Level1.Level2.Level3.value")) + (e-num (value "42"))) (d-let (p-assign (ident "deepValue")) (e-lookup-local @@ -121,9 +133,6 @@ deepType = C (e-tag (name "C")) (annotation (ty-lookup (name "Foo.Level1.Level2.Level3") (local)))) - (d-let - (p-assign (ident "Foo.Level1.Level2.Level3.value")) - (e-num (value "42"))) (s-nominal-decl (ty-header (name "Foo")) (ty-tag-union @@ -146,8 +155,11 @@ deepType = C (inferred-types (defs (patt (type "Num(Int(Unsigned64))")) - (patt (type "Foo.Level1.Level2.Level3")) - (patt (type "Num(Int(Unsigned64))"))) + (patt (type "Num(Int(Unsigned64))")) + (patt (type "Num(Int(Unsigned64))")) + (patt (type "Num(Int(Unsigned64))")) + (patt (type "Num(Int(Unsigned64))")) + (patt (type "Foo.Level1.Level2.Level3"))) (type_decls (nominal (type "Foo") (ty-header (name "Foo"))) @@ -159,6 +171,9 @@ deepType = C (ty-header (name "Foo.Level1.Level2.Level3")))) (expressions (expr (type "Num(Int(Unsigned64))")) - (expr (type "Foo.Level1.Level2.Level3")) - (expr (type "Num(Int(Unsigned64))")))) + (expr (type "Num(Int(Unsigned64))")) + (expr (type "Num(Int(Unsigned64))")) + (expr (type "Num(Int(Unsigned64))")) + (expr (type "Num(Int(Unsigned64))")) + (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 a771002e86..44d860fd95 100644 --- a/test/snapshots/nominal/nominal_associated_lookup_decl.md +++ b/test/snapshots/nominal/nominal_associated_lookup_decl.md @@ -58,15 +58,15 @@ useBar = Foo.bar # CANONICALIZE ~~~clojure (can-ir + (d-let + (p-assign (ident "Foo.bar")) + (e-num (value "42"))) (d-let (p-assign (ident "useBar")) (e-lookup-local (p-assign (ident "Foo.bar"))) (annotation (ty-lookup (name "U64") (builtin)))) - (d-let - (p-assign (ident "Foo.bar")) - (e-num (value "42"))) (s-nominal-decl (ty-header (name "Foo")) (ty-tag-union diff --git a/test/snapshots/nominal/nominal_associated_lookup_mixed.md b/test/snapshots/nominal/nominal_associated_lookup_mixed.md index 98c6aac71d..091fe735ce 100644 --- a/test/snapshots/nominal/nominal_associated_lookup_mixed.md +++ b/test/snapshots/nominal/nominal_associated_lookup_mixed.md @@ -89,15 +89,6 @@ result = Foo.transform(Foo.defaultBar) # CANONICALIZE ~~~clojure (can-ir - (d-let - (p-assign (ident "result")) - (e-call - (e-lookup-local - (p-assign (ident "Foo.transform"))) - (e-lookup-local - (p-assign (ident "Foo.defaultBar")))) - (annotation - (ty-lookup (name "Foo.Bar") (local)))) (d-let (p-assign (ident "Foo.defaultBar")) (e-nominal (nominal "Foo.Bar") @@ -113,6 +104,15 @@ result = Foo.transform(Foo.defaultBar) (ty-fn (effectful false) (ty-lookup (name "Foo.Bar") (local)) (ty-lookup (name "Foo.Bar") (local))))) + (d-let + (p-assign (ident "result")) + (e-call + (e-lookup-local + (p-assign (ident "Foo.transform"))) + (e-lookup-local + (p-assign (ident "Foo.defaultBar")))) + (annotation + (ty-lookup (name "Foo.Bar") (local)))) (s-nominal-decl (ty-header (name "Foo")) (ty-tag-union @@ -129,8 +129,8 @@ result = Foo.transform(Foo.defaultBar) (inferred-types (defs (patt (type "Foo.Bar")) - (patt (type "Foo.Bar")) - (patt (type "Foo.Bar -> Foo.Bar"))) + (patt (type "Foo.Bar -> Foo.Bar")) + (patt (type "Foo.Bar"))) (type_decls (nominal (type "Foo") (ty-header (name "Foo"))) @@ -138,6 +138,6 @@ result = Foo.transform(Foo.defaultBar) (ty-header (name "Foo.Bar")))) (expressions (expr (type "Foo.Bar")) - (expr (type "Foo.Bar")) - (expr (type "Foo.Bar -> Foo.Bar")))) + (expr (type "Foo.Bar -> Foo.Bar")) + (expr (type "Foo.Bar")))) ~~~ diff --git a/test/snapshots/nominal/nominal_associated_lookup_nested.md b/test/snapshots/nominal/nominal_associated_lookup_nested.md index b23ccce688..40f00377fd 100644 --- a/test/snapshots/nominal/nominal_associated_lookup_nested.md +++ b/test/snapshots/nominal/nominal_associated_lookup_nested.md @@ -84,6 +84,12 @@ myNum = Foo.Bar.baz # CANONICALIZE ~~~clojure (can-ir + (d-let + (p-assign (ident "Foo.Bar.baz")) + (e-num (value "5"))) + (d-let + (p-assign (ident "Foo.Bar.baz")) + (e-num (value "5"))) (d-let (p-assign (ident "myType")) (e-tag (name "Something")) @@ -95,9 +101,6 @@ myNum = Foo.Bar.baz (p-assign (ident "Foo.Bar.baz"))) (annotation (ty-lookup (name "U64") (builtin)))) - (d-let - (p-assign (ident "Foo.Bar.baz")) - (e-num (value "5"))) (s-nominal-decl (ty-header (name "Foo")) (ty-tag-union @@ -111,8 +114,9 @@ myNum = Foo.Bar.baz ~~~clojure (inferred-types (defs - (patt (type "Foo.Bar")) (patt (type "Num(Int(Unsigned64))")) + (patt (type "Num(Int(Unsigned64))")) + (patt (type "Foo.Bar")) (patt (type "Num(Int(Unsigned64))"))) (type_decls (nominal (type "Foo") @@ -120,7 +124,8 @@ myNum = Foo.Bar.baz (nominal (type "Foo.Bar") (ty-header (name "Foo.Bar")))) (expressions - (expr (type "Foo.Bar")) (expr (type "Num(Int(Unsigned64))")) + (expr (type "Num(Int(Unsigned64))")) + (expr (type "Foo.Bar")) (expr (type "Num(Int(Unsigned64))")))) ~~~ diff --git a/test/snapshots/nominal/nominal_associated_self_reference.md b/test/snapshots/nominal/nominal_associated_self_reference.md index f19e2d2344..e09e5a299b 100644 --- a/test/snapshots/nominal/nominal_associated_self_reference.md +++ b/test/snapshots/nominal/nominal_associated_self_reference.md @@ -101,12 +101,6 @@ external = Foo.defaultBar # CANONICALIZE ~~~clojure (can-ir - (d-let - (p-assign (ident "external")) - (e-lookup-local - (p-assign (ident "Foo.defaultBar"))) - (annotation - (ty-lookup (name "Foo.Bar") (local)))) (d-let (p-assign (ident "Foo.defaultBar")) (e-tag (name "X")) @@ -130,6 +124,12 @@ external = Foo.defaultBar (p-assign (ident "Foo.transform"))) (e-lookup-local (p-assign (ident "Foo.defaultBar"))))) + (d-let + (p-assign (ident "external")) + (e-lookup-local + (p-assign (ident "Foo.defaultBar"))) + (annotation + (ty-lookup (name "Foo.Bar") (local)))) (s-nominal-decl (ty-header (name "Foo")) (ty-tag-union @@ -145,9 +145,9 @@ external = Foo.defaultBar ~~~clojure (inferred-types (defs - (patt (type "Foo.Bar")) (patt (type "Foo.Bar")) (patt (type "Foo.Bar -> Foo.Bar")) + (patt (type "Foo.Bar")) (patt (type "Foo.Bar"))) (type_decls (nominal (type "Foo") @@ -155,8 +155,8 @@ external = Foo.defaultBar (nominal (type "Foo.Bar") (ty-header (name "Foo.Bar")))) (expressions - (expr (type "Foo.Bar")) (expr (type "Foo.Bar")) (expr (type "Foo.Bar -> Foo.Bar")) + (expr (type "Foo.Bar")) (expr (type "Foo.Bar")))) ~~~ diff --git a/test/snapshots/nominal/nominal_associated_value_alias.md b/test/snapshots/nominal/nominal_associated_value_alias.md index 3fdcc0c4a3..a6cc6247db 100644 --- a/test/snapshots/nominal/nominal_associated_value_alias.md +++ b/test/snapshots/nominal/nominal_associated_value_alias.md @@ -73,6 +73,9 @@ result = myBar # CANONICALIZE ~~~clojure (can-ir + (d-let + (p-assign (ident "Foo.bar")) + (e-num (value "42"))) (d-let (p-assign (ident "myBar")) (e-lookup-local @@ -85,9 +88,6 @@ result = myBar (p-assign (ident "myBar"))) (annotation (ty-lookup (name "U64") (builtin)))) - (d-let - (p-assign (ident "Foo.bar")) - (e-num (value "42"))) (s-nominal-decl (ty-header (name "Foo")) (ty-tag-union diff --git a/test/snapshots/nominal/nominal_deeply_nested_types.md b/test/snapshots/nominal/nominal_deeply_nested_types.md index c298cae930..c81b48cacb 100644 --- a/test/snapshots/nominal/nominal_deeply_nested_types.md +++ b/test/snapshots/nominal/nominal_deeply_nested_types.md @@ -102,6 +102,24 @@ Foo := [Whatever].{ # CANONICALIZE ~~~clojure (can-ir + (d-let + (p-assign (ident "Foo.Bar.Baz.Qux.w")) + (e-num (value "1"))) + (d-let + (p-assign (ident "Foo.Bar.Baz.Qux.w")) + (e-num (value "1"))) + (d-let + (p-assign (ident "Foo.Bar.Baz.z")) + (e-num (value "2"))) + (d-let + (p-assign (ident "Foo.Bar.Baz.Qux.w")) + (e-num (value "1"))) + (d-let + (p-assign (ident "Foo.Bar.Baz.z")) + (e-num (value "2"))) + (d-let + (p-assign (ident "Foo.Bar.y")) + (e-num (value "3"))) (d-let (p-assign (ident "Foo.Bar.Baz.Qux.w")) (e-num (value "1"))) @@ -135,6 +153,12 @@ Foo := [Whatever].{ ~~~clojure (inferred-types (defs + (patt (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) + (patt (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) + (patt (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) + (patt (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) + (patt (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) + (patt (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) (patt (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) (patt (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) (patt (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) @@ -149,6 +173,12 @@ Foo := [Whatever].{ (nominal (type "Foo.Bar.Baz.Qux") (ty-header (name "Foo.Bar.Baz.Qux")))) (expressions + (expr (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) + (expr (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) + (expr (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) + (expr (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) + (expr (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) + (expr (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) (expr (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) (expr (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) (expr (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) diff --git a/test/snapshots/nominal/nominal_nested_types.md b/test/snapshots/nominal/nominal_nested_types.md index 7564952d99..abfb0822ff 100644 --- a/test/snapshots/nominal/nominal_nested_types.md +++ b/test/snapshots/nominal/nominal_nested_types.md @@ -64,6 +64,9 @@ Foo := [Whatever].{ # CANONICALIZE ~~~clojure (can-ir + (d-let + (p-assign (ident "Foo.Bar.y")) + (e-num (value "6"))) (d-let (p-assign (ident "Foo.Bar.y")) (e-num (value "6"))) @@ -83,6 +86,7 @@ Foo := [Whatever].{ ~~~clojure (inferred-types (defs + (patt (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) (patt (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) (patt (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]"))) (type_decls @@ -91,6 +95,7 @@ Foo := [Whatever].{ (nominal (type "Foo.Bar") (ty-header (name "Foo.Bar")))) (expressions + (expr (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) (expr (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) (expr (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")))) ~~~ diff --git a/test/snapshots/nominal/type_module_associated_items_exposed.md b/test/snapshots/nominal/type_module_associated_items_exposed.md index 650c17dd60..8169185a92 100644 --- a/test/snapshots/nominal/type_module_associated_items_exposed.md +++ b/test/snapshots/nominal/type_module_associated_items_exposed.md @@ -63,6 +63,9 @@ Foo := [Blah].{ # CANONICALIZE ~~~clojure (can-ir + (d-let + (p-assign (ident "Foo.Bar.baz")) + (e-empty_record)) (d-let (p-assign (ident "Foo.Bar.baz")) (e-empty_record)) @@ -81,6 +84,7 @@ Foo := [Blah].{ ~~~clojure (inferred-types (defs + (patt (type "{}")) (patt (type "{}")) (patt (type "{}"))) (type_decls @@ -89,6 +93,7 @@ Foo := [Blah].{ (nominal (type "Foo.Bar") (ty-header (name "Foo.Bar")))) (expressions + (expr (type "{}")) (expr (type "{}")) (expr (type "{}")))) ~~~ diff --git a/test/snapshots/static_dispatch/Adv.md b/test/snapshots/static_dispatch/Adv.md index 04fe8feaad..857f3ea72f 100644 --- a/test/snapshots/static_dispatch/Adv.md +++ b/test/snapshots/static_dispatch/Adv.md @@ -317,6 +317,80 @@ main = { # CANONICALIZE ~~~clojure (can-ir + (d-let + (p-assign (ident "Adv.to_str")) + (e-closure + (captures + (capture (ident "s"))) + (e-lambda + (args + (p-nominal + (p-applied-tag))) + (e-lookup-local + (p-assign (ident "s"))))) + (annotation + (ty-fn (effectful false) + (ty-lookup (name "Adv") (local)) + (ty-lookup (name "Str") (builtin))))) + (d-let + (p-assign (ident "Adv.to_u64")) + (e-closure + (captures + (capture (ident "u"))) + (e-lambda + (args + (p-nominal + (p-applied-tag))) + (e-lookup-local + (p-assign (ident "u"))))) + (annotation + (ty-fn (effectful false) + (ty-lookup (name "Adv") (local)) + (ty-lookup (name "U64") (builtin))))) + (d-let + (p-assign (ident "Adv.update_str")) + (e-closure + (captures + (capture (ident "u64"))) + (e-lambda + (args + (p-nominal + (p-applied-tag)) + (p-assign (ident "next_str"))) + (e-nominal (nominal "Adv") + (e-tag (name "Val") + (args + (e-lookup-local + (p-assign (ident "u64"))) + (e-lookup-local + (p-assign (ident "next_str")))))))) + (annotation + (ty-fn (effectful false) + (ty-lookup (name "Adv") (local)) + (ty-lookup (name "Str") (builtin)) + (ty-lookup (name "Adv") (local))))) + (d-let + (p-assign (ident "Adv.update_u64")) + (e-closure + (captures + (capture (ident "str"))) + (e-lambda + (args + (p-nominal + (p-applied-tag)) + (p-assign (ident "next_u64"))) + (e-nominal (nominal "Adv") + (e-tag (name "Val") + (args + (e-lookup-local + (p-assign (ident "next_u64"))) + (e-lookup-local + (p-assign (ident "str")))))))) + (annotation + (ty-fn (effectful false) + (ty-lookup (name "Adv") (local)) + (ty-lookup (name "U64") (builtin)) + (ty-lookup (name "Adv") (local))))) (d-let (p-assign (ident "mismatch")) (e-block @@ -411,80 +485,6 @@ main = { (ty-tuple (ty-lookup (name "Str") (builtin)) (ty-lookup (name "U64") (builtin))))) - (d-let - (p-assign (ident "Adv.to_str")) - (e-closure - (captures - (capture (ident "s"))) - (e-lambda - (args - (p-nominal - (p-applied-tag))) - (e-lookup-local - (p-assign (ident "s"))))) - (annotation - (ty-fn (effectful false) - (ty-lookup (name "Adv") (local)) - (ty-lookup (name "Str") (builtin))))) - (d-let - (p-assign (ident "Adv.to_u64")) - (e-closure - (captures - (capture (ident "u"))) - (e-lambda - (args - (p-nominal - (p-applied-tag))) - (e-lookup-local - (p-assign (ident "u"))))) - (annotation - (ty-fn (effectful false) - (ty-lookup (name "Adv") (local)) - (ty-lookup (name "U64") (builtin))))) - (d-let - (p-assign (ident "Adv.update_str")) - (e-closure - (captures - (capture (ident "u64"))) - (e-lambda - (args - (p-nominal - (p-applied-tag)) - (p-assign (ident "next_str"))) - (e-nominal (nominal "Adv") - (e-tag (name "Val") - (args - (e-lookup-local - (p-assign (ident "u64"))) - (e-lookup-local - (p-assign (ident "next_str")))))))) - (annotation - (ty-fn (effectful false) - (ty-lookup (name "Adv") (local)) - (ty-lookup (name "Str") (builtin)) - (ty-lookup (name "Adv") (local))))) - (d-let - (p-assign (ident "Adv.update_u64")) - (e-closure - (captures - (capture (ident "str"))) - (e-lambda - (args - (p-nominal - (p-applied-tag)) - (p-assign (ident "next_u64"))) - (e-nominal (nominal "Adv") - (e-tag (name "Val") - (args - (e-lookup-local - (p-assign (ident "next_u64"))) - (e-lookup-local - (p-assign (ident "str")))))))) - (annotation - (ty-fn (effectful false) - (ty-lookup (name "Adv") (local)) - (ty-lookup (name "U64") (builtin)) - (ty-lookup (name "Adv") (local))))) (s-nominal-decl (ty-header (name "Adv")) (ty-tag-union @@ -496,24 +496,24 @@ main = { ~~~clojure (inferred-types (defs - (patt (type "_a")) - (patt (type "_a")) - (patt (type "_a")) - (patt (type "(Str, Num(Int(Unsigned64)))")) (patt (type "Adv -> Str")) (patt (type "Adv -> Num(Int(Unsigned64))")) (patt (type "Adv, Str -> Adv")) - (patt (type "Adv, Num(Int(Unsigned64)) -> Adv"))) + (patt (type "Adv, Num(Int(Unsigned64)) -> Adv")) + (patt (type "_a")) + (patt (type "_a")) + (patt (type "_a")) + (patt (type "(Str, Num(Int(Unsigned64)))"))) (type_decls (nominal (type "Adv") (ty-header (name "Adv")))) (expressions - (expr (type "_a")) - (expr (type "_a")) - (expr (type "_a")) - (expr (type "(Str, Num(Int(Unsigned64)))")) (expr (type "Adv -> Str")) (expr (type "Adv -> Num(Int(Unsigned64))")) (expr (type "Adv, Str -> Adv")) - (expr (type "Adv, Num(Int(Unsigned64)) -> Adv")))) + (expr (type "Adv, Num(Int(Unsigned64)) -> Adv")) + (expr (type "_a")) + (expr (type "_a")) + (expr (type "_a")) + (expr (type "(Str, Num(Int(Unsigned64)))")))) ~~~ diff --git a/test/snapshots/static_dispatch/Basic.md b/test/snapshots/static_dispatch/Basic.md index bfe89124b1..43c80003c7 100644 --- a/test/snapshots/static_dispatch/Basic.md +++ b/test/snapshots/static_dispatch/Basic.md @@ -167,6 +167,35 @@ main = (helper1(val), helper2(val)) # CANONICALIZE ~~~clojure (can-ir + (d-let + (p-assign (ident "Basic.to_str")) + (e-closure + (captures + (capture (ident "s"))) + (e-lambda + (args + (p-nominal + (p-applied-tag))) + (e-lookup-local + (p-assign (ident "s"))))) + (annotation + (ty-fn (effectful false) + (ty-lookup (name "Basic") (local)) + (ty-lookup (name "Str") (builtin))))) + (d-let + (p-assign (ident "Basic.to_str2")) + (e-lambda + (args + (p-assign (ident "test"))) + (e-dot-access (field "to_str") + (receiver + (e-lookup-local + (p-assign (ident "test")))) + (args))) + (annotation + (ty-fn (effectful false) + (ty-lookup (name "Basic") (local)) + (ty-lookup (name "Str") (builtin))))) (d-let (p-assign (ident "helper1")) (e-lambda @@ -232,35 +261,6 @@ main = (helper1(val), helper2(val)) (ty-tuple (ty-lookup (name "Str") (builtin)) (ty-lookup (name "Str") (builtin))))) - (d-let - (p-assign (ident "Basic.to_str")) - (e-closure - (captures - (capture (ident "s"))) - (e-lambda - (args - (p-nominal - (p-applied-tag))) - (e-lookup-local - (p-assign (ident "s"))))) - (annotation - (ty-fn (effectful false) - (ty-lookup (name "Basic") (local)) - (ty-lookup (name "Str") (builtin))))) - (d-let - (p-assign (ident "Basic.to_str2")) - (e-lambda - (args - (p-assign (ident "test"))) - (e-dot-access (field "to_str") - (receiver - (e-lookup-local - (p-assign (ident "test")))) - (args))) - (annotation - (ty-fn (effectful false) - (ty-lookup (name "Basic") (local)) - (ty-lookup (name "Str") (builtin))))) (s-nominal-decl (ty-header (name "Basic")) (ty-tag-union @@ -271,20 +271,20 @@ main = (helper1(val), helper2(val)) ~~~clojure (inferred-types (defs + (patt (type "Basic -> Str")) + (patt (type "Basic -> Str")) (patt (type "a -> b where [a.to_str : a -> b]")) (patt (type "a -> b where [a.to_str2 : a -> b]")) (patt (type "Basic")) - (patt (type "(Str, Str)")) - (patt (type "Basic -> Str")) - (patt (type "Basic -> Str"))) + (patt (type "(Str, Str)"))) (type_decls (nominal (type "Basic") (ty-header (name "Basic")))) (expressions + (expr (type "Basic -> Str")) + (expr (type "Basic -> Str")) (expr (type "a -> b where [a.to_str : a -> b]")) (expr (type "a -> b where [a.to_str2 : a -> b]")) (expr (type "Basic")) - (expr (type "(Str, Str)")) - (expr (type "Basic -> Str")) - (expr (type "Basic -> Str")))) + (expr (type "(Str, Str)")))) ~~~ diff --git a/test/snapshots/static_dispatch/BasicNoAnno.md b/test/snapshots/static_dispatch/BasicNoAnno.md index 141d45e519..e409204e09 100644 --- a/test/snapshots/static_dispatch/BasicNoAnno.md +++ b/test/snapshots/static_dispatch/BasicNoAnno.md @@ -124,6 +124,27 @@ main = (helper1(val), helper2(val)) # CANONICALIZE ~~~clojure (can-ir + (d-let + (p-assign (ident "BasicNoAnno.to_str")) + (e-closure + (captures + (capture (ident "s"))) + (e-lambda + (args + (p-nominal + (p-applied-tag))) + (e-lookup-local + (p-assign (ident "s")))))) + (d-let + (p-assign (ident "BasicNoAnno.to_str2")) + (e-lambda + (args + (p-assign (ident "test"))) + (e-dot-access (field "to_str") + (receiver + (e-lookup-local + (p-assign (ident "test")))) + (args)))) (d-let (p-assign (ident "helper1")) (e-lambda @@ -169,27 +190,6 @@ main = (helper1(val), helper2(val)) (ty-tuple (ty-lookup (name "Str") (builtin)) (ty-lookup (name "Str") (builtin))))) - (d-let - (p-assign (ident "BasicNoAnno.to_str")) - (e-closure - (captures - (capture (ident "s"))) - (e-lambda - (args - (p-nominal - (p-applied-tag))) - (e-lookup-local - (p-assign (ident "s")))))) - (d-let - (p-assign (ident "BasicNoAnno.to_str2")) - (e-lambda - (args - (p-assign (ident "test"))) - (e-dot-access (field "to_str") - (receiver - (e-lookup-local - (p-assign (ident "test")))) - (args)))) (s-nominal-decl (ty-header (name "BasicNoAnno")) (ty-tag-union @@ -200,20 +200,20 @@ main = (helper1(val), helper2(val)) ~~~clojure (inferred-types (defs + (patt (type "BasicNoAnno -> Str")) + (patt (type "a -> b where [a.to_str : a -> b]")) (patt (type "a -> b where [a.to_str : a -> b]")) (patt (type "a -> b where [a.to_str2 : a -> b]")) (patt (type "BasicNoAnno")) - (patt (type "(Str, Str)")) - (patt (type "BasicNoAnno -> Str")) - (patt (type "a -> b where [a.to_str : a -> b]"))) + (patt (type "(Str, Str)"))) (type_decls (nominal (type "BasicNoAnno") (ty-header (name "BasicNoAnno")))) (expressions + (expr (type "BasicNoAnno -> Str")) + (expr (type "a -> b where [a.to_str : a -> b]")) (expr (type "a -> b where [a.to_str : a -> b]")) (expr (type "a -> b where [a.to_str2 : a -> b]")) (expr (type "BasicNoAnno")) - (expr (type "(Str, Str)")) - (expr (type "BasicNoAnno -> Str")) - (expr (type "a -> b where [a.to_str : a -> b]")))) + (expr (type "(Str, Str)")))) ~~~ diff --git a/test/snapshots/static_dispatch/MethodDispatch.md b/test/snapshots/static_dispatch/MethodDispatch.md index 6a93251051..9e0068399d 100644 --- a/test/snapshots/static_dispatch/MethodDispatch.md +++ b/test/snapshots/static_dispatch/MethodDispatch.md @@ -231,6 +231,47 @@ NO CHANGE # CANONICALIZE ~~~clojure (can-ir + (d-let + (p-assign (ident "Container.get_value")) + (e-closure + (captures + (capture (ident "s"))) + (e-lambda + (args + (p-nominal + (p-applied-tag))) + (e-lookup-local + (p-assign (ident "s"))))) + (annotation + (ty-fn (effectful false) + (ty-lookup (name "Container") (local)) + (ty-lookup (name "Str") (builtin))))) + (d-let + (p-assign (ident "Container.transform")) + (e-closure + (captures + (capture (ident "s"))) + (e-lambda + (args + (p-nominal + (p-applied-tag)) + (p-assign (ident "fn"))) + (e-nominal (nominal "Container") + (e-tag (name "Box") + (args + (e-call + (e-lookup-local + (p-assign (ident "fn"))) + (e-lookup-local + (p-assign (ident "s"))))))))) + (annotation + (ty-fn (effectful false) + (ty-lookup (name "Container") (local)) + (ty-parens + (ty-fn (effectful false) + (ty-lookup (name "Str") (builtin)) + (ty-lookup (name "Str") (builtin)))) + (ty-lookup (name "Container") (local))))) (d-let (p-assign (ident "extract")) (e-lambda @@ -351,47 +392,6 @@ NO CHANGE (ty-lookup (name "Str") (builtin)) (ty-lookup (name "Str") (builtin)) (ty-lookup (name "Str") (builtin))))) - (d-let - (p-assign (ident "Container.get_value")) - (e-closure - (captures - (capture (ident "s"))) - (e-lambda - (args - (p-nominal - (p-applied-tag))) - (e-lookup-local - (p-assign (ident "s"))))) - (annotation - (ty-fn (effectful false) - (ty-lookup (name "Container") (local)) - (ty-lookup (name "Str") (builtin))))) - (d-let - (p-assign (ident "Container.transform")) - (e-closure - (captures - (capture (ident "s"))) - (e-lambda - (args - (p-nominal - (p-applied-tag)) - (p-assign (ident "fn"))) - (e-nominal (nominal "Container") - (e-tag (name "Box") - (args - (e-call - (e-lookup-local - (p-assign (ident "fn"))) - (e-lookup-local - (p-assign (ident "s"))))))))) - (annotation - (ty-fn (effectful false) - (ty-lookup (name "Container") (local)) - (ty-parens - (ty-fn (effectful false) - (ty-lookup (name "Str") (builtin)) - (ty-lookup (name "Str") (builtin)))) - (ty-lookup (name "Container") (local))))) (s-nominal-decl (ty-header (name "Container")) (ty-tag-union @@ -402,6 +402,8 @@ NO CHANGE ~~~clojure (inferred-types (defs + (patt (type "Container -> Str")) + (patt (type "Container, Str -> Str -> Container")) (patt (type "a -> Str where [a.get_value : a -> Str]")) (patt (type "a, Str -> Str -> a where [a.transform : a, Str -> Str -> aa.transform : a, Str -> Str -> a]")) (patt (type "Container")) @@ -409,13 +411,13 @@ NO CHANGE (patt (type "Str")) (patt (type "Str")) (patt (type "Container")) - (patt (type "(Str, Str, Str)")) - (patt (type "Container -> Str")) - (patt (type "Container, Str -> Str -> Container"))) + (patt (type "(Str, Str, Str)"))) (type_decls (nominal (type "Container") (ty-header (name "Container")))) (expressions + (expr (type "Container -> Str")) + (expr (type "Container, Str -> Str -> Container")) (expr (type "a -> Str where [a.get_value : a -> Str]")) (expr (type "a, Str -> Str -> a where [a.transform : a, Str -> Str -> aa.transform : a, Str -> Str -> a]")) (expr (type "Container")) @@ -423,7 +425,5 @@ NO CHANGE (expr (type "Str")) (expr (type "Str")) (expr (type "Container")) - (expr (type "(Str, Str, Str)")) - (expr (type "Container -> Str")) - (expr (type "Container, Str -> Str -> Container")))) + (expr (type "(Str, Str, Str)")))) ~~~ From df98dd66e56b2e03beac6af5dd610762ed4770f7 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 7 Nov 2025 22:53:43 -0500 Subject: [PATCH 16/38] Do less rebuilding and scanning --- src/canonicalize/Can.zig | 80 +++++++++++++++++----------------------- 1 file changed, 34 insertions(+), 46 deletions(-) diff --git a/src/canonicalize/Can.zig b/src/canonicalize/Can.zig index 5b99412d15..f7e400ed4c 100644 --- a/src/canonicalize/Can.zig +++ b/src/canonicalize/Can.zig @@ -155,41 +155,6 @@ const TypeBindingLocationConst = struct { binding: *const Scope.TypeBinding, }; -/// Add all qualified variants of a name to the current scope. -/// For a name like "Builtin.List.get", this adds: -/// - "get" (unqualified) -/// - "List.get" (1 level of qualification) -/// - "Builtin.List.get" (full qualification) -/// Works for any depth of nesting. -fn addAllQualifiedVariants( - self: *Self, - parent_full_text: []const u8, - name_text: []const u8, - pattern_idx: Pattern.Idx, -) !void { - const current_scope = &self.scopes.items[self.scopes.items.len - 1]; - - // Add unqualified name (e.g., "get") - // Use findIdent first to reuse existing ident if present, otherwise insert - const name_ident = self.env.common.findIdent(name_text) orelse - try self.env.insertIdent(base.Ident.for_text(name_text)); - try current_scope.idents.put(self.env.gpa, name_ident, pattern_idx); - - // Add all qualified variants by iterating through dots - // For "Builtin.List", add "List.get" and "Builtin.List.get" - var start: usize = 0; - while (std.mem.indexOfScalarPos(u8, parent_full_text, start, '.')) |dot_pos| { - const prefix = parent_full_text[dot_pos + 1 ..]; - const qualified_idx = try self.env.insertQualifiedIdent(prefix, name_text); - try current_scope.idents.put(self.env.gpa, qualified_idx, pattern_idx); - start = dot_pos + 1; - } - - // Add the full qualified name (e.g., "Builtin.List.get") - const full_qualified_idx = try self.env.insertQualifiedIdent(parent_full_text, name_text); - try current_scope.idents.put(self.env.gpa, full_qualified_idx, pattern_idx); -} - /// Deinitialize canonicalizer resources pub fn deinit( self: *Self, @@ -649,7 +614,7 @@ fn processTypeDeclFirstPass( } // Process the associated items (canonicalize their bodies) - try self.processAssociatedItemsSecondPass(qualified_name_idx, assoc.statements); + try self.processAssociatedItemsSecondPass(qualified_name_idx, type_header.name, assoc.statements); // After processing, introduce anno-only defs into the associated block scope // (They were just created by processAssociatedItemsSecondPass) @@ -770,6 +735,7 @@ fn canonicalizeAssociatedDeclWithAnno( fn processAssociatedItemsSecondPass( self: *Self, parent_name: Ident.Idx, + parent_type_name: Ident.Idx, statements: AST.Statement.Span, ) std.mem.Allocator.Error!void { const stmt_idxs = self.parse_ir.store.statementSlice(statements); @@ -793,7 +759,7 @@ fn processAssociatedItemsSecondPass( try self.scopeEnter(self.env.gpa, false); defer self.scopeExit(self.env.gpa) catch unreachable; - try self.processAssociatedItemsSecondPass(qualified_idx, assoc.statements); + try self.processAssociatedItemsSecondPass(qualified_idx, type_ident, assoc.statements); } }, .type_anno => |ta| { @@ -865,13 +831,25 @@ fn processAssociatedItemsSecondPass( const def_idx_u16: u16 = @intCast(@intFromEnum(def_idx)); try self.env.setExposedNodeIndexById(qualified_idx, def_idx_u16); - // Also make all qualified variants available in the current scope - // (This allows `get`, `List.get`, `Builtin.List.get`, etc. to all work) + // Make the real pattern available in current scope (replaces placeholder) + // We already added unqualified and type-qualified names earlier, + // but need to update them to point to the real pattern instead of placeholder. const def_cir = self.env.store.getDef(def_idx); const pattern_idx = def_cir.pattern; - const parent_full_text = self.env.getIdent(parent_name); - try self.addAllQualifiedVariants(parent_full_text, decl_text, pattern_idx); - } else {} + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + + // Add unqualified name (e.g., "my_not") - overwrites placeholder alias + try current_scope.idents.put(self.env.gpa, decl_ident, pattern_idx); + + // Add type-qualified name (e.g., "MyBool.my_not") - overwrites placeholder alias + // Use parent_type_name directly (no string parsing needed!) + const type_qualified_idx = try self.env.insertQualifiedIdent(self.env.getIdent(parent_type_name), decl_text); + try current_scope.idents.put(self.env.gpa, type_qualified_idx, pattern_idx); + + // Add fully qualified name (e.g., "Test.MyBool.my_not") - shadows placeholder in parent scope + // We already have qualified_idx = insertQualifiedIdent(parent_text, decl_text) + try current_scope.idents.put(self.env.gpa, qualified_idx, pattern_idx); + } } } }, @@ -893,12 +871,22 @@ fn processAssociatedItemsSecondPass( const def_idx_u16: u16 = @intCast(@intFromEnum(def_idx)); try self.env.setExposedNodeIndexById(qualified_idx, def_idx_u16); - // Also make all qualified variants available in the current scope - // (This allows `is_empty`, `List.is_empty`, `Builtin.List.is_empty`, etc. to all work) + // Make the real pattern available in current scope (replaces placeholder) const def_cir = self.env.store.getDef(def_idx); const pattern_idx = def_cir.pattern; - const parent_full_text = self.env.getIdent(parent_name); - try self.addAllQualifiedVariants(parent_full_text, name_text, pattern_idx); + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + + // Add unqualified name (e.g., "is_empty") - overwrites placeholder alias + try current_scope.idents.put(self.env.gpa, name_ident, pattern_idx); + + // Add type-qualified name (e.g., "List.is_empty") - overwrites placeholder alias + // Use parent_type_name directly (no string parsing needed!) + const type_qualified_idx = try self.env.insertQualifiedIdent(self.env.getIdent(parent_type_name), name_text); + try current_scope.idents.put(self.env.gpa, type_qualified_idx, pattern_idx); + + // Add fully qualified name (e.g., "Builtin.List.is_empty") - shadows placeholder in parent scope + // We already have qualified_idx = insertQualifiedIdent(parent_text, name_text) + try current_scope.idents.put(self.env.gpa, qualified_idx, pattern_idx); try self.env.store.addScratchDef(def_idx); }, From 55721f46b3b1377a2af34572eb16c3129422086d Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 7 Nov 2025 22:58:44 -0500 Subject: [PATCH 17/38] clean up some comments --- src/canonicalize/Can.zig | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/canonicalize/Can.zig b/src/canonicalize/Can.zig index f7e400ed4c..a8c397a05b 100644 --- a/src/canonicalize/Can.zig +++ b/src/canonicalize/Can.zig @@ -838,16 +838,14 @@ fn processAssociatedItemsSecondPass( const pattern_idx = def_cir.pattern; const current_scope = &self.scopes.items[self.scopes.items.len - 1]; - // Add unqualified name (e.g., "my_not") - overwrites placeholder alias + // Add unqualified name (e.g., "my_not") try current_scope.idents.put(self.env.gpa, decl_ident, pattern_idx); - // Add type-qualified name (e.g., "MyBool.my_not") - overwrites placeholder alias - // Use parent_type_name directly (no string parsing needed!) + // Add type-qualified name (e.g., "MyBool.my_not") const type_qualified_idx = try self.env.insertQualifiedIdent(self.env.getIdent(parent_type_name), decl_text); try current_scope.idents.put(self.env.gpa, type_qualified_idx, pattern_idx); - // Add fully qualified name (e.g., "Test.MyBool.my_not") - shadows placeholder in parent scope - // We already have qualified_idx = insertQualifiedIdent(parent_text, decl_text) + // Add fully qualified name (e.g., "Test.MyBool.my_not") try current_scope.idents.put(self.env.gpa, qualified_idx, pattern_idx); } } @@ -876,16 +874,14 @@ fn processAssociatedItemsSecondPass( const pattern_idx = def_cir.pattern; const current_scope = &self.scopes.items[self.scopes.items.len - 1]; - // Add unqualified name (e.g., "is_empty") - overwrites placeholder alias + // Add unqualified name (e.g., "is_empty") try current_scope.idents.put(self.env.gpa, name_ident, pattern_idx); - // Add type-qualified name (e.g., "List.is_empty") - overwrites placeholder alias - // Use parent_type_name directly (no string parsing needed!) + // Add type-qualified name (e.g., "List.is_empty") const type_qualified_idx = try self.env.insertQualifiedIdent(self.env.getIdent(parent_type_name), name_text); try current_scope.idents.put(self.env.gpa, type_qualified_idx, pattern_idx); - // Add fully qualified name (e.g., "Builtin.List.is_empty") - shadows placeholder in parent scope - // We already have qualified_idx = insertQualifiedIdent(parent_text, name_text) + // Add fully qualified name (e.g., "Builtin.List.is_empty") try current_scope.idents.put(self.env.gpa, qualified_idx, pattern_idx); try self.env.store.addScratchDef(def_idx); From ddb5645a0de17b83a1398e376fb4f027083eaa8e Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 7 Nov 2025 23:07:27 -0500 Subject: [PATCH 18/38] Report shadowing more consistently --- src/canonicalize/Can.zig | 60 ++++++++++++++++++++++++++++------------ 1 file changed, 43 insertions(+), 17 deletions(-) diff --git a/src/canonicalize/Can.zig b/src/canonicalize/Can.zig index a8c397a05b..4c526d84b4 100644 --- a/src/canonicalize/Can.zig +++ b/src/canonicalize/Can.zig @@ -565,12 +565,14 @@ fn processTypeDeclFirstPass( // Look up the fully qualified pattern (from parent scope via nesting) switch (self.scopeLookup(.ident, full_qualified_ident_idx)) { .found => |pattern_idx| { - const scope = &self.scopes.items[self.scopes.items.len - 1]; + const decl_region = self.parse_ir.tokenizedRegionToRegion(nested_decl.region); + // Add unqualified name (e.g., "my_not") - try scope.idents.put(self.env.gpa, nested_decl_ident, pattern_idx); + try self.scopeIntroduceWithDiagnostics(nested_decl_ident, pattern_idx, decl_region); + // Add type-qualified name (e.g., "MyBool.my_not") const type_qualified_ident_idx = try self.env.insertQualifiedIdent(nested_type_text, nested_decl_text); - try scope.idents.put(self.env.gpa, type_qualified_ident_idx, pattern_idx); + try self.scopeIntroduceWithDiagnostics(type_qualified_ident_idx, pattern_idx, decl_region); }, .not_found => {}, } @@ -593,12 +595,14 @@ fn processTypeDeclFirstPass( // Look up the fully qualified pattern (from parent scope via nesting) switch (self.scopeLookup(.ident, fully_qualified_ident_idx)) { .found => |pattern_idx| { - const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + const decl_region = self.parse_ir.tokenizedRegionToRegion(decl.region); + // Add unqualified name (e.g., "my_not") - try current_scope.idents.put(self.env.gpa, decl_ident, pattern_idx); + try self.scopeIntroduceWithDiagnostics(decl_ident, pattern_idx, decl_region); + // Add type-qualified name (e.g., "MyBool.my_not") const type_qualified_ident_idx = try self.env.insertQualifiedIdent(parent_type_text, decl_text); - try current_scope.idents.put(self.env.gpa, type_qualified_ident_idx, pattern_idx); + try self.scopeIntroduceWithDiagnostics(type_qualified_ident_idx, pattern_idx, decl_region); }, .not_found => {}, } @@ -632,12 +636,14 @@ fn processTypeDeclFirstPass( // Look up the fully qualified pattern (from parent scope via nesting) switch (self.scopeLookup(.ident, fully_qualified_ident_idx)) { .found => |pattern_idx| { - const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + const anno_region = self.parse_ir.tokenizedRegionToRegion(type_anno.region); + // Add unqualified name (e.g., "len") - try current_scope.idents.put(self.env.gpa, anno_ident, pattern_idx); + try self.scopeIntroduceWithDiagnostics(anno_ident, pattern_idx, anno_region); + // Add type-qualified name (e.g., "List.len") const type_qualified_ident_idx = try self.env.insertQualifiedIdent(parent_type_text, anno_text); - try current_scope.idents.put(self.env.gpa, type_qualified_ident_idx, pattern_idx); + try self.scopeIntroduceWithDiagnostics(type_qualified_ident_idx, pattern_idx, anno_region); }, .not_found => { // This can happen if the type_anno was followed by a matching decl @@ -836,17 +842,17 @@ fn processAssociatedItemsSecondPass( // but need to update them to point to the real pattern instead of placeholder. const def_cir = self.env.store.getDef(def_idx); const pattern_idx = def_cir.pattern; - const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + const decl_region = self.parse_ir.tokenizedRegionToRegion(decl.region); // Add unqualified name (e.g., "my_not") - try current_scope.idents.put(self.env.gpa, decl_ident, pattern_idx); + try self.scopeIntroduceWithDiagnostics(decl_ident, pattern_idx, decl_region); // Add type-qualified name (e.g., "MyBool.my_not") const type_qualified_idx = try self.env.insertQualifiedIdent(self.env.getIdent(parent_type_name), decl_text); - try current_scope.idents.put(self.env.gpa, type_qualified_idx, pattern_idx); + try self.scopeIntroduceWithDiagnostics(type_qualified_idx, pattern_idx, decl_region); // Add fully qualified name (e.g., "Test.MyBool.my_not") - try current_scope.idents.put(self.env.gpa, qualified_idx, pattern_idx); + try self.scopeIntroduceWithDiagnostics(qualified_idx, pattern_idx, decl_region); } } } @@ -872,17 +878,16 @@ fn processAssociatedItemsSecondPass( // Make the real pattern available in current scope (replaces placeholder) const def_cir = self.env.store.getDef(def_idx); const pattern_idx = def_cir.pattern; - const current_scope = &self.scopes.items[self.scopes.items.len - 1]; // Add unqualified name (e.g., "is_empty") - try current_scope.idents.put(self.env.gpa, name_ident, pattern_idx); + try self.scopeIntroduceWithDiagnostics(name_ident, pattern_idx, region); // Add type-qualified name (e.g., "List.is_empty") const type_qualified_idx = try self.env.insertQualifiedIdent(self.env.getIdent(parent_type_name), name_text); - try current_scope.idents.put(self.env.gpa, type_qualified_idx, pattern_idx); + try self.scopeIntroduceWithDiagnostics(type_qualified_idx, pattern_idx, region); // Add fully qualified name (e.g., "Builtin.List.is_empty") - try current_scope.idents.put(self.env.gpa, qualified_idx, pattern_idx); + try self.scopeIntroduceWithDiagnostics(qualified_idx, pattern_idx, region); try self.env.store.addScratchDef(def_idx); }, @@ -7763,6 +7768,27 @@ pub fn scopeIntroduceInternal( return Scope.IntroduceResult{ .success = {} }; } +/// Introduce an identifier to scope and report shadowing diagnostics if needed +fn scopeIntroduceWithDiagnostics( + self: *Self, + ident_idx: base.Ident.Idx, + pattern_idx: Pattern.Idx, + region: Region, +) std.mem.Allocator.Error!void { + switch (try self.scopeIntroduceInternal(self.env.gpa, .ident, ident_idx, pattern_idx, false, true)) { + .success => {}, + .shadowing_warning => |shadowed_pattern_idx| { + const original_region = self.env.store.getPatternRegion(shadowed_pattern_idx); + try self.env.pushDiagnostic(Diagnostic{ .shadowing_warning = .{ + .ident = ident_idx, + .region = region, + .original_region = original_region, + } }); + }, + .top_level_var_error, .var_across_function_boundary => {}, + } +} + /// Check if an identifier is marked as ignored (underscore prefix) fn identIsIgnored(ident_idx: base.Ident.Idx) bool { return ident_idx.attributes.ignored; From f004fef2ca6826dac0bedadb86342b9caf89dab2 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 7 Nov 2025 23:10:19 -0500 Subject: [PATCH 19/38] Clean up helpers and don't use them unnecesssarily --- src/canonicalize/Can.zig | 73 ++++++++++++++++++++++++++-------------- 1 file changed, 47 insertions(+), 26 deletions(-) diff --git a/src/canonicalize/Can.zig b/src/canonicalize/Can.zig index 4c526d84b4..f3f61a2862 100644 --- a/src/canonicalize/Can.zig +++ b/src/canonicalize/Can.zig @@ -447,7 +447,7 @@ fn processTypeDeclFirstPass( const type_decl_stmt_idx = try self.env.addStatement(placeholder_cir_type_decl, region); // Introduce the type name into scope early to support recursive references - try self.scopeIntroduceTypeDecl(qualified_name_idx, type_decl_stmt_idx, region); + try self.introduceType(qualified_name_idx, type_decl_stmt_idx, region); // Process type parameters and annotation in a separate scope const anno_idx = blk: { @@ -565,14 +565,18 @@ fn processTypeDeclFirstPass( // Look up the fully qualified pattern (from parent scope via nesting) switch (self.scopeLookup(.ident, full_qualified_ident_idx)) { .found => |pattern_idx| { - const decl_region = self.parse_ir.tokenizedRegionToRegion(nested_decl.region); + const scope = &self.scopes.items[self.scopes.items.len - 1]; + + // NOTE: Normally we would use introduceValue() to check for shadowing, but here + // we are creating aliases to make parent scope definitions accessible with shorter + // names in the associated block scope. We use direct .put() to avoid false shadowing warnings. // Add unqualified name (e.g., "my_not") - try self.scopeIntroduceWithDiagnostics(nested_decl_ident, pattern_idx, decl_region); + try scope.idents.put(self.env.gpa, nested_decl_ident, pattern_idx); // Add type-qualified name (e.g., "MyBool.my_not") const type_qualified_ident_idx = try self.env.insertQualifiedIdent(nested_type_text, nested_decl_text); - try self.scopeIntroduceWithDiagnostics(type_qualified_ident_idx, pattern_idx, decl_region); + try scope.idents.put(self.env.gpa, type_qualified_ident_idx, pattern_idx); }, .not_found => {}, } @@ -595,14 +599,18 @@ fn processTypeDeclFirstPass( // Look up the fully qualified pattern (from parent scope via nesting) switch (self.scopeLookup(.ident, fully_qualified_ident_idx)) { .found => |pattern_idx| { - const decl_region = self.parse_ir.tokenizedRegionToRegion(decl.region); + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + + // NOTE: Normally we would use introduceValue() to check for shadowing, but here + // we are creating aliases to make parent scope definitions accessible with shorter + // names in the associated block scope. We use direct .put() to avoid false shadowing warnings. // Add unqualified name (e.g., "my_not") - try self.scopeIntroduceWithDiagnostics(decl_ident, pattern_idx, decl_region); + try current_scope.idents.put(self.env.gpa, decl_ident, pattern_idx); // Add type-qualified name (e.g., "MyBool.my_not") const type_qualified_ident_idx = try self.env.insertQualifiedIdent(parent_type_text, decl_text); - try self.scopeIntroduceWithDiagnostics(type_qualified_ident_idx, pattern_idx, decl_region); + try current_scope.idents.put(self.env.gpa, type_qualified_ident_idx, pattern_idx); }, .not_found => {}, } @@ -636,14 +644,18 @@ fn processTypeDeclFirstPass( // Look up the fully qualified pattern (from parent scope via nesting) switch (self.scopeLookup(.ident, fully_qualified_ident_idx)) { .found => |pattern_idx| { - const anno_region = self.parse_ir.tokenizedRegionToRegion(type_anno.region); + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + + // NOTE: Normally we would use introduceValue() to check for shadowing, but here + // we are creating aliases to make parent scope definitions accessible with shorter + // names in the associated block scope. We use direct .put() to avoid false shadowing warnings. // Add unqualified name (e.g., "len") - try self.scopeIntroduceWithDiagnostics(anno_ident, pattern_idx, anno_region); + try current_scope.idents.put(self.env.gpa, anno_ident, pattern_idx); // Add type-qualified name (e.g., "List.len") const type_qualified_ident_idx = try self.env.insertQualifiedIdent(parent_type_text, anno_text); - try self.scopeIntroduceWithDiagnostics(type_qualified_ident_idx, pattern_idx, anno_region); + try current_scope.idents.put(self.env.gpa, type_qualified_ident_idx, pattern_idx); }, .not_found => { // This can happen if the type_anno was followed by a matching decl @@ -842,17 +854,21 @@ fn processAssociatedItemsSecondPass( // but need to update them to point to the real pattern instead of placeholder. const def_cir = self.env.store.getDef(def_idx); const pattern_idx = def_cir.pattern; - const decl_region = self.parse_ir.tokenizedRegionToRegion(decl.region); + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; - // Add unqualified name (e.g., "my_not") - try self.scopeIntroduceWithDiagnostics(decl_ident, pattern_idx, decl_region); + // NOTE: Normally we would use introduceValue() to check for shadowing, but here + // we are updating existing placeholder entries (not introducing new names), so we + // use direct .put() to avoid false duplicate definition diagnostics. - // Add type-qualified name (e.g., "MyBool.my_not") + // Update unqualified name (e.g., "my_not") + try current_scope.idents.put(self.env.gpa, decl_ident, pattern_idx); + + // Update type-qualified name (e.g., "MyBool.my_not") const type_qualified_idx = try self.env.insertQualifiedIdent(self.env.getIdent(parent_type_name), decl_text); - try self.scopeIntroduceWithDiagnostics(type_qualified_idx, pattern_idx, decl_region); + try current_scope.idents.put(self.env.gpa, type_qualified_idx, pattern_idx); - // Add fully qualified name (e.g., "Test.MyBool.my_not") - try self.scopeIntroduceWithDiagnostics(qualified_idx, pattern_idx, decl_region); + // Update fully qualified name (e.g., "Test.MyBool.my_not") + try current_scope.idents.put(self.env.gpa, qualified_idx, pattern_idx); } } } @@ -878,16 +894,21 @@ fn processAssociatedItemsSecondPass( // Make the real pattern available in current scope (replaces placeholder) const def_cir = self.env.store.getDef(def_idx); const pattern_idx = def_cir.pattern; + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; - // Add unqualified name (e.g., "is_empty") - try self.scopeIntroduceWithDiagnostics(name_ident, pattern_idx, region); + // NOTE: Normally we would use introduceValue() to check for shadowing, but here + // we are updating existing placeholder entries (not introducing new names), so we + // use direct .put() to avoid false duplicate definition diagnostics. - // Add type-qualified name (e.g., "List.is_empty") + // Update unqualified name (e.g., "is_empty") + try current_scope.idents.put(self.env.gpa, name_ident, pattern_idx); + + // Update type-qualified name (e.g., "List.is_empty") const type_qualified_idx = try self.env.insertQualifiedIdent(self.env.getIdent(parent_type_name), name_text); - try self.scopeIntroduceWithDiagnostics(type_qualified_idx, pattern_idx, region); + try current_scope.idents.put(self.env.gpa, type_qualified_idx, pattern_idx); - // Add fully qualified name (e.g., "Builtin.List.is_empty") - try self.scopeIntroduceWithDiagnostics(qualified_idx, pattern_idx, region); + // Update fully qualified name (e.g., "Builtin.List.is_empty") + try current_scope.idents.put(self.env.gpa, qualified_idx, pattern_idx); try self.env.store.addScratchDef(def_idx); }, @@ -7768,8 +7789,8 @@ pub fn scopeIntroduceInternal( return Scope.IntroduceResult{ .success = {} }; } -/// Introduce an identifier to scope and report shadowing diagnostics if needed -fn scopeIntroduceWithDiagnostics( +/// Introduce a value identifier to scope and report shadowing diagnostics if needed +fn introduceValue( self: *Self, ident_idx: base.Ident.Idx, pattern_idx: Pattern.Idx, @@ -7886,7 +7907,7 @@ fn checkScopeForUnusedVariables(self: *Self, scope: *const Scope) std.mem.Alloca } /// Introduce a type declaration into the current scope -fn scopeIntroduceTypeDecl( +fn introduceType( self: *Self, name_ident: Ident.Idx, type_decl_stmt: Statement.Idx, From ef34c0574a8b74a11607f5abb918ed39fb37d56b Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 7 Nov 2025 23:23:58 -0500 Subject: [PATCH 20/38] Revert an unnecessary loop. --- src/check/Check.zig | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/src/check/Check.zig b/src/check/Check.zig index ff22566c48..b006db8bf3 100644 --- a/src/check/Check.zig +++ b/src/check/Check.zig @@ -596,26 +596,16 @@ fn updateVar(self: *Self, target_var: Var, content: types_mod.Content, rank: typ /// other modules directly. The Bool and Result types are used in language constructs like /// `if` conditions and need to be available in every module's type store. fn copyBuiltinTypes(self: *Self) !void { + const bool_stmt_idx = self.common_idents.bool_stmt; + if (self.common_idents.builtin_module) |builtin_env| { // Copy Bool type from Builtin module using the direct reference - const bool_stmt_idx = self.common_idents.bool_stmt; const bool_type_var = ModuleEnv.varFrom(bool_stmt_idx); self.bool_var = try self.copyVar(bool_type_var, builtin_env, Region.zero()); } else { - // If Builtin module reference is null, we're compiling the Builtin module itself - // Search for the Bool type declaration in all_statements - const all_stmts = self.cir.store.sliceStatements(self.cir.all_statements); - for (all_stmts) |stmt_idx| { - const stmt = self.cir.store.getStatement(stmt_idx); - if (stmt == .s_nominal_decl) { - const header = self.cir.store.getTypeHeader(stmt.s_nominal_decl.header); - const ident_text = self.cir.getIdent(header.name); - if (std.mem.eql(u8, ident_text, "Builtin.Bool")) { - self.bool_var = ModuleEnv.varFrom(stmt_idx); - break; - } - } - } + // If Builtin module reference is null, use the statement from the current module + // This happens when compiling the Builtin module itself + self.bool_var = ModuleEnv.varFrom(bool_stmt_idx); } // Result type is accessed via external references, no need to copy it here From f820b2c1abc01cbe4a871cb63db0355ba9cba3a6 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 7 Nov 2025 23:32:29 -0500 Subject: [PATCH 21/38] Remove some unnecessary stuff --- src/check/Check.zig | 18 +++++++++--------- src/eval/test/anno_only_interp_test.zig | 16 +--------------- 2 files changed, 10 insertions(+), 24 deletions(-) diff --git a/src/check/Check.zig b/src/check/Check.zig index b006db8bf3..2f6824ab9c 100644 --- a/src/check/Check.zig +++ b/src/check/Check.zig @@ -950,7 +950,7 @@ fn generateAnnotationType(self: *Self, annotation_idx: CIR.Annotation.Idx) std.m try self.generateAnnoTypeInPlace(annotation.anno, .annotation); // Redirect the root annotation to inner annotation - _ = try self.types.setVarRedirect(ModuleEnv.varFrom(annotation_idx), ModuleEnv.varFrom(annotation.anno)); + try self.types.setVarRedirect(ModuleEnv.varFrom(annotation_idx), ModuleEnv.varFrom(annotation.anno)); } /// Given a where clause, generate static dispatch constraints and add to scratch_static_dispatch_constraints @@ -2455,9 +2455,9 @@ fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx, rank: types_mod.Rank, expected // We never instantiate rigid variables if (resolved_pat.rank == Rank.generalized and resolved_pat.content != .rigid) { const instantiated = try self.instantiateVar(pat_var, rank, .use_last_var); - _ = try self.types.setVarRedirect(expr_var, instantiated); + try self.types.setVarRedirect(expr_var, instantiated); } else { - _ = try self.types.setVarRedirect(expr_var, pat_var); + try self.types.setVarRedirect(expr_var, pat_var); } // Unify this expression with the referenced pattern @@ -2649,7 +2649,7 @@ fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx, rank: types_mod.Rank, expected }, .e_closure => |closure| { does_fx = try self.checkExpr(closure.lambda_idx, rank, expected) or does_fx; - _ = try self.types.setVarRedirect(expr_var, ModuleEnv.varFrom(closure.lambda_idx)); + try self.types.setVarRedirect(expr_var, ModuleEnv.varFrom(closure.lambda_idx)); }, // function calling // .e_call => |call| { @@ -2798,7 +2798,7 @@ fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx, rank: types_mod.Rank, expected } // Redirect the expr to the function's return type - _ = try self.types.setVarRedirect(expr_var, func.ret); + try self.types.setVarRedirect(expr_var, func.ret); } else { // TODO(jared): Better arity difference error message @@ -2815,7 +2815,7 @@ fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx, rank: types_mod.Rank, expected try self.var_pool.addVarToRank(call_func_var, rank); _ = try self.unify(func_var, call_func_var, rank); - _ = try self.types.setVarRedirect(expr_var, call_func_ret); + try self.types.setVarRedirect(expr_var, call_func_ret); } } else { // We get here if the type of expr being called @@ -2843,7 +2843,7 @@ fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx, rank: types_mod.Rank, expected // Then, we set the root expr to redirect to the return // type of that function, since a call expr ultimate // resolve to the returned type - _ = try self.types.setVarRedirect(expr_var, call_func_ret); + try self.types.setVarRedirect(expr_var, call_func_ret); } } }, @@ -2968,7 +2968,7 @@ fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx, rank: types_mod.Rank, expected }, .e_dbg => |dbg| { does_fx = try self.checkExpr(dbg.expr, rank, expected) or does_fx; - _ = try self.types.setVarRedirect(expr_var, ModuleEnv.varFrom(dbg.expr)); + try self.types.setVarRedirect(expr_var, ModuleEnv.varFrom(dbg.expr)); }, .e_expect => |expect| { does_fx = try self.checkExpr(expect.body, rank, expected) or does_fx; @@ -2988,7 +2988,7 @@ fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx, rank: types_mod.Rank, expected }, .expected => |expected_type| { // Redirect expr_var to the annotation var so that lookups get the correct type - _ = try self.types.setVarRedirect(expr_var, expected_type.var_); + try self.types.setVarRedirect(expr_var, expected_type.var_); }, } }, diff --git a/src/eval/test/anno_only_interp_test.zig b/src/eval/test/anno_only_interp_test.zig index 5890d482e0..167901aba0 100644 --- a/src/eval/test/anno_only_interp_test.zig +++ b/src/eval/test/anno_only_interp_test.zig @@ -213,8 +213,6 @@ test "e_anno_only - value only crashes when accessed (False branch)" { test "List.first on nonempty list" { const src = - \\import Builtin exposing [List, Try] - \\ \\result = List.first([1, 2, 3]) ; @@ -230,8 +228,6 @@ test "List.first on nonempty list" { test "List.get with valid index returns Ok" { const src = - \\import Builtin exposing [List, Try] - \\ \\result = List.get([1, 2, 3], 1) ; @@ -247,8 +243,6 @@ test "List.get with valid index returns Ok" { test "List.get with invalid index returns Err" { const src = - \\import Builtin exposing [List, Try] - \\ \\result = List.get([1, 2, 3], 10) ; @@ -264,8 +258,6 @@ test "List.get with invalid index returns Err" { test "List.get on empty list returns Err" { const src = - \\import Builtin exposing [List, Try] - \\ \\empty : List(U64) \\empty = [] \\result = List.get(empty, 0) @@ -283,8 +275,6 @@ test "List.get on empty list returns Err" { test "List.get with different element types - Str" { const src = - \\import Builtin exposing [List, Try] - \\ \\result = List.get(["foo", "bar", "baz"], 1) ; @@ -300,9 +290,7 @@ test "List.get with different element types - Str" { test "List.get with different element types - Bool" { const src = - \\import Builtin exposing [List, Try, Bool] - \\ - \\result = List.get([Bool.True, Bool.False, Bool.True], 2) + \\result = List.get([True, False, True], 2) ; var result = try parseCheckAndEvalModule(src); @@ -317,8 +305,6 @@ test "List.get with different element types - Bool" { test "List.get with nested lists" { const src = - \\import Builtin exposing [List, Try] - \\ \\result = List.get([[1, 2], [3, 4], [5, 6]], 1) ; From bb2221f698ae281d1a981273fe68e533176f2256 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 7 Nov 2025 23:49:31 -0500 Subject: [PATCH 22/38] Make Builtin itself un-importable --- src/canonicalize/Can.zig | 16 ++----- src/check/mod.zig | 1 + src/check/test/TestEnv.zig | 38 +++++++---------- src/check/test/builtin_scope_test.zig | 60 +++++++++++++++++++++++++++ 4 files changed, 79 insertions(+), 36 deletions(-) create mode 100644 src/check/test/builtin_scope_test.zig diff --git a/src/canonicalize/Can.zig b/src/canonicalize/Can.zig index f3f61a2862..79569c7738 100644 --- a/src/canonicalize/Can.zig +++ b/src/canonicalize/Can.zig @@ -304,27 +304,17 @@ pub fn setupAutoImportedBuiltinTypes( if (envs_map.get(type_ident)) |type_entry| { const module_env = type_entry.env; - // Create an import for the parent Builtin module (only once, shared across all types) + // Create an import entry for the parent Builtin module + // This is needed for type bindings to reference, but we do NOT add it to + // import_indices or scope - "Builtin" should not be directly importable! const builtin_module_name = module_env.module_name; - // Check if we already have this import in our indices - const is_new_import = !self.import_indices.contains(builtin_module_name); - const module_import_idx = try self.env.imports.getOrPut( gpa, self.env.common.getStringStore(), builtin_module_name, ); - if (is_new_import) { - // Add to import_indices so getOrCreateAutoImport can find it - try self.import_indices.put(gpa, builtin_module_name, module_import_idx); - - // Also add to current scope so scopeLookupImportedModule can find it - // This ensures consistency with getOrCreateAutoImport - _ = try current_scope.introduceImportedModule(gpa, builtin_module_name, module_import_idx); - } - // Get target_node_idx from statement_idx const target_node_idx = if (type_entry.statement_idx) |stmt_idx| module_env.getExposedNodeIndexByStatementIdx(stmt_idx) diff --git a/src/check/mod.zig b/src/check/mod.zig index b31fd3f9b9..6f62c93860 100644 --- a/src/check/mod.zig +++ b/src/check/mod.zig @@ -40,4 +40,5 @@ test "check tests" { std.testing.refAllDecls(@import("test/let_polymorphism_integration_test.zig")); std.testing.refAllDecls(@import("test/num_type_inference_test.zig")); std.testing.refAllDecls(@import("test/num_type_requirements_test.zig")); + std.testing.refAllDecls(@import("test/builtin_scope_test.zig")); } diff --git a/src/check/test/TestEnv.zig b/src/check/test/TestEnv.zig index ab3f271d1e..a8c5911d0b 100644 --- a/src/check/test/TestEnv.zig +++ b/src/check/test/TestEnv.zig @@ -215,21 +215,21 @@ pub fn initWithImport(module_name: []const u8, source: []const u8, other_module_ .builtin_module = other_test_env.builtin_module.env, }; - // Build imported_envs array dynamically based on module_env.imports order - // This matches the production approach in compile_package.zig - const import_count = module_env.imports.imports.items.items.len; - var imported_envs = try std.ArrayList(*const ModuleEnv).initCapacity(gpa, import_count); + // Build imported_envs array + // Always include the builtin module for auto-imported types (Bool, Str, etc.) + var imported_envs = try std.ArrayList(*const ModuleEnv).initCapacity(gpa, 2); defer imported_envs.deinit(gpa); + // Add builtin module unconditionally (needed for auto-imported types) + try imported_envs.append(gpa, other_test_env.builtin_module.env); + + // Process explicit imports + const import_count = module_env.imports.imports.items.items.len; for (module_env.imports.imports.items.items[0..import_count]) |str_idx| { const import_name = module_env.getString(str_idx); - if (std.mem.eql(u8, import_name, "Builtin")) { - try imported_envs.append(gpa, other_test_env.builtin_module.env); - } else if (std.mem.eql(u8, import_name, other_module_name)) { + if (std.mem.eql(u8, import_name, other_module_name)) { // Cross-module import - append the other test module's env try imported_envs.append(gpa, other_test_env.module_env); - } else { - std.debug.print("WARNING: Unknown import in test: {s}\n", .{import_name}); } } @@ -331,22 +331,14 @@ pub fn init(module_name: []const u8, source: []const u8) !TestEnv { .builtin_module = builtin_module.env, }; - // Build imported_envs array dynamically based on module_env.imports order - // This matches the production approach in compile_package.zig - const import_count = module_env.imports.imports.items.items.len; - var imported_envs = try std.ArrayList(*const ModuleEnv).initCapacity(gpa, import_count); + // Build imported_envs array + // Always include the builtin module for auto-imported types (Bool, Str, etc.) + var imported_envs = try std.ArrayList(*const ModuleEnv).initCapacity(gpa, 2); defer imported_envs.deinit(gpa); - for (module_env.imports.imports.items.items[0..import_count]) |str_idx| { - const import_name = module_env.getString(str_idx); - // For tests, all imports are to the Builtin module - if (std.mem.eql(u8, import_name, "Builtin")) { - try imported_envs.append(gpa, builtin_module.env); - } else { - // If there are other imports in the future, handle them here - std.debug.print("WARNING: Unknown import in test: {s}\n", .{import_name}); - } - } + // Add builtin module unconditionally (needed for auto-imported types) + try imported_envs.append(gpa, builtin_module.env); + // Type Check - Pass the imported modules in other_modules parameter var checker = try Check.init( diff --git a/src/check/test/builtin_scope_test.zig b/src/check/test/builtin_scope_test.zig new file mode 100644 index 0000000000..830bae73f3 --- /dev/null +++ b/src/check/test/builtin_scope_test.zig @@ -0,0 +1,60 @@ +//! Tests verifying that "Builtin" is not in scope and cannot be imported, +//! but that nested types like Str, List, etc. are available. + +const TestEnv = @import("./TestEnv.zig"); +const testing = @import("std").testing; +const std = @import("std"); + +test "cannot import Builtin module" { + const src = + \\import Builtin + \\ + \\x = 5 + ; + + var test_env = try TestEnv.init("Test", src); + defer test_env.deinit(); + + // Should have a canonicalization problem because Builtin is not a module that can be imported + const diagnostics = try test_env.module_env.getDiagnostics(); + defer test_env.module_env.gpa.free(diagnostics); + + // Expect at least one diagnostic (module not found error) + try testing.expect(diagnostics.len > 0); +} + +test "can define userspace type named Builtin" { + const src = + \\Test := [A, B, C] + \\ + \\Builtin := [D, E, F] + \\ + \\x : Builtin + \\x = D + ; + + var test_env = try TestEnv.init("Test", src); + defer test_env.deinit(); + + // Should have no problems - Builtin is a valid userspace name + try test_env.assertDefType("x", "Builtin"); +} + +test "builtin types are still available without import" { + const src = + \\Test := [Whatever] + \\ + \\x : Str + \\x = "hello" + \\ + \\y : List(U64) + \\y = [1, 2, 3] + ; + + var test_env = try TestEnv.init("Test", src); + defer test_env.deinit(); + + // Builtin types like Str and List should work without importing Builtin + try test_env.assertDefType("x", "Str"); + try test_env.assertDefType("y", "List(Num(Int(Unsigned64)))"); +} From 197bc0aa05fa3d2ee084fe9df24f274de1f3747d Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Fri, 7 Nov 2025 23:49:51 -0500 Subject: [PATCH 23/38] zig fmt --- src/check/test/TestEnv.zig | 1 - 1 file changed, 1 deletion(-) diff --git a/src/check/test/TestEnv.zig b/src/check/test/TestEnv.zig index a8c5911d0b..318e4b1006 100644 --- a/src/check/test/TestEnv.zig +++ b/src/check/test/TestEnv.zig @@ -339,7 +339,6 @@ pub fn init(module_name: []const u8, source: []const u8) !TestEnv { // Add builtin module unconditionally (needed for auto-imported types) try imported_envs.append(gpa, builtin_module.env); - // Type Check - Pass the imported modules in other_modules parameter var checker = try Check.init( gpa, From 61f0aa135dd9a3cb718f7cb850fa853b2cc52123 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Sat, 8 Nov 2025 00:14:48 -0500 Subject: [PATCH 24/38] Extract updatePlaceholder --- src/canonicalize/Can.zig | 50 +++++++++++++-------------- src/check/test/builtin_scope_test.zig | 38 ++++++++++++++++++++ 2 files changed, 62 insertions(+), 26 deletions(-) diff --git a/src/canonicalize/Can.zig b/src/canonicalize/Can.zig index 79569c7738..fe187ab0db 100644 --- a/src/canonicalize/Can.zig +++ b/src/canonicalize/Can.zig @@ -4,6 +4,7 @@ //! constructs into a simplified, normalized form suitable for type inference. const std = @import("std"); +const builtin = @import("builtin"); const testing = std.testing; const base = @import("base"); const parse = @import("parse"); @@ -557,10 +558,6 @@ fn processTypeDeclFirstPass( .found => |pattern_idx| { const scope = &self.scopes.items[self.scopes.items.len - 1]; - // NOTE: Normally we would use introduceValue() to check for shadowing, but here - // we are creating aliases to make parent scope definitions accessible with shorter - // names in the associated block scope. We use direct .put() to avoid false shadowing warnings. - // Add unqualified name (e.g., "my_not") try scope.idents.put(self.env.gpa, nested_decl_ident, pattern_idx); @@ -591,10 +588,6 @@ fn processTypeDeclFirstPass( .found => |pattern_idx| { const current_scope = &self.scopes.items[self.scopes.items.len - 1]; - // NOTE: Normally we would use introduceValue() to check for shadowing, but here - // we are creating aliases to make parent scope definitions accessible with shorter - // names in the associated block scope. We use direct .put() to avoid false shadowing warnings. - // Add unqualified name (e.g., "my_not") try current_scope.idents.put(self.env.gpa, decl_ident, pattern_idx); @@ -636,10 +629,6 @@ fn processTypeDeclFirstPass( .found => |pattern_idx| { const current_scope = &self.scopes.items[self.scopes.items.len - 1]; - // NOTE: Normally we would use introduceValue() to check for shadowing, but here - // we are creating aliases to make parent scope definitions accessible with shorter - // names in the associated block scope. We use direct .put() to avoid false shadowing warnings. - // Add unqualified name (e.g., "len") try current_scope.idents.put(self.env.gpa, anno_ident, pattern_idx); @@ -846,19 +835,15 @@ fn processAssociatedItemsSecondPass( const pattern_idx = def_cir.pattern; const current_scope = &self.scopes.items[self.scopes.items.len - 1]; - // NOTE: Normally we would use introduceValue() to check for shadowing, but here - // we are updating existing placeholder entries (not introducing new names), so we - // use direct .put() to avoid false duplicate definition diagnostics. - // Update unqualified name (e.g., "my_not") - try current_scope.idents.put(self.env.gpa, decl_ident, pattern_idx); + try self.updatePlaceholder(current_scope, decl_ident, pattern_idx); // Update type-qualified name (e.g., "MyBool.my_not") const type_qualified_idx = try self.env.insertQualifiedIdent(self.env.getIdent(parent_type_name), decl_text); - try current_scope.idents.put(self.env.gpa, type_qualified_idx, pattern_idx); + try self.updatePlaceholder(current_scope, type_qualified_idx, pattern_idx); // Update fully qualified name (e.g., "Test.MyBool.my_not") - try current_scope.idents.put(self.env.gpa, qualified_idx, pattern_idx); + try self.updatePlaceholder(current_scope, qualified_idx, pattern_idx); } } } @@ -886,19 +871,15 @@ fn processAssociatedItemsSecondPass( const pattern_idx = def_cir.pattern; const current_scope = &self.scopes.items[self.scopes.items.len - 1]; - // NOTE: Normally we would use introduceValue() to check for shadowing, but here - // we are updating existing placeholder entries (not introducing new names), so we - // use direct .put() to avoid false duplicate definition diagnostics. - // Update unqualified name (e.g., "is_empty") - try current_scope.idents.put(self.env.gpa, name_ident, pattern_idx); + try self.updatePlaceholder(current_scope, name_ident, pattern_idx); // Update type-qualified name (e.g., "List.is_empty") const type_qualified_idx = try self.env.insertQualifiedIdent(self.env.getIdent(parent_type_name), name_text); - try current_scope.idents.put(self.env.gpa, type_qualified_idx, pattern_idx); + try self.updatePlaceholder(current_scope, type_qualified_idx, pattern_idx); // Update fully qualified name (e.g., "Builtin.List.is_empty") - try current_scope.idents.put(self.env.gpa, qualified_idx, pattern_idx); + try self.updatePlaceholder(current_scope, qualified_idx, pattern_idx); try self.env.store.addScratchDef(def_idx); }, @@ -8029,6 +8010,23 @@ fn introduceType( } } +/// Update a placeholder pattern in scope with the actual pattern. +/// In debug builds, asserts that the old value was actually a placeholder (.assign pattern). +fn updatePlaceholder( + self: *Self, + scope: *Scope, + ident_idx: Ident.Idx, + pattern_idx: Pattern.Idx, +) std.mem.Allocator.Error!void { + if (builtin.mode == .Debug) { + if (scope.idents.get(ident_idx)) |old_pattern_idx| { + const old_pattern = self.env.store.getPattern(old_pattern_idx); + std.debug.assert(old_pattern == .assign); + } + } + try scope.idents.put(self.env.gpa, ident_idx, pattern_idx); +} + fn scopeUpdateTypeDecl( self: *Self, name_ident: Ident.Idx, diff --git a/src/check/test/builtin_scope_test.zig b/src/check/test/builtin_scope_test.zig index 830bae73f3..5935ae587b 100644 --- a/src/check/test/builtin_scope_test.zig +++ b/src/check/test/builtin_scope_test.zig @@ -58,3 +58,41 @@ test "builtin types are still available without import" { try test_env.assertDefType("x", "Str"); try test_env.assertDefType("y", "List(Num(Int(Unsigned64)))"); } + +test "can import userspace Builtin module" { + const builtin_module_src = + \\Builtin := [D, E, F] + \\ + \\value : Builtin + \\value = D + ; + + var builtin_module = try TestEnv.init("Builtin", builtin_module_src); + defer builtin_module.deinit(); + + const main_src = + \\Main := [Whatever] + \\ + \\import Builtin + \\ + \\x : Builtin + \\x = Builtin.value + ; + + var main_module = try TestEnv.initWithImport("Main", main_src, "Builtin", &builtin_module); + defer main_module.deinit(); + + // Should successfully import the userspace Builtin module without "module not found" error + const diagnostics = try main_module.module_env.getDiagnostics(); + defer main_module.module_env.gpa.free(diagnostics); + + // Check that there's no "module not found" error for "Builtin" + for (diagnostics) |diag| { + if (diag == .module_not_found) { + const module_name = main_module.module_env.getIdent(diag.module_not_found.module_name); + if (std.mem.eql(u8, module_name, "Builtin")) { + try testing.expect(false); // Should not have module_not_found for Builtin + } + } + } +} From fb5b9b53ffa9fb44942d769e86283446789cbdf6 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Sat, 8 Nov 2025 00:23:31 -0500 Subject: [PATCH 25/38] Fix CI --- src/canonicalize/Can.zig | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/canonicalize/Can.zig b/src/canonicalize/Can.zig index fe187ab0db..314d126c3b 100644 --- a/src/canonicalize/Can.zig +++ b/src/canonicalize/Can.zig @@ -306,8 +306,8 @@ pub fn setupAutoImportedBuiltinTypes( const module_env = type_entry.env; // Create an import entry for the parent Builtin module - // This is needed for type bindings to reference, but we do NOT add it to - // import_indices or scope - "Builtin" should not be directly importable! + // Add to import_indices (needed for type resolution) but NOT to scope + // (so "Builtin" is not directly importable) const builtin_module_name = module_env.module_name; const module_import_idx = try self.env.imports.getOrPut( @@ -316,6 +316,11 @@ pub fn setupAutoImportedBuiltinTypes( builtin_module_name, ); + const is_new_import = !self.import_indices.contains(builtin_module_name); + if (is_new_import) { + try self.import_indices.put(gpa, builtin_module_name, module_import_idx); + } + // Get target_node_idx from statement_idx const target_node_idx = if (type_entry.statement_idx) |stmt_idx| module_env.getExposedNodeIndexByStatementIdx(stmt_idx) From aa1169f3d9364e0e1b78bad43c6e203db6d4f111 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Sat, 8 Nov 2025 00:36:55 -0500 Subject: [PATCH 26/38] Do stuff in a less bad way --- src/canonicalize/Can.zig | 34 +++++++++++++++------------------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/src/canonicalize/Can.zig b/src/canonicalize/Can.zig index 314d126c3b..051e330324 100644 --- a/src/canonicalize/Can.zig +++ b/src/canonicalize/Can.zig @@ -300,30 +300,26 @@ pub fn setupAutoImportedBuiltinTypes( const current_scope = &self.scopes.items[0]; // Top-level scope const builtin_types = [_][]const u8{ "Bool", "Result", "Dict", "Set", "Str", "U8", "I8", "U16", "I16", "U32", "I32", "U64", "I64", "U128", "I128", "Dec", "F32", "F64" }; + + var builtin_import_idx: ?Import.Idx = null; for (builtin_types) |type_name_text| { const type_ident = try env.insertIdent(base.Ident.for_text(type_name_text)); if (envs_map.get(type_ident)) |type_entry| { - const module_env = type_entry.env; - - // Create an import entry for the parent Builtin module - // Add to import_indices (needed for type resolution) but NOT to scope - // (so "Builtin" is not directly importable) - const builtin_module_name = module_env.module_name; - - const module_import_idx = try self.env.imports.getOrPut( - gpa, - self.env.common.getStringStore(), - builtin_module_name, - ); - - const is_new_import = !self.import_indices.contains(builtin_module_name); - if (is_new_import) { - try self.import_indices.put(gpa, builtin_module_name, module_import_idx); - } + // Add Builtin module to import_indices on first match + const import_idx = builtin_import_idx orelse blk: { + const idx = try self.env.imports.getOrPut( + gpa, + self.env.common.getStringStore(), + "Builtin", + ); + try self.import_indices.put(gpa, "Builtin", idx); + builtin_import_idx = idx; + break :blk idx; + }; // Get target_node_idx from statement_idx const target_node_idx = if (type_entry.statement_idx) |stmt_idx| - module_env.getExposedNodeIndexByStatementIdx(stmt_idx) + type_entry.env.getExposedNodeIndexByStatementIdx(stmt_idx) else null; @@ -333,7 +329,7 @@ pub fn setupAutoImportedBuiltinTypes( .module_ident = type_ident, // Use type name as module ident for module_envs lookup .original_ident = type_ident, .target_node_idx = target_node_idx, - .import_idx = module_import_idx, + .import_idx = import_idx, .origin_region = zero_region, .module_not_found = false, }, From 1fc21773adc718076a42659874cf36548a3a08d9 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Sat, 8 Nov 2025 00:47:18 -0500 Subject: [PATCH 27/38] Clean up some test code --- src/canonicalize/Can.zig | 23 +++++++-------------- src/eval/test/comptime_eval_test.zig | 31 +++++++++++++++++++--------- 2 files changed, 29 insertions(+), 25 deletions(-) diff --git a/src/canonicalize/Can.zig b/src/canonicalize/Can.zig index 051e330324..e52cb05e95 100644 --- a/src/canonicalize/Can.zig +++ b/src/canonicalize/Can.zig @@ -299,24 +299,17 @@ pub fn setupAutoImportedBuiltinTypes( const zero_region = Region{ .start = Region.Position.zero(), .end = Region.Position.zero() }; const current_scope = &self.scopes.items[0]; // Top-level scope - const builtin_types = [_][]const u8{ "Bool", "Result", "Dict", "Set", "Str", "U8", "I8", "U16", "I16", "U32", "I32", "U64", "I64", "U128", "I128", "Dec", "F32", "F64" }; + const builtin_import_idx = try self.env.imports.getOrPut( + gpa, + self.env.common.getStringStore(), + "Builtin", + ); + try self.import_indices.put(gpa, "Builtin", builtin_import_idx); - var builtin_import_idx: ?Import.Idx = null; + const builtin_types = [_][]const u8{ "Bool", "Result", "Dict", "Set", "Str", "U8", "I8", "U16", "I16", "U32", "I32", "U64", "I64", "U128", "I128", "Dec", "F32", "F64" }; for (builtin_types) |type_name_text| { const type_ident = try env.insertIdent(base.Ident.for_text(type_name_text)); if (envs_map.get(type_ident)) |type_entry| { - // Add Builtin module to import_indices on first match - const import_idx = builtin_import_idx orelse blk: { - const idx = try self.env.imports.getOrPut( - gpa, - self.env.common.getStringStore(), - "Builtin", - ); - try self.import_indices.put(gpa, "Builtin", idx); - builtin_import_idx = idx; - break :blk idx; - }; - // Get target_node_idx from statement_idx const target_node_idx = if (type_entry.statement_idx) |stmt_idx| type_entry.env.getExposedNodeIndexByStatementIdx(stmt_idx) @@ -329,7 +322,7 @@ pub fn setupAutoImportedBuiltinTypes( .module_ident = type_ident, // Use type name as module ident for module_envs lookup .original_ident = type_ident, .target_node_idx = target_node_idx, - .import_idx = import_idx, + .import_idx = builtin_import_idx, .origin_region = zero_region, .module_not_found = false, }, diff --git a/src/eval/test/comptime_eval_test.zig b/src/eval/test/comptime_eval_test.zig index 63d9cf2e80..7d40526d39 100644 --- a/src/eval/test/comptime_eval_test.zig +++ b/src/eval/test/comptime_eval_test.zig @@ -19,13 +19,19 @@ const ModuleEnv = can.ModuleEnv; const testing = std.testing; const test_allocator = testing.allocator; -/// Helper to parse, canonicalize, type-check, and run comptime evaluation on a full module -fn parseCheckAndEvalModule(src: []const u8) !struct { +const EvalModuleResult = struct { module_env: *ModuleEnv, evaluator: ComptimeEvaluator, problems: *check.problem.Store, builtin_module: builtin_loading.LoadedModule, -} { +}; + +/// Helper to parse, canonicalize, type-check, and run comptime evaluation on a full module +fn parseCheckAndEvalModule(src: []const u8) !EvalModuleResult { + return parseCheckAndEvalModuleWithName(src, "TestModule"); +} + +fn parseCheckAndEvalModuleWithName(src: []const u8, module_name: []const u8) !EvalModuleResult { const gpa = test_allocator; const module_env = try gpa.create(ModuleEnv); @@ -34,7 +40,7 @@ fn parseCheckAndEvalModule(src: []const u8) !struct { errdefer module_env.deinit(); module_env.common.source = src; - module_env.module_name = "TestModule"; + module_env.module_name = module_name; try module_env.common.calcLineStarts(module_env.gpa); // Parse the source code @@ -51,9 +57,9 @@ fn parseCheckAndEvalModule(src: []const u8) !struct { errdefer builtin_module.deinit(); // Initialize CIR fields in ModuleEnv - try module_env.initCIRFields(gpa, "test"); + try module_env.initCIRFields(gpa, module_name); const common_idents: Check.CommonIdents = .{ - .module_name = try module_env.insertIdent(base.Ident.for_text("test")), + .module_name = try module_env.insertIdent(base.Ident.for_text(module_name)), .list = try module_env.insertIdent(base.Ident.for_text("List")), .box = try module_env.insertIdent(base.Ident.for_text("Box")), .bool_stmt = builtin_indices.bool_type, @@ -91,14 +97,16 @@ fn parseCheckAndEvalModule(src: []const u8) !struct { }; } -/// Helper to parse, canonicalize, type-check, and run comptime evaluation with imported modules -fn parseCheckAndEvalModuleWithImport(src: []const u8, import_name: []const u8, imported_module: *const ModuleEnv) !struct { +const EvalModuleWithImportResult = struct { module_env: *ModuleEnv, evaluator: ComptimeEvaluator, problems: *check.problem.Store, other_envs: []const *const ModuleEnv, builtin_module: builtin_loading.LoadedModule, -} { +}; + +/// Helper to parse, canonicalize, type-check, and run comptime evaluation with imported modules +fn parseCheckAndEvalModuleWithImport(src: []const u8, import_name: []const u8, imported_module: *const ModuleEnv) !EvalModuleWithImportResult { const gpa = test_allocator; const module_env = try gpa.create(ModuleEnv); @@ -138,6 +146,9 @@ fn parseCheckAndEvalModuleWithImport(src: []const u8, import_name: []const u8, i var module_envs = std.AutoHashMap(base.Ident.Idx, Can.AutoImportedType).init(gpa); defer module_envs.deinit(); + // Populate module_envs with builtin types (like production does) + try Can.populateModuleEnvs(&module_envs, module_env, builtin_module.env, builtin_indices); + // Convert import name to Ident.Idx using the MODULE's ident store (not a temporary one!) // This is important because the canonicalizer will look up identifiers in this same store const import_ident = try module_env.insertIdent(base.Ident.for_text(import_name)); @@ -382,7 +393,7 @@ test "comptime eval - cross-module crash is detected" { \\} ; - var result_a = try parseCheckAndEvalModule(src_a); + var result_a = try parseCheckAndEvalModuleWithName(src_a, "A"); defer cleanupEvalModule(&result_a); const summary_a = try result_a.evaluator.evalAll(); From 51e10aa05845a13519f41678443d2f6db943554f Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Sat, 8 Nov 2025 01:57:58 -0500 Subject: [PATCH 28/38] More canonicalization fixes --- src/build/builtin_compiler/main.zig | 24 ++--- src/canonicalize/Can.zig | 152 +++++++++++++++++++--------- 2 files changed, 113 insertions(+), 63 deletions(-) diff --git a/src/build/builtin_compiler/main.zig b/src/build/builtin_compiler/main.zig index d874098cac..fb2c1dd85d 100644 --- a/src/build/builtin_compiler/main.zig +++ b/src/build/builtin_compiler/main.zig @@ -675,19 +675,7 @@ fn compileModule( return error.ParseError; } - // 4. Create module imports map (for cross-module references) - var module_envs = std.AutoHashMap(base.Ident.Idx, Can.AutoImportedType).init(gpa); - defer module_envs.deinit(); - - // Add dependencies (e.g., Dict for Set, Bool for Str) - // IMPORTANT: Use the module's own ident store, not a temporary one, - // because auto-import lookups will use the module's ident store - for (deps) |dep| { - const dep_ident = try module_env.insertIdent(base.Ident.for_text(dep.name)); - try module_envs.put(dep_ident, .{ .env = dep.env }); - } - - // 5. Canonicalize + // 4. Canonicalize try module_env.initCIRFields(gpa, module_name); var can_result = try gpa.create(Can); @@ -696,7 +684,8 @@ fn compileModule( gpa.destroy(can_result); } - can_result.* = try Can.init(module_env, parse_ast, &module_envs); + // When compiling Builtin itself, pass null for module_envs so setupAutoImportedBuiltinTypes doesn't run + can_result.* = try Can.init(module_env, parse_ast, null); try can_result.canonicalizeFile(); try can_result.validateForChecking(); @@ -712,6 +701,10 @@ fn compileModule( const type_name = module_env.getIdentText(d.name); std.debug.print(" - Undeclared type: {s}\n", .{type_name}); }, + .ident_not_in_scope => |d| { + const ident_name = module_env.getIdentText(d.ident); + std.debug.print(" - Ident not in scope: {s}\n", .{ident_name}); + }, .nested_value_not_found => |d| { const parent = module_env.getIdentText(d.parent_name); const nested = module_env.getIdentText(d.nested_name); @@ -789,6 +782,9 @@ fn compileModule( try imported_envs.append(gpa, dep.env); } + var module_envs = std.AutoHashMap(base.Ident.Idx, Can.AutoImportedType).init(gpa); + defer module_envs.deinit(); + var checker = try Check.init( gpa, &module_env.types, diff --git a/src/canonicalize/Can.zig b/src/canonicalize/Can.zig index e52cb05e95..781018afbc 100644 --- a/src/canonicalize/Can.zig +++ b/src/canonicalize/Can.zig @@ -283,21 +283,16 @@ pub fn populateModuleEnvs( } /// Set up auto-imported builtin types (Bool, Result, Dict, Set, Str, and numeric types) from the Builtin module. -/// This function is shared between production and test environments to ensure consistency. -/// -/// These nested types in Builtin.roc need special handling: -/// 1. Add them to scope's type_bindings so type annotations work +/// Used for all modules EXCEPT Builtin itself. pub fn setupAutoImportedBuiltinTypes( self: *Self, env: *ModuleEnv, gpa: std.mem.Allocator, module_envs: ?*const std.AutoHashMap(Ident.Idx, AutoImportedType), ) std.mem.Allocator.Error!void { - // Auto-import builtin types (Bool, Result, Dict, Set, Str, and numeric types) - // These are nested types in Builtin module but need to be auto-imported like standalone modules if (module_envs) |envs_map| { const zero_region = Region{ .start = Region.Position.zero(), .end = Region.Position.zero() }; - const current_scope = &self.scopes.items[0]; // Top-level scope + const current_scope = &self.scopes.items[0]; const builtin_import_idx = try self.env.imports.getOrPut( gpa, @@ -310,16 +305,14 @@ pub fn setupAutoImportedBuiltinTypes( for (builtin_types) |type_name_text| { const type_ident = try env.insertIdent(base.Ident.for_text(type_name_text)); if (envs_map.get(type_ident)) |type_entry| { - // Get target_node_idx from statement_idx const target_node_idx = if (type_entry.statement_idx) |stmt_idx| type_entry.env.getExposedNodeIndexByStatementIdx(stmt_idx) else null; - // Add type binding to scope try current_scope.type_bindings.put(gpa, type_ident, Scope.TypeBinding{ .external_nominal = .{ - .module_ident = type_ident, // Use type name as module ident for module_envs lookup + .module_ident = type_ident, .original_ident = type_ident, .target_node_idx = target_node_idx, .import_idx = builtin_import_idx, @@ -330,22 +323,16 @@ pub fn setupAutoImportedBuiltinTypes( } } - // Also add primitive builtin types (List, Box) to type_bindings - // so we can detect conflicts with O(1) HashMap lookup instead of string scanning - // Note: Str is NOT primitive anymore - it's auto-imported like Bool - const primitive_builtins = [_][]const u8{ "List", "Box" }; for (primitive_builtins) |type_name_text| { const type_ident = try env.insertIdent(base.Ident.for_text(type_name_text)); - // Add a minimal type binding to detect conflicts - // These primitives don't have module entries, so we use a marker binding try current_scope.type_bindings.put(gpa, type_ident, Scope.TypeBinding{ .external_nominal = .{ .module_ident = type_ident, .original_ident = type_ident, .target_node_idx = null, - .import_idx = @enumFromInt(0), // Dummy import index for primitives + .import_idx = @enumFromInt(0), .origin_region = zero_region, .module_not_found = false, }, @@ -380,6 +367,7 @@ fn processTypeDeclFirstPass( self: *Self, type_decl: anytype, parent_name: ?Ident.Idx, + defer_associated_blocks: bool, ) std.mem.Allocator.Error!void { // Canonicalize the type declaration header first const header_idx = try self.canonicalizeTypeHeader(type_decl.header); @@ -434,6 +422,14 @@ fn processTypeDeclFirstPass( // Introduce the type name into scope early to support recursive references try self.introduceType(qualified_name_idx, type_decl_stmt_idx, region); + // For nested types, also add an unqualified alias so child scopes can find it + // E.g., when introducing "Builtin.Bool", also add "Bool" -> "Builtin.Bool" + // This allows nested scopes (like Str's or Num.U8's associated blocks) to find Bool via scope lookup + if (parent_name != null) { + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + try current_scope.introduceTypeAlias(self.env.gpa, type_header.name, type_decl_stmt_idx); + } + // Process type parameters and annotation in a separate scope const anno_idx = blk: { // Enter a new scope for type parameters @@ -495,7 +491,22 @@ fn processTypeDeclFirstPass( // Process associated items completely (both symbol introduction and canonicalization) // This eliminates the need for a separate third pass - if (type_decl.associated) |assoc| { + // Unless defer_associated_blocks is true (when called from processAssociatedItemsFirstPass + // to handle sibling type forward references) + if (!defer_associated_blocks) { + if (type_decl.associated) |assoc| { + try self.processAssociatedBlock(qualified_name_idx, type_header.name, assoc); + } + } +} + +/// Process an associated block: introduce all items, set up scope with aliases, and canonicalize +fn processAssociatedBlock( + self: *Self, + qualified_name_idx: Ident.Idx, + type_name: Ident.Idx, + assoc: anytype, +) std.mem.Allocator.Error!void { // First, introduce placeholder patterns for all associated items try self.processAssociatedItemsFirstPass(qualified_name_idx, assoc.statements); @@ -503,17 +514,21 @@ fn processTypeDeclFirstPass( try self.scopeEnter(self.env.gpa, false); // false = not a function boundary defer self.scopeExit(self.env.gpa) catch unreachable; - // First, introduce the parent type itself into this scope so it can be referenced by its unqualified name + // Introduce the parent type itself into this scope so it can be referenced by its unqualified name // For example, if we're processing MyBool's associated items, we need "MyBool" to resolve to "Test.MyBool" if (self.scopeLookupTypeDecl(qualified_name_idx)) |parent_type_decl_idx| { const current_scope = &self.scopes.items[self.scopes.items.len - 1]; - try current_scope.introduceTypeAlias(self.env.gpa, type_header.name, parent_type_decl_idx); + try current_scope.introduceTypeAlias(self.env.gpa, type_name, parent_type_decl_idx); } + // Note: Sibling types and ancestor types are accessible via parent scope lookup. + // When nested types were introduced in processTypeDeclFirstPass, unqualified aliases + // were added in their declaration scope, making them visible to all child scopes. + // Introduce aliases into this scope so associated items can reference each other // We only add unqualified and type-qualified names; fully qualified names are // already in the parent scope and accessible via scope nesting - const parent_type_text = self.env.getIdent(type_header.name); + const parent_type_text = self.env.getIdent(type_name); for (self.parse_ir.store.statementSlice(assoc.statements)) |assoc_stmt_idx| { const assoc_stmt = self.parse_ir.store.getStatement(assoc_stmt_idx); switch (assoc_stmt) { @@ -603,7 +618,7 @@ fn processTypeDeclFirstPass( } // Process the associated items (canonicalize their bodies) - try self.processAssociatedItemsSecondPass(qualified_name_idx, type_header.name, assoc.statements); + try self.processAssociatedItemsSecondPass(qualified_name_idx, type_name, assoc.statements); // After processing, introduce anno-only defs into the associated block scope // (They were just created by processAssociatedItemsSecondPass) @@ -640,7 +655,6 @@ fn processTypeDeclFirstPass( else => {}, } } - } } /// Canonicalize an associated item declaration with a qualified name @@ -937,15 +951,27 @@ fn processAssociatedItemsFirstPass( parent_name: Ident.Idx, statements: AST.Statement.Span, ) std.mem.Allocator.Error!void { + // Two-phase approach for sibling types: + // Phase 1: Introduce all type declarations (defer their associated blocks) + // Phase 2: Process all deferred associated blocks (now all siblings are in scope) + + // Phase 1: Introduce type declarations without processing their associated blocks + for (self.parse_ir.store.statementSlice(statements)) |stmt_idx| { + const stmt = self.parse_ir.store.getStatement(stmt_idx); + if (stmt == .type_decl) { + try self.processTypeDeclFirstPass(stmt.type_decl, parent_name, true); // defer associated blocks + } + } + + // Phase 2a: Introduce all value declarations and type annotations first (before processing associated blocks) + // This ensures that sibling items (like list_get_unsafe) are available + // when processing associated blocks (like List's) for (self.parse_ir.store.statementSlice(statements)) |stmt_idx| { const stmt = self.parse_ir.store.getStatement(stmt_idx); switch (stmt) { - .type_decl => |type_decl| { - // Recursively process nested type declarations (this introduces the qualified name) - try self.processTypeDeclFirstPass(type_decl, parent_name); - }, .decl => |decl| { - // Introduce declarations with qualified names for recursive references + // Create placeholder for declarations so they can be referenced by sibling types + // processAssociatedItemsSecondPass will later use updatePlaceholder to replace these const pattern = self.parse_ir.store.getPattern(decl.pattern); if (pattern == .ident) { const pattern_ident_tok = pattern.ident.ident_tok; @@ -964,30 +990,58 @@ fn processAssociatedItemsFirstPass( }; const placeholder_pattern_idx = try self.env.addPattern(placeholder_pattern, region); - // Introduce the qualified name to scope - switch (try self.scopeIntroduceInternal(self.env.gpa, .ident, qualified_idx, placeholder_pattern_idx, false, true)) { - .success => {}, - .shadowing_warning => |shadowed_pattern_idx| { - const original_region = self.env.store.getPatternRegion(shadowed_pattern_idx); - try self.env.pushDiagnostic(Diagnostic{ .shadowing_warning = .{ - .ident = qualified_idx, - .region = region, - .original_region = original_region, - } }); - }, - .top_level_var_error => { - // This shouldn't happen for declarations in associated blocks - }, - .var_across_function_boundary => { - // This shouldn't happen for declarations in associated blocks - }, - } + // Directly put placeholder in scope (no conflict checking) + // updatePlaceholder will verify it's replacing a placeholder later + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + try current_scope.idents.put(self.env.gpa, qualified_idx, placeholder_pattern_idx); + try current_scope.idents.put(self.env.gpa, decl_ident, placeholder_pattern_idx); } } }, - else => { - // Skip other statement types in first pass + .type_anno => |type_anno| { + // Create placeholder for anno-only defs so they can be referenced by sibling types + // processAssociatedItemsSecondPass will later use updatePlaceholder to replace these + if (self.parse_ir.tokens.resolveIdentifier(type_anno.name)) |anno_ident| { + const parent_text = self.env.getIdent(parent_name); + const anno_text = self.env.getIdent(anno_ident); + const qualified_idx = try self.env.insertQualifiedIdent(parent_text, anno_text); + + const region = self.parse_ir.tokenizedRegionToRegion(type_anno.region); + const placeholder_pattern = Pattern{ + .assign = .{ + .ident = qualified_idx, + }, + }; + const placeholder_pattern_idx = try self.env.addPattern(placeholder_pattern, region); + + // Directly put placeholder in scope (no conflict checking) + // updatePlaceholder will verify it's replacing a placeholder later + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + try current_scope.idents.put(self.env.gpa, qualified_idx, placeholder_pattern_idx); + try current_scope.idents.put(self.env.gpa, anno_ident, placeholder_pattern_idx); + } }, + else => { + // Skip other statement types + }, + } + } + + // Phase 2b: Now process all deferred associated blocks + // All sibling types and declarations are now in scope + for (self.parse_ir.store.statementSlice(statements)) |stmt_idx| { + const stmt = self.parse_ir.store.getStatement(stmt_idx); + if (stmt == .type_decl) { + const type_decl = stmt.type_decl; + if (type_decl.associated) |assoc| { + const type_header = self.parse_ir.store.getTypeHeader(type_decl.header) catch continue; + const type_ident = self.parse_ir.tokens.resolveIdentifier(type_header.name) orelse continue; + const parent_text = self.env.getIdent(parent_name); + const type_text = self.env.getIdent(type_ident); + const qualified_idx = try self.env.insertQualifiedIdent(parent_text, type_text); + + try self.processAssociatedBlock(qualified_idx, type_ident, assoc); + } } } } @@ -1073,7 +1127,7 @@ pub fn canonicalizeFile( const stmt = self.parse_ir.store.getStatement(stmt_id); switch (stmt) { .type_decl => |type_decl| { - try self.processTypeDeclFirstPass(type_decl, null); + try self.processTypeDeclFirstPass(type_decl, null, false); }, else => { // Skip non-type-declaration statements in first pass From bab81abc9519dce76cf745f1e6c5c2b89c127174 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Sat, 8 Nov 2025 07:50:57 -0500 Subject: [PATCH 29/38] Fix shadowing detection regression --- src/canonicalize/Can.zig | 376 +++++++++++++++++++++++++-------------- 1 file changed, 242 insertions(+), 134 deletions(-) diff --git a/src/canonicalize/Can.zig b/src/canonicalize/Can.zig index 781018afbc..7fe98baaf4 100644 --- a/src/canonicalize/Can.zig +++ b/src/canonicalize/Can.zig @@ -44,6 +44,8 @@ exposed_scope: Scope = undefined, exposed_ident_texts: std.StringHashMapUnmanaged(Region) = .{}, /// Track exposed types by text to handle changing indices exposed_type_texts: std.StringHashMapUnmanaged(Region) = .{}, +/// Track which identifiers in the current scope are placeholders (not yet replaced with real definitions) +placeholder_idents: std.AutoHashMapUnmanaged(Ident.Idx, void) = .{}, /// Stack of function regions for tracking var reassignment across function boundaries function_regions: std.array_list.Managed(Region), /// Maps var patterns to the function region they were declared in @@ -166,6 +168,7 @@ pub fn deinit( self.exposed_scope.deinit(gpa); self.exposed_ident_texts.deinit(gpa); self.exposed_type_texts.deinit(gpa); + self.placeholder_idents.deinit(gpa); for (0..self.scopes.items.len) |i| { var scope = &self.scopes.items[i]; @@ -742,30 +745,16 @@ fn processAssociatedItemsSecondPass( parent_name: Ident.Idx, parent_type_name: Ident.Idx, statements: AST.Statement.Span, -) std.mem.Allocator.Error!void { +) std.mem.Allocator.Error!void { const stmt_idxs = self.parse_ir.store.statementSlice(statements); var i: usize = 0; while (i < stmt_idxs.len) : (i += 1) { const stmt_idx = stmt_idxs[i]; const stmt = self.parse_ir.store.getStatement(stmt_idx); switch (stmt) { - .type_decl => |type_decl| { - // Recursively process nested type declarations - if (type_decl.associated) |assoc| { - const type_header = self.parse_ir.store.getTypeHeader(type_decl.header) catch continue; - const type_ident = self.parse_ir.tokens.resolveIdentifier(type_header.name) orelse continue; - - // Build qualified name for nested type - const parent_text = self.env.getIdent(parent_name); - const type_text = self.env.getIdent(type_ident); - const qualified_idx = try self.env.insertQualifiedIdent(parent_text, type_text); - - // Enter a new scope for the nested associated block - try self.scopeEnter(self.env.gpa, false); - defer self.scopeExit(self.env.gpa) catch unreachable; - - try self.processAssociatedItemsSecondPass(qualified_idx, type_ident, assoc.statements); - } + .type_decl => { + // Skip nested type declarations - they're already processed by processAssociatedItemsFirstPass Phase 2 + // which calls processAssociatedBlock for each nested type }, .type_anno => |ta| { const name_ident = self.parse_ir.tokens.resolveIdentifier(ta.name) orelse { @@ -802,96 +791,97 @@ fn processAssociatedItemsSecondPass( // Now, check the next stmt to see if it matches this anno const next_i = i + 1; - if (next_i < stmt_idxs.len) { + const has_matching_decl = if (next_i < stmt_idxs.len) blk: { const next_stmt_id = stmt_idxs[next_i]; const next_stmt = self.parse_ir.store.getStatement(next_stmt_id); - switch (next_stmt) { - .decl => |decl| { - // Check if the declaration pattern matches the annotation name - const pattern = self.parse_ir.store.getPattern(decl.pattern); - if (pattern == .ident) { - const pattern_ident_tok = pattern.ident.ident_tok; - if (self.parse_ir.tokens.resolveIdentifier(pattern_ident_tok)) |decl_ident| { - // Check if names match - if (name_ident.idx == decl_ident.idx) { - // Skip the next statement since we're processing it now - i = next_i; + if (next_stmt == .decl) { + const decl = next_stmt.decl; + // Check if the declaration pattern matches the annotation name + const pattern = self.parse_ir.store.getPattern(decl.pattern); + if (pattern == .ident) { + const pattern_ident_tok = pattern.ident.ident_tok; + if (self.parse_ir.tokens.resolveIdentifier(pattern_ident_tok)) |decl_ident| { + // Check if names match + if (name_ident.idx == decl_ident.idx) { + // Skip the next statement since we're processing it now + i = next_i; - // Build qualified name (e.g., "Foo.bar") - const parent_text = self.env.getIdent(parent_name); - const decl_text = self.env.getIdent(decl_ident); - const qualified_idx = try self.env.insertQualifiedIdent(parent_text, decl_text); + // Build qualified name (e.g., "Foo.bar") + const parent_text = self.env.getIdent(parent_name); + const decl_text = self.env.getIdent(decl_ident); + const qualified_idx = try self.env.insertQualifiedIdent(parent_text, decl_text); - // Canonicalize with the qualified name and type annotation - const def_idx = try self.canonicalizeAssociatedDeclWithAnno( - decl, - qualified_idx, - type_anno_idx, - where_clauses, - ); - try self.env.store.addScratchDef(def_idx); + // Canonicalize with the qualified name and type annotation + const def_idx = try self.canonicalizeAssociatedDeclWithAnno( + decl, + qualified_idx, + type_anno_idx, + where_clauses, + ); + try self.env.store.addScratchDef(def_idx); - // Register this associated item by its qualified name - const def_idx_u16: u16 = @intCast(@intFromEnum(def_idx)); - try self.env.setExposedNodeIndexById(qualified_idx, def_idx_u16); + // Register this associated item by its qualified name + const def_idx_u16: u16 = @intCast(@intFromEnum(def_idx)); + try self.env.setExposedNodeIndexById(qualified_idx, def_idx_u16); - // Make the real pattern available in current scope (replaces placeholder) - // We already added unqualified and type-qualified names earlier, - // but need to update them to point to the real pattern instead of placeholder. - const def_cir = self.env.store.getDef(def_idx); - const pattern_idx = def_cir.pattern; - const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + // Make the real pattern available in current scope (replaces placeholder) + // We already added unqualified and type-qualified names earlier, + // but need to update them to point to the real pattern instead of placeholder. + const def_cir = self.env.store.getDef(def_idx); + const pattern_idx = def_cir.pattern; + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; - // Update unqualified name (e.g., "my_not") - try self.updatePlaceholder(current_scope, decl_ident, pattern_idx); + // Update unqualified name (e.g., "my_not") + try self.updatePlaceholder(current_scope, decl_ident, pattern_idx); - // Update type-qualified name (e.g., "MyBool.my_not") - const type_qualified_idx = try self.env.insertQualifiedIdent(self.env.getIdent(parent_type_name), decl_text); - try self.updatePlaceholder(current_scope, type_qualified_idx, pattern_idx); + // Update type-qualified name (e.g., "MyBool.my_not") + const type_qualified_idx = try self.env.insertQualifiedIdent(self.env.getIdent(parent_type_name), decl_text); + try self.updatePlaceholder(current_scope, type_qualified_idx, pattern_idx); - // Update fully qualified name (e.g., "Test.MyBool.my_not") - try self.updatePlaceholder(current_scope, qualified_idx, pattern_idx); - } + // Update fully qualified name (e.g., "Test.MyBool.my_not") + try self.updatePlaceholder(current_scope, qualified_idx, pattern_idx); + + break :blk true; // Found and processed matching decl } } - }, - else => { - // If the next stmt does not match this annotation, - // create an anno-only def for the associated item - - const region = self.parse_ir.tokenizedRegionToRegion(ta.region); - - // Build qualified name for the annotation (e.g., "Str.isEmpty") - const parent_text = self.env.getIdent(parent_name); - const name_text = self.env.getIdent(name_ident); - const qualified_idx = try self.env.insertQualifiedIdent(parent_text, name_text); - - // Create anno-only def with the qualified name - const def_idx = try self.createAnnoOnlyDef(qualified_idx, type_anno_idx, where_clauses, region); - - // Register this associated item by its qualified name - const def_idx_u16: u16 = @intCast(@intFromEnum(def_idx)); - try self.env.setExposedNodeIndexById(qualified_idx, def_idx_u16); - - // Make the real pattern available in current scope (replaces placeholder) - const def_cir = self.env.store.getDef(def_idx); - const pattern_idx = def_cir.pattern; - const current_scope = &self.scopes.items[self.scopes.items.len - 1]; - - // Update unqualified name (e.g., "is_empty") - try self.updatePlaceholder(current_scope, name_ident, pattern_idx); - - // Update type-qualified name (e.g., "List.is_empty") - const type_qualified_idx = try self.env.insertQualifiedIdent(self.env.getIdent(parent_type_name), name_text); - try self.updatePlaceholder(current_scope, type_qualified_idx, pattern_idx); - - // Update fully qualified name (e.g., "Builtin.List.is_empty") - try self.updatePlaceholder(current_scope, qualified_idx, pattern_idx); - - try self.env.store.addScratchDef(def_idx); - }, + } } + break :blk false; // No matching decl found + } else false; // No next statement + + // If there's no matching decl, create an anno-only def + if (!has_matching_decl) { + const region = self.parse_ir.tokenizedRegionToRegion(ta.region); + + // Build qualified name for the annotation (e.g., "Str.isEmpty") + const parent_text = self.env.getIdent(parent_name); + const name_text = self.env.getIdent(name_ident); + const qualified_idx = try self.env.insertQualifiedIdent(parent_text, name_text); + + // Create anno-only def with the qualified name + const def_idx = try self.createAnnoOnlyDef(qualified_idx, type_anno_idx, where_clauses, region); + + // Register this associated item by its qualified name + const def_idx_u16: u16 = @intCast(@intFromEnum(def_idx)); + try self.env.setExposedNodeIndexById(qualified_idx, def_idx_u16); + + // Make the real pattern available in current scope (replaces placeholder) + const def_cir = self.env.store.getDef(def_idx); + const pattern_idx = def_cir.pattern; + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + + // Update unqualified name (e.g., "is_empty") + try self.updatePlaceholder(current_scope, name_ident, pattern_idx); + + // Update type-qualified name (e.g., "List.is_empty") + const type_qualified_idx = try self.env.insertQualifiedIdent(self.env.getIdent(parent_type_name), name_text); + try self.updatePlaceholder(current_scope, type_qualified_idx, pattern_idx); + + // Update fully qualified name (e.g., "Builtin.List.is_empty") + try self.updatePlaceholder(current_scope, qualified_idx, pattern_idx); + + try self.env.store.addScratchDef(def_idx); } }, .decl => |decl| { @@ -1006,6 +996,7 @@ fn processAssociatedItemsFirstPass( const anno_text = self.env.getIdent(anno_ident); const qualified_idx = try self.env.insertQualifiedIdent(parent_text, anno_text); + const region = self.parse_ir.tokenizedRegionToRegion(type_anno.region); const placeholder_pattern = Pattern{ .assign = .{ @@ -1123,11 +1114,12 @@ pub fn canonicalizeFile( const scratch_statements_start = self.env.store.scratch.?.statements.top(); // First pass: Process all type declarations to introduce them into scope + // Defer associated blocks until after we've created placeholders for top-level items for (self.parse_ir.store.statementSlice(file.statements)) |stmt_id| { const stmt = self.parse_ir.store.getStatement(stmt_id); switch (stmt) { .type_decl => |type_decl| { - try self.processTypeDeclFirstPass(type_decl, null, false); + try self.processTypeDeclFirstPass(type_decl, null, true); // defer associated blocks }, else => { // Skip non-type-declaration statements in first pass @@ -1135,8 +1127,59 @@ pub fn canonicalizeFile( } } - // For type modules, expose the main type and all associated items before the second pass - // This ensures unused variable checking in the third pass doesn't flag exposed items + // Phase 1.5: Create placeholders for top-level declarations and type annotations + // This ensures sibling items (like list_get_unsafe) are available during associated block processing + for (self.parse_ir.store.statementSlice(file.statements)) |stmt_id| { + const stmt = self.parse_ir.store.getStatement(stmt_id); + switch (stmt) { + .decl => |decl| { + const pattern = self.parse_ir.store.getPattern(decl.pattern); + if (pattern == .ident) { + const pattern_ident_tok = pattern.ident.ident_tok; + if (self.parse_ir.tokens.resolveIdentifier(pattern_ident_tok)) |decl_ident| { + const region = self.parse_ir.tokenizedRegionToRegion(decl.region); + const placeholder_pattern = Pattern{ + .assign = .{ + .ident = decl_ident, + }, + }; + const placeholder_pattern_idx = try self.env.addPattern(placeholder_pattern, region); + + + // Directly put placeholder in current scope + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + try current_scope.idents.put(self.env.gpa, decl_ident, placeholder_pattern_idx); + // Track that this is a placeholder + try self.placeholder_idents.put(self.env.gpa, decl_ident, {}); + } + } + }, + .type_anno => |type_anno| { + if (self.parse_ir.tokens.resolveIdentifier(type_anno.name)) |anno_ident| { + const region = self.parse_ir.tokenizedRegionToRegion(type_anno.region); + const placeholder_pattern = Pattern{ + .assign = .{ + .ident = anno_ident, + }, + }; + const placeholder_pattern_idx = try self.env.addPattern(placeholder_pattern, region); + + + // Directly put placeholder in current scope + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + try current_scope.idents.put(self.env.gpa, anno_ident, placeholder_pattern_idx); + // Track that this is a placeholder + try self.placeholder_idents.put(self.env.gpa, anno_ident, {}); + } + }, + else => { + // Skip other statement types + }, + } + } + + // For type modules, expose the main type and all associated items BEFORE processing associated blocks + // This ensures unused variable checking (which happens during scope exit) doesn't flag exposed items if (self.env.module_kind == .type_module) { const module_name_text = self.env.module_name; for (self.parse_ir.store.statementSlice(file.statements)) |stmt_id| { @@ -1158,6 +1201,23 @@ pub fn canonicalizeFile( } } + // Phase 1.6: Now process all deferred type declaration associated blocks + // All top-level placeholders are now in scope, and for type modules, all items are marked as exposed + var assoc_blocks_found: usize = 0; + for (self.parse_ir.store.statementSlice(file.statements)) |stmt_id| { + const stmt = self.parse_ir.store.getStatement(stmt_id); + if (stmt == .type_decl) { + const type_decl = stmt.type_decl; + if (type_decl.associated) |assoc| { + const type_header = self.parse_ir.store.getTypeHeader(type_decl.header) catch continue; + const type_ident = self.parse_ir.tokens.resolveIdentifier(type_header.name) orelse continue; + assoc_blocks_found += 1; + + try self.processAssociatedBlock(type_ident, type_ident, assoc); + } + } + } + // Second pass: Process all other statements const ast_stmt_idxs = self.parse_ir.store.statementSlice(file.statements); var i: usize = 0; @@ -1480,18 +1540,40 @@ fn createAnnoOnlyDef( }; const pattern_idx = try self.env.addPattern(pattern, region); - // Introduce the name to scope - switch (try self.scopeIntroduceInternal(self.env.gpa, .ident, ident, pattern_idx, false, true)) { - .success => {}, - .shadowing_warning => |shadowed_pattern_idx| { - const original_region = self.env.store.getPatternRegion(shadowed_pattern_idx); - try self.env.pushDiagnostic(Diagnostic{ .shadowing_warning = .{ - .ident = ident, - .region = region, - .original_region = original_region, - } }); - }, - else => {}, + // Check if a placeholder already exists (from Phase 1.5 for top-level items or Phase 2a for associated items) + // Use scopeContains to check all scopes, not just the current one + const existing_pattern = self.scopeContains(.ident, ident); + const placeholder_exists = if (existing_pattern) |existing_pat_idx| blk: { + // Check if it's a placeholder (assign pattern) + const existing_pat = self.env.store.getPattern(existing_pat_idx); + break :blk existing_pat == .assign; + } else false; + + if (placeholder_exists) { + // Find which scope has the placeholder and replace it + var scope_idx = self.scopes.items.len; + while (scope_idx > 0) { + scope_idx -= 1; + const scope = &self.scopes.items[scope_idx]; + if (scope.idents.get(ident)) |_| { + try self.updatePlaceholder(scope, ident, pattern_idx); + break; + } + } + } else { + // Introduce the name to scope normally + switch (try self.scopeIntroduceInternal(self.env.gpa, .ident, ident, pattern_idx, false, true)) { + .success => {}, + .shadowing_warning => |shadowed_pattern_idx| { + const original_region = self.env.store.getPatternRegion(shadowed_pattern_idx); + try self.env.pushDiagnostic(Diagnostic{ .shadowing_warning = .{ + .ident = ident, + .region = region, + .original_region = original_region, + } }); + }, + else => {}, + } } // Create the e_anno_only expression @@ -4572,31 +4654,41 @@ fn canonicalizePattern( .ident = ident_idx, } }, region); - // Introduce the identifier into scope mapping to this pattern node - switch (try self.scopeIntroduceInternal(self.env.gpa, .ident, ident_idx, pattern_idx, false, true)) { - .success => {}, - .shadowing_warning => |shadowed_pattern_idx| { - const original_region = self.env.store.getPatternRegion(shadowed_pattern_idx); - try self.env.pushDiagnostic(Diagnostic{ .shadowing_warning = .{ - .ident = ident_idx, - .region = region, - .original_region = original_region, - } }); - }, - .top_level_var_error => { - return try self.env.pushMalformed(Pattern.Idx, Diagnostic{ - .invalid_top_level_statement = .{ - .stmt = try self.env.insertString("var"), + // Check if a placeholder exists for this identifier + // Placeholders are tracked in placeholder_idents and exist only until replaced + const placeholder_exists = self.placeholder_idents.contains(ident_idx); + + if (placeholder_exists) { + // Replace the placeholder in the current scope + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + try self.updatePlaceholder(current_scope, ident_idx, pattern_idx); + } else { + // Introduce the identifier into scope mapping to this pattern node + switch (try self.scopeIntroduceInternal(self.env.gpa, .ident, ident_idx, pattern_idx, false, true)) { + .success => {}, + .shadowing_warning => |shadowed_pattern_idx| { + const original_region = self.env.store.getPatternRegion(shadowed_pattern_idx); + try self.env.pushDiagnostic(Diagnostic{ .shadowing_warning = .{ + .ident = ident_idx, .region = region, - }, - }); - }, - .var_across_function_boundary => { - return try self.env.pushMalformed(Pattern.Idx, Diagnostic{ .ident_already_in_scope = .{ - .ident = ident_idx, - .region = region, - } }); - }, + .original_region = original_region, + } }); + }, + .top_level_var_error => { + return try self.env.pushMalformed(Pattern.Idx, Diagnostic{ + .invalid_top_level_statement = .{ + .stmt = try self.env.insertString("var"), + .region = region, + }, + }); + }, + .var_across_function_boundary => { + return try self.env.pushMalformed(Pattern.Idx, Diagnostic{ .ident_already_in_scope = .{ + .ident = ident_idx, + .region = region, + } }); + }, + } } return pattern_idx; @@ -8073,6 +8165,8 @@ fn updatePlaceholder( } } try scope.idents.put(self.env.gpa, ident_idx, pattern_idx); + // Remove from placeholder set since it's now a real definition + _ = self.placeholder_idents.remove(ident_idx); } fn scopeUpdateTypeDecl( @@ -8972,6 +9066,20 @@ fn exposeAssociatedItems(self: *Self, parent_name: Ident.Idx, type_decl: anytype } } }, + .type_anno => |type_anno| { + // Get the annotation name (for annotation-only definitions like compiler intrinsics) + if (self.parse_ir.tokens.resolveIdentifier(type_anno.name)) |anno_ident| { + // Build qualified name (e.g., "Bool.is_ne") + const parent_text = self.env.getIdent(parent_name); + const anno_text = self.env.getIdent(anno_ident); + const qualified_idx = try self.env.insertQualifiedIdent(parent_text, anno_text); + + // Expose both the qualified and unqualified names + // (both are added to scope in Phase 2a) + try self.env.addExposedById(qualified_idx); + try self.env.addExposedById(anno_ident); + } + }, else => {}, } } From 6efc03e56bb3f9e0158a0f6439b7d9bbb42d399a Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Sat, 8 Nov 2025 11:21:25 -0500 Subject: [PATCH 30/38] More fixes --- src/canonicalize/Can.zig | 400 +++++++++--------- .../nominal/nominal_associated_decls.md | 5 - .../nominal_associated_deep_nesting.md | 15 - .../nominal_associated_lookup_nested.md | 5 - .../nominal/nominal_deeply_nested_types.md | 30 -- .../snapshots/nominal/nominal_nested_types.md | 5 - .../type_module_associated_items_exposed.md | 5 - 7 files changed, 204 insertions(+), 261 deletions(-) diff --git a/src/canonicalize/Can.zig b/src/canonicalize/Can.zig index 7fe98baaf4..6f165f2222 100644 --- a/src/canonicalize/Can.zig +++ b/src/canonicalize/Can.zig @@ -45,6 +45,7 @@ exposed_ident_texts: std.StringHashMapUnmanaged(Region) = .{}, /// Track exposed types by text to handle changing indices exposed_type_texts: std.StringHashMapUnmanaged(Region) = .{}, /// Track which identifiers in the current scope are placeholders (not yet replaced with real definitions) +/// This is empty for 99% of files; only used during multi-phase canonicalization (mainly Builtin.roc) placeholder_idents: std.AutoHashMapUnmanaged(Ident.Idx, void) = .{}, /// Stack of function regions for tracking var reassignment across function boundaries function_regions: std.array_list.Managed(Region), @@ -510,154 +511,172 @@ fn processAssociatedBlock( type_name: Ident.Idx, assoc: anytype, ) std.mem.Allocator.Error!void { - // First, introduce placeholder patterns for all associated items - try self.processAssociatedItemsFirstPass(qualified_name_idx, assoc.statements); + // First, introduce placeholder patterns for all associated items + try self.processAssociatedItemsFirstPass(qualified_name_idx, assoc.statements); - // Now enter a new scope for the associated block where both qualified and unqualified names work - try self.scopeEnter(self.env.gpa, false); // false = not a function boundary - defer self.scopeExit(self.env.gpa) catch unreachable; + // Now enter a new scope for the associated block where both qualified and unqualified names work + try self.scopeEnter(self.env.gpa, false); // false = not a function boundary + defer self.scopeExit(self.env.gpa) catch unreachable; - // Introduce the parent type itself into this scope so it can be referenced by its unqualified name - // For example, if we're processing MyBool's associated items, we need "MyBool" to resolve to "Test.MyBool" - if (self.scopeLookupTypeDecl(qualified_name_idx)) |parent_type_decl_idx| { - const current_scope = &self.scopes.items[self.scopes.items.len - 1]; - try current_scope.introduceTypeAlias(self.env.gpa, type_name, parent_type_decl_idx); - } + // Introduce the parent type itself into this scope so it can be referenced by its unqualified name + // For example, if we're processing MyBool's associated items, we need "MyBool" to resolve to "Test.MyBool" + if (self.scopeLookupTypeDecl(qualified_name_idx)) |parent_type_decl_idx| { + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + try current_scope.introduceTypeAlias(self.env.gpa, type_name, parent_type_decl_idx); + } - // Note: Sibling types and ancestor types are accessible via parent scope lookup. - // When nested types were introduced in processTypeDeclFirstPass, unqualified aliases - // were added in their declaration scope, making them visible to all child scopes. + // Note: Sibling types and ancestor types are accessible via parent scope lookup. + // When nested types were introduced in processTypeDeclFirstPass, unqualified aliases + // were added in their declaration scope, making them visible to all child scopes. - // Introduce aliases into this scope so associated items can reference each other - // We only add unqualified and type-qualified names; fully qualified names are - // already in the parent scope and accessible via scope nesting - const parent_type_text = self.env.getIdent(type_name); - for (self.parse_ir.store.statementSlice(assoc.statements)) |assoc_stmt_idx| { - const assoc_stmt = self.parse_ir.store.getStatement(assoc_stmt_idx); - switch (assoc_stmt) { - .type_decl => |nested_type_decl| { - const nested_header = self.parse_ir.store.getTypeHeader(nested_type_decl.header) catch continue; - const unqualified_ident = self.parse_ir.tokens.resolveIdentifier(nested_header.name) orelse continue; + // Introduce aliases into this scope so associated items can reference each other + // We only add unqualified and type-qualified names; fully qualified names are + // already in the parent scope and accessible via scope nesting + const parent_type_text = self.env.getIdent(type_name); + for (self.parse_ir.store.statementSlice(assoc.statements)) |assoc_stmt_idx| { + const assoc_stmt = self.parse_ir.store.getStatement(assoc_stmt_idx); + switch (assoc_stmt) { + .type_decl => |nested_type_decl| { + const nested_header = self.parse_ir.store.getTypeHeader(nested_type_decl.header) catch continue; + const unqualified_ident = self.parse_ir.tokens.resolveIdentifier(nested_header.name) orelse continue; - // Build fully qualified name (e.g., "Test.MyBool") - const parent_text = self.env.getIdent(qualified_name_idx); - const nested_type_text = self.env.getIdent(unqualified_ident); - const qualified_ident_idx = try self.env.insertQualifiedIdent(parent_text, nested_type_text); + // Build fully qualified name (e.g., "Test.MyBool") + const parent_text = self.env.getIdent(qualified_name_idx); + const nested_type_text = self.env.getIdent(unqualified_ident); + const qualified_ident_idx = try self.env.insertQualifiedIdent(parent_text, nested_type_text); - // Introduce unqualified type alias (fully qualified is already in parent scope) - if (self.scopeLookupTypeDecl(qualified_ident_idx)) |qualified_type_decl_idx| { - const current_scope = &self.scopes.items[self.scopes.items.len - 1]; - try current_scope.introduceTypeAlias(self.env.gpa, unqualified_ident, qualified_type_decl_idx); - } + // Introduce unqualified type alias (fully qualified is already in parent scope) + if (self.scopeLookupTypeDecl(qualified_ident_idx)) |qualified_type_decl_idx| { + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + try current_scope.introduceTypeAlias(self.env.gpa, unqualified_ident, qualified_type_decl_idx); + } - // Introduce associated items of nested types - if (nested_type_decl.associated) |nested_assoc| { - for (self.parse_ir.store.statementSlice(nested_assoc.statements)) |nested_assoc_stmt_idx| { - const nested_assoc_stmt = self.parse_ir.store.getStatement(nested_assoc_stmt_idx); - if (nested_assoc_stmt == .decl) { - const nested_decl = nested_assoc_stmt.decl; - const nested_pattern = self.parse_ir.store.getPattern(nested_decl.pattern); - if (nested_pattern == .ident) { - const nested_pattern_ident_tok = nested_pattern.ident.ident_tok; - if (self.parse_ir.tokens.resolveIdentifier(nested_pattern_ident_tok)) |nested_decl_ident| { - // Build fully qualified name (e.g., "Test.MyBool.my_not") - const qualified_text = self.env.getIdent(qualified_ident_idx); - const nested_decl_text = self.env.getIdent(nested_decl_ident); - const full_qualified_ident_idx = try self.env.insertQualifiedIdent(qualified_text, nested_decl_text); + // Introduce associated items of nested types + if (nested_type_decl.associated) |nested_assoc| { + for (self.parse_ir.store.statementSlice(nested_assoc.statements)) |nested_assoc_stmt_idx| { + const nested_assoc_stmt = self.parse_ir.store.getStatement(nested_assoc_stmt_idx); + if (nested_assoc_stmt == .decl) { + const nested_decl = nested_assoc_stmt.decl; + const nested_pattern = self.parse_ir.store.getPattern(nested_decl.pattern); + if (nested_pattern == .ident) { + const nested_pattern_ident_tok = nested_pattern.ident.ident_tok; + if (self.parse_ir.tokens.resolveIdentifier(nested_pattern_ident_tok)) |nested_decl_ident| { + // Build fully qualified name (e.g., "Test.MyBool.my_not") + const qualified_text = self.env.getIdent(qualified_ident_idx); + const nested_decl_text = self.env.getIdent(nested_decl_ident); + const full_qualified_ident_idx = try self.env.insertQualifiedIdent(qualified_text, nested_decl_text); - // Look up the fully qualified pattern (from parent scope via nesting) - switch (self.scopeLookup(.ident, full_qualified_ident_idx)) { - .found => |pattern_idx| { - const scope = &self.scopes.items[self.scopes.items.len - 1]; + // Look up the fully qualified pattern (from parent scope via nesting) + switch (self.scopeLookup(.ident, full_qualified_ident_idx)) { + .found => |pattern_idx| { + const scope = &self.scopes.items[self.scopes.items.len - 1]; - // Add unqualified name (e.g., "my_not") - try scope.idents.put(self.env.gpa, nested_decl_ident, pattern_idx); + // Check if this is a placeholder + const is_placeholder = self.isPlaceholder(full_qualified_ident_idx); - // Add type-qualified name (e.g., "MyBool.my_not") - const type_qualified_ident_idx = try self.env.insertQualifiedIdent(nested_type_text, nested_decl_text); - try scope.idents.put(self.env.gpa, type_qualified_ident_idx, pattern_idx); - }, - .not_found => {}, - } + // Add unqualified name (e.g., "my_not") + try scope.idents.put(self.env.gpa, nested_decl_ident, pattern_idx); + if (is_placeholder) { + try self.placeholder_idents.put(self.env.gpa, nested_decl_ident, {}); + } + + // Add type-qualified name (e.g., "MyBool.my_not") + const type_qualified_ident_idx = try self.env.insertQualifiedIdent(nested_type_text, nested_decl_text); + try scope.idents.put(self.env.gpa, type_qualified_ident_idx, pattern_idx); + if (is_placeholder) { + try self.placeholder_idents.put(self.env.gpa, type_qualified_ident_idx, {}); + } + }, + .not_found => {}, } } } } } - }, - .decl => |decl| { - const pattern = self.parse_ir.store.getPattern(decl.pattern); - if (pattern == .ident) { - const pattern_ident_tok = pattern.ident.ident_tok; - if (self.parse_ir.tokens.resolveIdentifier(pattern_ident_tok)) |decl_ident| { - // Build fully qualified name (e.g., "Test.MyBool.my_not") - const parent_text = self.env.getIdent(qualified_name_idx); - const decl_text = self.env.getIdent(decl_ident); - const fully_qualified_ident_idx = try self.env.insertQualifiedIdent(parent_text, decl_text); - - // Look up the fully qualified pattern (from parent scope via nesting) - switch (self.scopeLookup(.ident, fully_qualified_ident_idx)) { - .found => |pattern_idx| { - const current_scope = &self.scopes.items[self.scopes.items.len - 1]; - - // Add unqualified name (e.g., "my_not") - try current_scope.idents.put(self.env.gpa, decl_ident, pattern_idx); - - // Add type-qualified name (e.g., "MyBool.my_not") - const type_qualified_ident_idx = try self.env.insertQualifiedIdent(parent_type_text, decl_text); - try current_scope.idents.put(self.env.gpa, type_qualified_ident_idx, pattern_idx); - }, - .not_found => {}, - } - } - } - }, - else => { - // Note: .type_anno is not handled here because anno-only patterns - // are created during processAssociatedItemsSecondPass, so they need - // to be re-introduced AFTER that call completes - }, - } - } - - // Process the associated items (canonicalize their bodies) - try self.processAssociatedItemsSecondPass(qualified_name_idx, type_name, assoc.statements); - - // After processing, introduce anno-only defs into the associated block scope - // (They were just created by processAssociatedItemsSecondPass) - // We only add unqualified and type-qualified names; fully qualified is in parent scope - for (self.parse_ir.store.statementSlice(assoc.statements)) |anno_stmt_idx| { - const anno_stmt = self.parse_ir.store.getStatement(anno_stmt_idx); - switch (anno_stmt) { - .type_anno => |type_anno| { - if (self.parse_ir.tokens.resolveIdentifier(type_anno.name)) |anno_ident| { - // Build fully qualified name (e.g., "Test.MyBool.len") + } + }, + .decl => |decl| { + const pattern = self.parse_ir.store.getPattern(decl.pattern); + if (pattern == .ident) { + const pattern_ident_tok = pattern.ident.ident_tok; + if (self.parse_ir.tokens.resolveIdentifier(pattern_ident_tok)) |decl_ident| { + // Build fully qualified name (e.g., "Test.MyBool.my_not") const parent_text = self.env.getIdent(qualified_name_idx); - const anno_text = self.env.getIdent(anno_ident); - const fully_qualified_ident_idx = try self.env.insertQualifiedIdent(parent_text, anno_text); + const decl_text = self.env.getIdent(decl_ident); + const fully_qualified_ident_idx = try self.env.insertQualifiedIdent(parent_text, decl_text); // Look up the fully qualified pattern (from parent scope via nesting) switch (self.scopeLookup(.ident, fully_qualified_ident_idx)) { .found => |pattern_idx| { const current_scope = &self.scopes.items[self.scopes.items.len - 1]; - // Add unqualified name (e.g., "len") - try current_scope.idents.put(self.env.gpa, anno_ident, pattern_idx); + // Check if this is a placeholder by checking if the identifier is tracked + const is_placeholder = self.isPlaceholder(fully_qualified_ident_idx); - // Add type-qualified name (e.g., "List.len") - const type_qualified_ident_idx = try self.env.insertQualifiedIdent(parent_type_text, anno_text); + // Add unqualified name (e.g., "my_not") + try current_scope.idents.put(self.env.gpa, decl_ident, pattern_idx); + if (is_placeholder) { + try self.placeholder_idents.put(self.env.gpa, decl_ident, {}); + } + + // Add type-qualified name (e.g., "MyBool.my_not") + const type_qualified_ident_idx = try self.env.insertQualifiedIdent(parent_type_text, decl_text); try current_scope.idents.put(self.env.gpa, type_qualified_ident_idx, pattern_idx); + if (is_placeholder) { + try self.placeholder_idents.put(self.env.gpa, type_qualified_ident_idx, {}); + } }, - .not_found => { - // This can happen if the type_anno was followed by a matching decl - // (in which case it's not an anno-only def) - }, + .not_found => {}, } } - }, - else => {}, - } + } + }, + else => { + // Note: .type_anno is not handled here because anno-only patterns + // are created during processAssociatedItemsSecondPass, so they need + // to be re-introduced AFTER that call completes + }, } + } + + // Process the associated items (canonicalize their bodies) + try self.processAssociatedItemsSecondPass(qualified_name_idx, type_name, assoc.statements); + + // After processing, introduce anno-only defs into the associated block scope + // (They were just created by processAssociatedItemsSecondPass) + // We only add unqualified and type-qualified names; fully qualified is in parent scope + for (self.parse_ir.store.statementSlice(assoc.statements)) |anno_stmt_idx| { + const anno_stmt = self.parse_ir.store.getStatement(anno_stmt_idx); + switch (anno_stmt) { + .type_anno => |type_anno| { + if (self.parse_ir.tokens.resolveIdentifier(type_anno.name)) |anno_ident| { + // Build fully qualified name (e.g., "Test.MyBool.len") + const parent_text = self.env.getIdent(qualified_name_idx); + const anno_text = self.env.getIdent(anno_ident); + const fully_qualified_ident_idx = try self.env.insertQualifiedIdent(parent_text, anno_text); + + // Look up the fully qualified pattern (from parent scope via nesting) + switch (self.scopeLookup(.ident, fully_qualified_ident_idx)) { + .found => |pattern_idx| { + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + + // Add unqualified name (e.g., "len") + try current_scope.idents.put(self.env.gpa, anno_ident, pattern_idx); + + // Add type-qualified name (e.g., "List.len") + const type_qualified_ident_idx = try self.env.insertQualifiedIdent(parent_type_text, anno_text); + try current_scope.idents.put(self.env.gpa, type_qualified_ident_idx, pattern_idx); + }, + .not_found => { + // This can happen if the type_anno was followed by a matching decl + // (in which case it's not an anno-only def) + }, + } + } + }, + else => {}, + } + } } /// Canonicalize an associated item declaration with a qualified name @@ -745,7 +764,7 @@ fn processAssociatedItemsSecondPass( parent_name: Ident.Idx, parent_type_name: Ident.Idx, statements: AST.Statement.Span, -) std.mem.Allocator.Error!void { +) std.mem.Allocator.Error!void { const stmt_idxs = self.parse_ir.store.statementSlice(statements); var i: usize = 0; while (i < stmt_idxs.len) : (i += 1) { @@ -837,10 +856,14 @@ fn processAssociatedItemsSecondPass( // Update type-qualified name (e.g., "MyBool.my_not") const type_qualified_idx = try self.env.insertQualifiedIdent(self.env.getIdent(parent_type_name), decl_text); - try self.updatePlaceholder(current_scope, type_qualified_idx, pattern_idx); + if (type_qualified_idx.idx != decl_ident.idx) { + try self.updatePlaceholder(current_scope, type_qualified_idx, pattern_idx); + } // Update fully qualified name (e.g., "Test.MyBool.my_not") - try self.updatePlaceholder(current_scope, qualified_idx, pattern_idx); + if (qualified_idx.idx != type_qualified_idx.idx and qualified_idx.idx != decl_ident.idx) { + try self.updatePlaceholder(current_scope, qualified_idx, pattern_idx); + } break :blk true; // Found and processed matching decl } @@ -876,10 +899,14 @@ fn processAssociatedItemsSecondPass( // Update type-qualified name (e.g., "List.is_empty") const type_qualified_idx = try self.env.insertQualifiedIdent(self.env.getIdent(parent_type_name), name_text); - try self.updatePlaceholder(current_scope, type_qualified_idx, pattern_idx); + if (type_qualified_idx.idx != name_ident.idx) { + try self.updatePlaceholder(current_scope, type_qualified_idx, pattern_idx); + } // Update fully qualified name (e.g., "Builtin.List.is_empty") - try self.updatePlaceholder(current_scope, qualified_idx, pattern_idx); + if (qualified_idx.idx != type_qualified_idx.idx and qualified_idx.idx != name_ident.idx) { + try self.updatePlaceholder(current_scope, qualified_idx, pattern_idx); + } try self.env.store.addScratchDef(def_idx); } @@ -980,6 +1007,10 @@ fn processAssociatedItemsFirstPass( }; const placeholder_pattern_idx = try self.env.addPattern(placeholder_pattern, region); + // Track both identifiers as placeholders + try self.placeholder_idents.put(self.env.gpa, qualified_idx, {}); + try self.placeholder_idents.put(self.env.gpa, decl_ident, {}); + // Directly put placeholder in scope (no conflict checking) // updatePlaceholder will verify it's replacing a placeholder later const current_scope = &self.scopes.items[self.scopes.items.len - 1]; @@ -996,7 +1027,6 @@ fn processAssociatedItemsFirstPass( const anno_text = self.env.getIdent(anno_ident); const qualified_idx = try self.env.insertQualifiedIdent(parent_text, anno_text); - const region = self.parse_ir.tokenizedRegionToRegion(type_anno.region); const placeholder_pattern = Pattern{ .assign = .{ @@ -1005,6 +1035,10 @@ fn processAssociatedItemsFirstPass( }; const placeholder_pattern_idx = try self.env.addPattern(placeholder_pattern, region); + // Track both identifiers as placeholders + try self.placeholder_idents.put(self.env.gpa, qualified_idx, {}); + try self.placeholder_idents.put(self.env.gpa, anno_ident, {}); + // Directly put placeholder in scope (no conflict checking) // updatePlaceholder will verify it's replacing a placeholder later const current_scope = &self.scopes.items[self.scopes.items.len - 1]; @@ -1113,13 +1147,15 @@ pub fn canonicalizeFile( const scratch_defs_start = self.env.store.scratchDefTop(); const scratch_statements_start = self.env.store.scratch.?.statements.top(); - // First pass: Process all type declarations to introduce them into scope - // Defer associated blocks until after we've created placeholders for top-level items + // First pass (1a): Process type declarations WITH associated blocks to introduce them into scope + // Defer associated blocks themselves until after we've created placeholders for top-level items for (self.parse_ir.store.statementSlice(file.statements)) |stmt_id| { const stmt = self.parse_ir.store.getStatement(stmt_id); switch (stmt) { .type_decl => |type_decl| { - try self.processTypeDeclFirstPass(type_decl, null, true); // defer associated blocks + if (type_decl.associated) |_| { + try self.processTypeDeclFirstPass(type_decl, null, true); // defer associated blocks + } }, else => { // Skip non-type-declaration statements in first pass @@ -1127,59 +1163,9 @@ pub fn canonicalizeFile( } } - // Phase 1.5: Create placeholders for top-level declarations and type annotations - // This ensures sibling items (like list_get_unsafe) are available during associated block processing - for (self.parse_ir.store.statementSlice(file.statements)) |stmt_id| { - const stmt = self.parse_ir.store.getStatement(stmt_id); - switch (stmt) { - .decl => |decl| { - const pattern = self.parse_ir.store.getPattern(decl.pattern); - if (pattern == .ident) { - const pattern_ident_tok = pattern.ident.ident_tok; - if (self.parse_ir.tokens.resolveIdentifier(pattern_ident_tok)) |decl_ident| { - const region = self.parse_ir.tokenizedRegionToRegion(decl.region); - const placeholder_pattern = Pattern{ - .assign = .{ - .ident = decl_ident, - }, - }; - const placeholder_pattern_idx = try self.env.addPattern(placeholder_pattern, region); - - - // Directly put placeholder in current scope - const current_scope = &self.scopes.items[self.scopes.items.len - 1]; - try current_scope.idents.put(self.env.gpa, decl_ident, placeholder_pattern_idx); - // Track that this is a placeholder - try self.placeholder_idents.put(self.env.gpa, decl_ident, {}); - } - } - }, - .type_anno => |type_anno| { - if (self.parse_ir.tokens.resolveIdentifier(type_anno.name)) |anno_ident| { - const region = self.parse_ir.tokenizedRegionToRegion(type_anno.region); - const placeholder_pattern = Pattern{ - .assign = .{ - .ident = anno_ident, - }, - }; - const placeholder_pattern_idx = try self.env.addPattern(placeholder_pattern, region); - - - // Directly put placeholder in current scope - const current_scope = &self.scopes.items[self.scopes.items.len - 1]; - try current_scope.idents.put(self.env.gpa, anno_ident, placeholder_pattern_idx); - // Track that this is a placeholder - try self.placeholder_idents.put(self.env.gpa, anno_ident, {}); - } - }, - else => { - // Skip other statement types - }, - } - } - - // For type modules, expose the main type and all associated items BEFORE processing associated blocks - // This ensures unused variable checking (which happens during scope exit) doesn't flag exposed items + // For type modules, mark the main type and all associated items as exposed + // This must happen BEFORE processing to avoid unused variable warnings + // Note: addExposedById just marks items as exposed; node indices are set later when defs are created if (self.env.module_kind == .type_module) { const module_name_text = self.env.module_name; for (self.parse_ir.store.statementSlice(file.statements)) |stmt_id| { @@ -1202,8 +1188,8 @@ pub fn canonicalizeFile( } // Phase 1.6: Now process all deferred type declaration associated blocks - // All top-level placeholders are now in scope, and for type modules, all items are marked as exposed - var assoc_blocks_found: usize = 0; + // processAssociatedBlock creates placeholders for associated items via processAssociatedItemsFirstPass + // This introduces nested types (like Foo.Bar) that other type declarations may reference for (self.parse_ir.store.statementSlice(file.statements)) |stmt_id| { const stmt = self.parse_ir.store.getStatement(stmt_id); if (stmt == .type_decl) { @@ -1211,13 +1197,28 @@ pub fn canonicalizeFile( if (type_decl.associated) |assoc| { const type_header = self.parse_ir.store.getTypeHeader(type_decl.header) catch continue; const type_ident = self.parse_ir.tokens.resolveIdentifier(type_header.name) orelse continue; - assoc_blocks_found += 1; try self.processAssociatedBlock(type_ident, type_ident, assoc); } } } + // Phase 1.7: Process type declarations WITHOUT associated blocks + // These can now reference nested types that were introduced in Phase 1.6 + for (self.parse_ir.store.statementSlice(file.statements)) |stmt_id| { + const stmt = self.parse_ir.store.getStatement(stmt_id); + switch (stmt) { + .type_decl => |type_decl| { + if (type_decl.associated == null) { + try self.processTypeDeclFirstPass(type_decl, null, false); // no associated block to defer + } + }, + else => { + // Skip non-type-declaration statements + }, + } + } + // Second pass: Process all other statements const ast_stmt_idxs = self.parse_ir.store.statementSlice(file.statements); var i: usize = 0; @@ -4654,13 +4655,13 @@ fn canonicalizePattern( .ident = ident_idx, } }, region); - // Check if a placeholder exists for this identifier - // Placeholders are tracked in placeholder_idents and exist only until replaced - const placeholder_exists = self.placeholder_idents.contains(ident_idx); + // Check if a placeholder exists for this identifier in the current scope + // Placeholders are tracked in the placeholder_idents hash map + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + const placeholder_exists = self.isPlaceholder(ident_idx); if (placeholder_exists) { // Replace the placeholder in the current scope - const current_scope = &self.scopes.items[self.scopes.items.len - 1]; try self.updatePlaceholder(current_scope, ident_idx, pattern_idx); } else { // Introduce the identifier into scope mapping to this pattern node @@ -8150,8 +8151,16 @@ fn introduceType( } } +/// Check if an identifier is a placeholder, with fast path for empty map (99% of files). +/// Returns true if the identifier is tracked as a placeholder. +fn isPlaceholder(self: *const Self, ident_idx: Ident.Idx) bool { + // Fast path: if map is empty, no placeholders exist + if (self.placeholder_idents.count() == 0) return false; + return self.placeholder_idents.contains(ident_idx); +} + /// Update a placeholder pattern in scope with the actual pattern. -/// In debug builds, asserts that the old value was actually a placeholder (.assign pattern). +/// In debug builds, asserts that the identifier was tracked as a placeholder. fn updatePlaceholder( self: *Self, scope: *Scope, @@ -8159,14 +8168,13 @@ fn updatePlaceholder( pattern_idx: Pattern.Idx, ) std.mem.Allocator.Error!void { if (builtin.mode == .Debug) { - if (scope.idents.get(ident_idx)) |old_pattern_idx| { - const old_pattern = self.env.store.getPattern(old_pattern_idx); - std.debug.assert(old_pattern == .assign); - } + std.debug.assert(self.isPlaceholder(ident_idx)); + } + // Remove from placeholder tracking since it's now a real definition + if (self.placeholder_idents.count() > 0) { + _ = self.placeholder_idents.remove(ident_idx); } try scope.idents.put(self.env.gpa, ident_idx, pattern_idx); - // Remove from placeholder set since it's now a real definition - _ = self.placeholder_idents.remove(ident_idx); } fn scopeUpdateTypeDecl( diff --git a/test/snapshots/nominal/nominal_associated_decls.md b/test/snapshots/nominal/nominal_associated_decls.md index 5f00517caa..83900f6730 100644 --- a/test/snapshots/nominal/nominal_associated_decls.md +++ b/test/snapshots/nominal/nominal_associated_decls.md @@ -64,9 +64,6 @@ Foo := [Whatever].{ # CANONICALIZE ~~~clojure (can-ir - (d-let - (p-assign (ident "Foo.Bar.baz")) - (e-num (value "5"))) (d-let (p-assign (ident "Foo.Bar.baz")) (e-num (value "5"))) @@ -86,7 +83,6 @@ Foo := [Whatever].{ ~~~clojure (inferred-types (defs - (patt (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) (patt (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) (patt (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]"))) (type_decls @@ -95,7 +91,6 @@ Foo := [Whatever].{ (nominal (type "Foo.Bar") (ty-header (name "Foo.Bar")))) (expressions - (expr (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) (expr (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) (expr (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")))) ~~~ diff --git a/test/snapshots/nominal/nominal_associated_deep_nesting.md b/test/snapshots/nominal/nominal_associated_deep_nesting.md index 9ced393c6b..f86ca29bde 100644 --- a/test/snapshots/nominal/nominal_associated_deep_nesting.md +++ b/test/snapshots/nominal/nominal_associated_deep_nesting.md @@ -110,15 +110,6 @@ deepType = C # CANONICALIZE ~~~clojure (can-ir - (d-let - (p-assign (ident "Foo.Level1.Level2.Level3.value")) - (e-num (value "42"))) - (d-let - (p-assign (ident "Foo.Level1.Level2.Level3.value")) - (e-num (value "42"))) - (d-let - (p-assign (ident "Foo.Level1.Level2.Level3.value")) - (e-num (value "42"))) (d-let (p-assign (ident "Foo.Level1.Level2.Level3.value")) (e-num (value "42"))) @@ -154,9 +145,6 @@ deepType = C ~~~clojure (inferred-types (defs - (patt (type "Num(Int(Unsigned64))")) - (patt (type "Num(Int(Unsigned64))")) - (patt (type "Num(Int(Unsigned64))")) (patt (type "Num(Int(Unsigned64))")) (patt (type "Num(Int(Unsigned64))")) (patt (type "Foo.Level1.Level2.Level3"))) @@ -170,9 +158,6 @@ deepType = C (nominal (type "Foo.Level1.Level2.Level3") (ty-header (name "Foo.Level1.Level2.Level3")))) (expressions - (expr (type "Num(Int(Unsigned64))")) - (expr (type "Num(Int(Unsigned64))")) - (expr (type "Num(Int(Unsigned64))")) (expr (type "Num(Int(Unsigned64))")) (expr (type "Num(Int(Unsigned64))")) (expr (type "Foo.Level1.Level2.Level3")))) diff --git a/test/snapshots/nominal/nominal_associated_lookup_nested.md b/test/snapshots/nominal/nominal_associated_lookup_nested.md index 40f00377fd..2a579f7a22 100644 --- a/test/snapshots/nominal/nominal_associated_lookup_nested.md +++ b/test/snapshots/nominal/nominal_associated_lookup_nested.md @@ -84,9 +84,6 @@ myNum = Foo.Bar.baz # CANONICALIZE ~~~clojure (can-ir - (d-let - (p-assign (ident "Foo.Bar.baz")) - (e-num (value "5"))) (d-let (p-assign (ident "Foo.Bar.baz")) (e-num (value "5"))) @@ -114,7 +111,6 @@ myNum = Foo.Bar.baz ~~~clojure (inferred-types (defs - (patt (type "Num(Int(Unsigned64))")) (patt (type "Num(Int(Unsigned64))")) (patt (type "Foo.Bar")) (patt (type "Num(Int(Unsigned64))"))) @@ -124,7 +120,6 @@ myNum = Foo.Bar.baz (nominal (type "Foo.Bar") (ty-header (name "Foo.Bar")))) (expressions - (expr (type "Num(Int(Unsigned64))")) (expr (type "Num(Int(Unsigned64))")) (expr (type "Foo.Bar")) (expr (type "Num(Int(Unsigned64))")))) diff --git a/test/snapshots/nominal/nominal_deeply_nested_types.md b/test/snapshots/nominal/nominal_deeply_nested_types.md index c81b48cacb..c298cae930 100644 --- a/test/snapshots/nominal/nominal_deeply_nested_types.md +++ b/test/snapshots/nominal/nominal_deeply_nested_types.md @@ -102,24 +102,6 @@ Foo := [Whatever].{ # CANONICALIZE ~~~clojure (can-ir - (d-let - (p-assign (ident "Foo.Bar.Baz.Qux.w")) - (e-num (value "1"))) - (d-let - (p-assign (ident "Foo.Bar.Baz.Qux.w")) - (e-num (value "1"))) - (d-let - (p-assign (ident "Foo.Bar.Baz.z")) - (e-num (value "2"))) - (d-let - (p-assign (ident "Foo.Bar.Baz.Qux.w")) - (e-num (value "1"))) - (d-let - (p-assign (ident "Foo.Bar.Baz.z")) - (e-num (value "2"))) - (d-let - (p-assign (ident "Foo.Bar.y")) - (e-num (value "3"))) (d-let (p-assign (ident "Foo.Bar.Baz.Qux.w")) (e-num (value "1"))) @@ -153,12 +135,6 @@ Foo := [Whatever].{ ~~~clojure (inferred-types (defs - (patt (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) - (patt (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) - (patt (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) - (patt (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) - (patt (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) - (patt (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) (patt (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) (patt (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) (patt (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) @@ -173,12 +149,6 @@ Foo := [Whatever].{ (nominal (type "Foo.Bar.Baz.Qux") (ty-header (name "Foo.Bar.Baz.Qux")))) (expressions - (expr (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) - (expr (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) - (expr (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) - (expr (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) - (expr (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) - (expr (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) (expr (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) (expr (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) (expr (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) diff --git a/test/snapshots/nominal/nominal_nested_types.md b/test/snapshots/nominal/nominal_nested_types.md index abfb0822ff..7564952d99 100644 --- a/test/snapshots/nominal/nominal_nested_types.md +++ b/test/snapshots/nominal/nominal_nested_types.md @@ -64,9 +64,6 @@ Foo := [Whatever].{ # CANONICALIZE ~~~clojure (can-ir - (d-let - (p-assign (ident "Foo.Bar.y")) - (e-num (value "6"))) (d-let (p-assign (ident "Foo.Bar.y")) (e-num (value "6"))) @@ -86,7 +83,6 @@ Foo := [Whatever].{ ~~~clojure (inferred-types (defs - (patt (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) (patt (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) (patt (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]"))) (type_decls @@ -95,7 +91,6 @@ Foo := [Whatever].{ (nominal (type "Foo.Bar") (ty-header (name "Foo.Bar")))) (expressions - (expr (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) (expr (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) (expr (type "num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")))) ~~~ diff --git a/test/snapshots/nominal/type_module_associated_items_exposed.md b/test/snapshots/nominal/type_module_associated_items_exposed.md index 8169185a92..650c17dd60 100644 --- a/test/snapshots/nominal/type_module_associated_items_exposed.md +++ b/test/snapshots/nominal/type_module_associated_items_exposed.md @@ -63,9 +63,6 @@ Foo := [Blah].{ # CANONICALIZE ~~~clojure (can-ir - (d-let - (p-assign (ident "Foo.Bar.baz")) - (e-empty_record)) (d-let (p-assign (ident "Foo.Bar.baz")) (e-empty_record)) @@ -84,7 +81,6 @@ Foo := [Blah].{ ~~~clojure (inferred-types (defs - (patt (type "{}")) (patt (type "{}")) (patt (type "{}"))) (type_decls @@ -93,7 +89,6 @@ Foo := [Blah].{ (nominal (type "Foo.Bar") (ty-header (name "Foo.Bar")))) (expressions - (expr (type "{}")) (expr (type "{}")) (expr (type "{}")))) ~~~ From a40386ef20fa126af9a5aad7f661e0c28fc1ecc4 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Sat, 8 Nov 2025 12:38:02 -0500 Subject: [PATCH 31/38] Fix merge conflicts --- src/canonicalize/Can.zig | 155 +++++++++--------- .../test/exposed_shadowing_test.zig | 10 +- 2 files changed, 84 insertions(+), 81 deletions(-) diff --git a/src/canonicalize/Can.zig b/src/canonicalize/Can.zig index 6f165f2222..48c491b282 100644 --- a/src/canonicalize/Can.zig +++ b/src/canonicalize/Can.zig @@ -512,7 +512,7 @@ fn processAssociatedBlock( assoc: anytype, ) std.mem.Allocator.Error!void { // First, introduce placeholder patterns for all associated items - try self.processAssociatedItemsFirstPass(qualified_name_idx, assoc.statements); + try self.processAssociatedItemsFirstPass(qualified_name_idx, type_name, assoc.statements); // Now enter a new scope for the associated block where both qualified and unqualified names work try self.scopeEnter(self.env.gpa, false); // false = not a function boundary @@ -966,6 +966,7 @@ fn processAssociatedItemsSecondPass( fn processAssociatedItemsFirstPass( self: *Self, parent_name: Ident.Idx, + parent_type_name: Ident.Idx, statements: AST.Statement.Span, ) std.mem.Allocator.Error!void { // Two-phase approach for sibling types: @@ -994,9 +995,7 @@ fn processAssociatedItemsFirstPass( const pattern_ident_tok = pattern.ident.ident_tok; if (self.parse_ir.tokens.resolveIdentifier(pattern_ident_tok)) |decl_ident| { // Build qualified name (e.g., "Foo.Bar.baz") - const parent_text = self.env.getIdent(parent_name); - const decl_text = self.env.getIdent(decl_ident); - const qualified_idx = try self.env.insertQualifiedIdent(parent_text, decl_text); + const qualified_idx = try self.env.insertQualifiedIdent(self.env.getIdent(parent_name), self.env.getIdent(decl_ident)); // Create placeholder pattern with qualified name const region = self.parse_ir.tokenizedRegionToRegion(decl.region); @@ -1007,15 +1006,21 @@ fn processAssociatedItemsFirstPass( }; const placeholder_pattern_idx = try self.env.addPattern(placeholder_pattern, region); - // Track both identifiers as placeholders + // Also compute type-qualified name (e.g., "List.map") + // Re-fetch identifiers since insertQualifiedIdent may have reallocated the identifier table + const type_qualified_idx = try self.env.insertQualifiedIdent(self.env.getIdent(parent_type_name), self.env.getIdent(decl_ident)); + + // Track all three identifiers as placeholders try self.placeholder_idents.put(self.env.gpa, qualified_idx, {}); try self.placeholder_idents.put(self.env.gpa, decl_ident, {}); + try self.placeholder_idents.put(self.env.gpa, type_qualified_idx, {}); // Directly put placeholder in scope (no conflict checking) // updatePlaceholder will verify it's replacing a placeholder later const current_scope = &self.scopes.items[self.scopes.items.len - 1]; try current_scope.idents.put(self.env.gpa, qualified_idx, placeholder_pattern_idx); try current_scope.idents.put(self.env.gpa, decl_ident, placeholder_pattern_idx); + try current_scope.idents.put(self.env.gpa, type_qualified_idx, placeholder_pattern_idx); } } }, @@ -1023,9 +1028,7 @@ fn processAssociatedItemsFirstPass( // Create placeholder for anno-only defs so they can be referenced by sibling types // processAssociatedItemsSecondPass will later use updatePlaceholder to replace these if (self.parse_ir.tokens.resolveIdentifier(type_anno.name)) |anno_ident| { - const parent_text = self.env.getIdent(parent_name); - const anno_text = self.env.getIdent(anno_ident); - const qualified_idx = try self.env.insertQualifiedIdent(parent_text, anno_text); + const qualified_idx = try self.env.insertQualifiedIdent(self.env.getIdent(parent_name), self.env.getIdent(anno_ident)); const region = self.parse_ir.tokenizedRegionToRegion(type_anno.region); const placeholder_pattern = Pattern{ @@ -1035,15 +1038,21 @@ fn processAssociatedItemsFirstPass( }; const placeholder_pattern_idx = try self.env.addPattern(placeholder_pattern, region); - // Track both identifiers as placeholders + // Also compute type-qualified name (e.g., "List.is_empty") + // Re-fetch anno_text since insertQualifiedIdent may have reallocated the identifier table + const type_qualified_idx = try self.env.insertQualifiedIdent(self.env.getIdent(parent_type_name), self.env.getIdent(anno_ident)); + + // Track all three identifiers as placeholders try self.placeholder_idents.put(self.env.gpa, qualified_idx, {}); try self.placeholder_idents.put(self.env.gpa, anno_ident, {}); + try self.placeholder_idents.put(self.env.gpa, type_qualified_idx, {}); // Directly put placeholder in scope (no conflict checking) // updatePlaceholder will verify it's replacing a placeholder later const current_scope = &self.scopes.items[self.scopes.items.len - 1]; try current_scope.idents.put(self.env.gpa, qualified_idx, placeholder_pattern_idx); try current_scope.idents.put(self.env.gpa, anno_ident, placeholder_pattern_idx); + try current_scope.idents.put(self.env.gpa, type_qualified_idx, placeholder_pattern_idx); } }, else => { @@ -1163,27 +1172,45 @@ pub fn canonicalizeFile( } } - // For type modules, mark the main type and all associated items as exposed - // This must happen BEFORE processing to avoid unused variable warnings - // Note: addExposedById just marks items as exposed; node indices are set later when defs are created - if (self.env.module_kind == .type_module) { - const module_name_text = self.env.module_name; - for (self.parse_ir.store.statementSlice(file.statements)) |stmt_id| { - const stmt = self.parse_ir.store.getStatement(stmt_id); - if (stmt == .type_decl) { - const type_decl = stmt.type_decl; - const type_header = self.parse_ir.store.getTypeHeader(type_decl.header) catch continue; - const type_name_ident = self.parse_ir.tokens.resolveIdentifier(type_header.name) orelse continue; - const type_name_text = self.env.getIdent(type_name_ident); - - if (std.mem.eql(u8, type_name_text, module_name_text)) { - // Expose the main type - try self.env.addExposedById(type_name_ident); - // Expose all associated items recursively - try self.exposeAssociatedItems(type_name_ident, type_decl); - break; + // Phase 1.5.5: Create placeholders for top-level decls and type annos + // This ensures they're available when processing associated blocks + const top_level_stmts = self.parse_ir.store.statementSlice(file.statements); + for (top_level_stmts) |stmt_id| { + const stmt = self.parse_ir.store.getStatement(stmt_id); + switch (stmt) { + .decl => |decl| { + const pattern = self.parse_ir.store.getPattern(decl.pattern); + if (pattern == .ident) { + const pattern_ident_tok = pattern.ident.ident_tok; + if (self.parse_ir.tokens.resolveIdentifier(pattern_ident_tok)) |decl_ident| { + const region = self.parse_ir.tokenizedRegionToRegion(decl.region); + const placeholder_pattern = Pattern{ + .assign = .{ + .ident = decl_ident, + }, + }; + const placeholder_pattern_idx = try self.env.addPattern(placeholder_pattern, region); + try self.placeholder_idents.put(self.env.gpa, decl_ident, {}); + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + try current_scope.idents.put(self.env.gpa, decl_ident, placeholder_pattern_idx); + } } - } + }, + .type_anno => |type_anno| { + if (self.parse_ir.tokens.resolveIdentifier(type_anno.name)) |anno_ident| { + const region = self.parse_ir.tokenizedRegionToRegion(type_anno.region); + const placeholder_pattern = Pattern{ + .assign = .{ + .ident = anno_ident, + }, + }; + const placeholder_pattern_idx = try self.env.addPattern(placeholder_pattern, region); + try self.placeholder_idents.put(self.env.gpa, anno_ident, {}); + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + try current_scope.idents.put(self.env.gpa, anno_ident, placeholder_pattern_idx); + } + }, + else => {}, } } @@ -1198,7 +1225,17 @@ pub fn canonicalizeFile( const type_header = self.parse_ir.store.getTypeHeader(type_decl.header) catch continue; const type_ident = self.parse_ir.tokens.resolveIdentifier(type_header.name) orelse continue; - try self.processAssociatedBlock(type_ident, type_ident, assoc); + // Build fully qualified name (e.g., "Builtin.Str") + // For type-modules where the main type name equals the module name, + // use just the module name to avoid "Builtin.Builtin" + const module_name_text = self.env.module_name; + const type_name_text = self.env.getIdent(type_ident); + const qualified_type_ident = if (std.mem.eql(u8, module_name_text, type_name_text)) + type_ident // Type-module: use unqualified name + else + try self.env.insertQualifiedIdent(module_name_text, type_name_text); + + try self.processAssociatedBlock(qualified_type_ident, type_ident, assoc); } } } @@ -1541,41 +1578,9 @@ fn createAnnoOnlyDef( }; const pattern_idx = try self.env.addPattern(pattern, region); - // Check if a placeholder already exists (from Phase 1.5 for top-level items or Phase 2a for associated items) - // Use scopeContains to check all scopes, not just the current one - const existing_pattern = self.scopeContains(.ident, ident); - const placeholder_exists = if (existing_pattern) |existing_pat_idx| blk: { - // Check if it's a placeholder (assign pattern) - const existing_pat = self.env.store.getPattern(existing_pat_idx); - break :blk existing_pat == .assign; - } else false; - - if (placeholder_exists) { - // Find which scope has the placeholder and replace it - var scope_idx = self.scopes.items.len; - while (scope_idx > 0) { - scope_idx -= 1; - const scope = &self.scopes.items[scope_idx]; - if (scope.idents.get(ident)) |_| { - try self.updatePlaceholder(scope, ident, pattern_idx); - break; - } - } - } else { - // Introduce the name to scope normally - switch (try self.scopeIntroduceInternal(self.env.gpa, .ident, ident, pattern_idx, false, true)) { - .success => {}, - .shadowing_warning => |shadowed_pattern_idx| { - const original_region = self.env.store.getPatternRegion(shadowed_pattern_idx); - try self.env.pushDiagnostic(Diagnostic{ .shadowing_warning = .{ - .ident = ident, - .region = region, - .original_region = original_region, - } }); - }, - else => {}, - } - } + // Note: We don't update placeholders here. For associated items, the calling code + // (processAssociatedItemsSecondPass) will update all three identifiers (qualified, + // type-qualified, unqualified). For top-level items, there are no placeholders to update. // Create the e_anno_only expression const anno_only_expr = try self.env.addExpr(Expr{ .e_anno_only = .{} }, region); @@ -1774,8 +1779,8 @@ fn createExposedScope( // Get the interned identifier if (self.parse_ir.tokens.resolveIdentifier(type_name.ident)) |ident_idx| { - // Add to exposed_items for permanent storage (unconditionally) - try self.env.addExposedById(ident_idx); + // Don't add types to exposed_items - types are not values + // Only add to type_bindings for type resolution // Use a dummy statement index - we just need to track that it's exposed const dummy_idx = @as(Statement.Idx, @enumFromInt(0)); @@ -1807,8 +1812,8 @@ fn createExposedScope( // Get the interned identifier if (self.parse_ir.tokens.resolveIdentifier(type_with_constructors.ident)) |ident_idx| { - // Add to exposed_items for permanent storage (unconditionally) - try self.env.addExposedById(ident_idx); + // Don't add types to exposed_items - types are not values + // Only add to type_bindings for type resolution // Use a dummy statement index - we just need to track that it's exposed const dummy_idx = @as(Statement.Idx, @enumFromInt(0)); @@ -9052,10 +9057,8 @@ fn exposeAssociatedItems(self: *Self, parent_name: Ident.Idx, type_decl: anytype const nested_text = self.env.getIdent(nested_ident); const qualified_idx = try self.env.insertQualifiedIdent(parent_text, nested_text); - // Expose the nested type - try self.env.addExposedById(qualified_idx); - - // Recursively expose its associated items + // Don't expose the nested type itself - types are not values + // Only recursively expose its associated items (defs, not types) try self.exposeAssociatedItems(qualified_idx, nested_type_decl); }, .decl => |decl| { @@ -9082,10 +9085,10 @@ fn exposeAssociatedItems(self: *Self, parent_name: Ident.Idx, type_decl: anytype const anno_text = self.env.getIdent(anno_ident); const qualified_idx = try self.env.insertQualifiedIdent(parent_text, anno_text); - // Expose both the qualified and unqualified names - // (both are added to scope in Phase 2a) + // Expose the qualified name + // The unqualified name is added to scope but doesn't need to be in exposed_items + // because lookups use the qualified name try self.env.addExposedById(qualified_idx); - try self.env.addExposedById(anno_ident); } }, else => {}, diff --git a/src/canonicalize/test/exposed_shadowing_test.zig b/src/canonicalize/test/exposed_shadowing_test.zig index 30fa77c0f3..b9dabcffc2 100644 --- a/src/canonicalize/test/exposed_shadowing_test.zig +++ b/src/canonicalize/test/exposed_shadowing_test.zig @@ -339,15 +339,15 @@ test "exposed_items is populated correctly" { .canonicalizeFile(); // Check that exposed_items contains the correct number of items // The exposed items were added during canonicalization - // Should have exactly 3 entries (duplicates not stored) - try testing.expectEqual(@as(usize, 3), env.common.exposed_items.count()); - // Check that exposed_items contains all exposed items + // Should have exactly 2 value entries (duplicates not stored, types not included) + // Types are not stored in exposed_items - they are handled by the type system + try testing.expectEqual(@as(usize, 2), env.common.exposed_items.count()); + // Check that exposed_items contains all exposed values (not types) const foo_idx = env.common.idents.findByString("foo").?; const bar_idx = env.common.idents.findByString("bar").?; - const mytype_idx = env.common.idents.findByString("MyType").?; try testing.expect(env.common.exposed_items.containsById(env.gpa, @bitCast(foo_idx))); try testing.expect(env.common.exposed_items.containsById(env.gpa, @bitCast(bar_idx))); - try testing.expect(env.common.exposed_items.containsById(env.gpa, @bitCast(mytype_idx))); + // MyType is not in exposed_items because it's a type, not a value } test "exposed_items persists after canonicalization" { From 0c0c2e8cb4ef5f98f1d5722767a44b4165958629 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Sat, 8 Nov 2025 15:57:16 -0500 Subject: [PATCH 32/38] More fixes --- src/canonicalize/Can.zig | 406 ++++++++++++++---- test/snapshots/fuzz_crash/fuzz_crash_023.md | 23 + .../nominal_associated_alias_within_block.md | 26 +- .../nominal/nominal_associated_type_alias.md | 8 +- .../nominal/nominal_associated_value_alias.md | 4 +- .../nominal/nominal_associated_vs_module.md | 10 +- ...ominal_associated_with_final_expression.md | 2 +- .../nominal/nominal_deeply_nested_tag.md | 18 +- .../nominal/nominal_four_level_nested_tag.md | 24 +- .../nominal/nominal_nested_type_ref.md | 12 +- .../nominal/nominal_simple_nested_tag.md | 12 +- 11 files changed, 398 insertions(+), 147 deletions(-) diff --git a/src/canonicalize/Can.zig b/src/canonicalize/Can.zig index 48c491b282..f1da9afaeb 100644 --- a/src/canonicalize/Can.zig +++ b/src/canonicalize/Can.zig @@ -545,10 +545,20 @@ fn processAssociatedBlock( const nested_type_text = self.env.getIdent(unqualified_ident); const qualified_ident_idx = try self.env.insertQualifiedIdent(parent_text, nested_type_text); - // Introduce unqualified type alias (fully qualified is already in parent scope) + // Introduce type aliases (fully qualified is already in parent scope from processTypeDeclFirstPass) if (self.scopeLookupTypeDecl(qualified_ident_idx)) |qualified_type_decl_idx| { const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + + // Add unqualified alias (e.g., "Bar" -> the fully qualified type) try current_scope.introduceTypeAlias(self.env.gpa, unqualified_ident, qualified_type_decl_idx); + + // Add user-facing qualified alias (e.g., "Foo.Bar" -> the fully qualified type) + // This allows users to write "Foo.Bar" in type annotations + // Re-fetch nested_type_text since insertQualifiedIdent may have reallocated + const type_name_text_str = self.env.getIdent(type_name); + const nested_type_text_str = self.env.getIdent(unqualified_ident); + const user_qualified_ident_idx = try self.env.insertQualifiedIdent(type_name_text_str, nested_type_text_str); + try current_scope.introduceTypeAlias(self.env.gpa, user_qualified_ident_idx, qualified_type_decl_idx); } // Introduce associated items of nested types @@ -969,15 +979,113 @@ fn processAssociatedItemsFirstPass( parent_type_name: Ident.Idx, statements: AST.Statement.Span, ) std.mem.Allocator.Error!void { - // Two-phase approach for sibling types: - // Phase 1: Introduce all type declarations (defer their associated blocks) - // Phase 2: Process all deferred associated blocks (now all siblings are in scope) + // Multi-phase approach for sibling types: + // Phase 1a: Introduce nominal type declarations (defer annotations and associated blocks) + // Phase 1b: Add user-facing aliases for the nominal types + // Phase 1c: Process type aliases (which can now reference the nominal types) + // Phase 2: Process deferred associated blocks - // Phase 1: Introduce type declarations without processing their associated blocks + // Phase 1a: Introduce nominal type declarations WITHOUT processing annotations/associated blocks + // This creates placeholder types that can be referenced for (self.parse_ir.store.statementSlice(statements)) |stmt_idx| { const stmt = self.parse_ir.store.getStatement(stmt_idx); if (stmt == .type_decl) { - try self.processTypeDeclFirstPass(stmt.type_decl, parent_name, true); // defer associated blocks + const type_decl = stmt.type_decl; + // Only process nominal types in this phase; aliases will be processed later + if (type_decl.kind == .nominal) { + try self.processTypeDeclFirstPass(type_decl, parent_name, true); // defer associated blocks + } + } + } + + // Phase 1b: Add user-facing qualified aliases for nominal types + // This must happen before Phase 1c so that type aliases can reference these types + for (self.parse_ir.store.statementSlice(statements)) |stmt_idx| { + const stmt = self.parse_ir.store.getStatement(stmt_idx); + if (stmt == .type_decl) { + const type_decl = stmt.type_decl; + if (type_decl.kind == .nominal) { + const type_header = self.parse_ir.store.getTypeHeader(type_decl.header) catch continue; + const nested_type_ident = self.parse_ir.tokens.resolveIdentifier(type_header.name) orelse continue; + + // Build fully qualified name (e.g., "module.Foo.Bar") + const parent_text = self.env.getIdent(parent_name); + const nested_type_text = self.env.getIdent(nested_type_ident); + const fully_qualified_ident_idx = try self.env.insertQualifiedIdent(parent_text, nested_type_text); + + // Look up the fully qualified type that was just registered + if (self.scopeLookupTypeDecl(fully_qualified_ident_idx)) |type_decl_idx| { + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + + // Build user-facing qualified name by stripping module prefix + // Re-fetch strings since insertQualifiedIdent may have reallocated + const fully_qualified_text = self.env.getIdent(fully_qualified_ident_idx); + const module_prefix = self.env.module_name; + + // Check if the fully qualified name starts with the module name + const user_facing_text = if (std.mem.startsWith(u8, fully_qualified_text, module_prefix) and + fully_qualified_text.len > module_prefix.len and + fully_qualified_text[module_prefix.len] == '.') + fully_qualified_text[module_prefix.len + 1..] // Skip "module." + else + fully_qualified_text; // No module prefix, use as-is + + // Only add alias if it's different from the fully qualified name + if (!std.mem.eql(u8, user_facing_text, fully_qualified_text)) { + const user_qualified_ident_idx = try self.env.insertIdent(base.Ident.for_text(user_facing_text)); + try current_scope.introduceTypeAlias(self.env.gpa, user_qualified_ident_idx, type_decl_idx); + } + } + } + } + } + + // Phase 1c: Now process type aliases (which can reference the nominal types registered above) + for (self.parse_ir.store.statementSlice(statements)) |stmt_idx| { + const stmt = self.parse_ir.store.getStatement(stmt_idx); + if (stmt == .type_decl) { + const type_decl = stmt.type_decl; + if (type_decl.kind == .alias) { + try self.processTypeDeclFirstPass(type_decl, parent_name, true); // defer associated blocks + } + } + } + + // Phase 1d: Add user-facing aliases for type aliases too + for (self.parse_ir.store.statementSlice(statements)) |stmt_idx| { + const stmt = self.parse_ir.store.getStatement(stmt_idx); + if (stmt == .type_decl) { + const type_decl = stmt.type_decl; + if (type_decl.kind == .alias) { + const type_header = self.parse_ir.store.getTypeHeader(type_decl.header) catch continue; + const nested_type_ident = self.parse_ir.tokens.resolveIdentifier(type_header.name) orelse continue; + + // Build fully qualified name + const parent_text = self.env.getIdent(parent_name); + const nested_type_text = self.env.getIdent(nested_type_ident); + const fully_qualified_ident_idx = try self.env.insertQualifiedIdent(parent_text, nested_type_text); + + // Look up the type alias + if (self.scopeLookupTypeDecl(fully_qualified_ident_idx)) |type_decl_idx| { + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + + // Build user-facing qualified name by stripping module prefix + const fully_qualified_text = self.env.getIdent(fully_qualified_ident_idx); + const module_prefix = self.env.module_name; + + const user_facing_text = if (std.mem.startsWith(u8, fully_qualified_text, module_prefix) and + fully_qualified_text.len > module_prefix.len and + fully_qualified_text[module_prefix.len] == '.') + fully_qualified_text[module_prefix.len + 1..] + else + fully_qualified_text; + + if (!std.mem.eql(u8, user_facing_text, fully_qualified_text)) { + const user_qualified_ident_idx = try self.env.insertIdent(base.Ident.for_text(user_facing_text)); + try current_scope.introduceTypeAlias(self.env.gpa, user_qualified_ident_idx, type_decl_idx); + } + } + } } } @@ -1174,43 +1282,34 @@ pub fn canonicalizeFile( // Phase 1.5.5: Create placeholders for top-level decls and type annos // This ensures they're available when processing associated blocks - const top_level_stmts = self.parse_ir.store.statementSlice(file.statements); - for (top_level_stmts) |stmt_id| { - const stmt = self.parse_ir.store.getStatement(stmt_id); - switch (stmt) { - .decl => |decl| { - const pattern = self.parse_ir.store.getPattern(decl.pattern); - if (pattern == .ident) { - const pattern_ident_tok = pattern.ident.ident_tok; - if (self.parse_ir.tokens.resolveIdentifier(pattern_ident_tok)) |decl_ident| { - const region = self.parse_ir.tokenizedRegionToRegion(decl.region); - const placeholder_pattern = Pattern{ - .assign = .{ - .ident = decl_ident, - }, - }; - const placeholder_pattern_idx = try self.env.addPattern(placeholder_pattern, region); - try self.placeholder_idents.put(self.env.gpa, decl_ident, {}); - const current_scope = &self.scopes.items[self.scopes.items.len - 1]; - try current_scope.idents.put(self.env.gpa, decl_ident, placeholder_pattern_idx); + // IMPORTANT: Only do this for type-modules, not apps/hosted/etc + // Apps and other module types process their decls normally without placeholders + if (self.env.module_kind == .type_module) { + const top_level_stmts = self.parse_ir.store.statementSlice(file.statements); + for (top_level_stmts) |stmt_id| { + const stmt = self.parse_ir.store.getStatement(stmt_id); + switch (stmt) { + .decl => |decl| { + const pattern = self.parse_ir.store.getPattern(decl.pattern); + if (pattern == .ident) { + const pattern_ident_tok = pattern.ident.ident_tok; + if (self.parse_ir.tokens.resolveIdentifier(pattern_ident_tok)) |decl_ident| { + const region = self.parse_ir.tokenizedRegionToRegion(decl.region); + const placeholder_pattern = Pattern{ + .assign = .{ + .ident = decl_ident, + }, + }; + const placeholder_pattern_idx = try self.env.addPattern(placeholder_pattern, region); + try self.placeholder_idents.put(self.env.gpa, decl_ident, {}); + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + try current_scope.idents.put(self.env.gpa, decl_ident, placeholder_pattern_idx); + } } - } - }, - .type_anno => |type_anno| { - if (self.parse_ir.tokens.resolveIdentifier(type_anno.name)) |anno_ident| { - const region = self.parse_ir.tokenizedRegionToRegion(type_anno.region); - const placeholder_pattern = Pattern{ - .assign = .{ - .ident = anno_ident, - }, - }; - const placeholder_pattern_idx = try self.env.addPattern(placeholder_pattern, region); - try self.placeholder_idents.put(self.env.gpa, anno_ident, {}); - const current_scope = &self.scopes.items[self.scopes.items.len - 1]; - try current_scope.idents.put(self.env.gpa, anno_ident, placeholder_pattern_idx); - } - }, - else => {}, + }, + // .type_anno handling removed - will add back with better solution + else => {}, + } } } @@ -1578,6 +1677,20 @@ fn createAnnoOnlyDef( }; const pattern_idx = try self.env.addPattern(pattern, region); + // Introduce the identifier to scope so it can be referenced + switch (try self.scopeIntroduceInternal(self.env.gpa, .ident, ident, pattern_idx, false, true)) { + .success => {}, + .shadowing_warning => |shadowed_pattern_idx| { + const original_region = self.env.store.getPatternRegion(shadowed_pattern_idx); + try self.env.pushDiagnostic(Diagnostic{ .shadowing_warning = .{ + .ident = ident, + .region = region, + .original_region = original_region, + } }); + }, + else => {}, + } + // Note: We don't update placeholders here. For associated items, the calling code // (processAssociatedItemsSecondPass) will update all three identifiers (qualified, // type-qualified, unqualified). For top-level items, there are no placeholders to update. @@ -7203,39 +7316,139 @@ pub fn canonicalizeBlockStatement(self: *Self, ast_stmt: AST.Statement, ast_stmt switch (next_stmt) { .decl => |decl| { - // Immediately process the next decl, with the annotation - mb_canonicailzed_stmt = try self.canonicalizeBlockDecl(decl, TypeAnnoIdent{ - .name = name_ident, - .anno_idx = type_anno_idx, - .where = where_clauses, - }); - stmts_processed = .two; + // Check if the decl name matches the anno name + const decl_pattern = self.parse_ir.store.getPattern(decl.pattern); + const names_match = name_check: { + if (decl_pattern == .ident) { + if (self.parse_ir.tokens.resolveIdentifier(decl_pattern.ident.ident_tok)) |decl_ident| { + break :name_check name_ident.idx == decl_ident.idx; + } + } + break :name_check false; + }; + + if (names_match) { + // Names match - immediately process the next decl with the annotation + mb_canonicailzed_stmt = try self.canonicalizeBlockDecl(decl, TypeAnnoIdent{ + .name = name_ident, + .anno_idx = type_anno_idx, + .where = where_clauses, + }); + stmts_processed = .two; + } else { + // Names don't match - create anno-only def for this anno + // and let the decl be processed separately in the next iteration + + // Check if a placeholder already exists (from Phase 1.5.5) + const pattern_idx = if (self.isPlaceholder(name_ident)) placeholder_check: { + // Reuse the existing placeholder pattern + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + const existing_pattern = current_scope.idents.get(name_ident) orelse { + // This shouldn't happen, but handle it gracefully + const pattern = Pattern{ + .assign = .{ + .ident = name_ident, + }, + }; + break :placeholder_check try self.env.addPattern(pattern, region); + }; + // Remove from placeholder tracking since we're making it real + _ = self.placeholder_idents.remove(name_ident); + break :placeholder_check existing_pattern; + } else create_new: { + // No placeholder - create new pattern and introduce to scope + const pattern = Pattern{ + .assign = .{ + .ident = name_ident, + }, + }; + const new_pattern_idx = try self.env.addPattern(pattern, region); + + // Introduce the name to scope + switch (try self.scopeIntroduceInternal(self.env.gpa, .ident, name_ident, new_pattern_idx, false, true)) { + .success => {}, + .shadowing_warning => |shadowed_pattern_idx| { + const original_region = self.env.store.getPatternRegion(shadowed_pattern_idx); + try self.env.pushDiagnostic(Diagnostic{ .shadowing_warning = .{ + .ident = name_ident, + .region = region, + .original_region = original_region, + } }); + }, + else => {}, + } + break :create_new new_pattern_idx; + }; + + // Create the e_anno_only expression + const anno_only_expr = try self.env.addExpr(Expr{ .e_anno_only = .{} }, region); + + // Create the annotation structure + const annotation = CIR.Annotation{ + .anno = type_anno_idx, + .where = where_clauses, + }; + const annotation_idx = try self.env.addAnnotation(annotation, region); + + // Add the decl as a def so it gets included in all_defs + const def_idx = try self.env.addDef(.{ + .pattern = pattern_idx, + .expr = anno_only_expr, + .annotation = annotation_idx, + .kind = .let, + }, region); + try self.env.store.addScratchDef(def_idx); + + // Create the statement + const stmt_idx = try self.env.addStatement(Statement{ .s_decl = .{ + .pattern = pattern_idx, + .expr = anno_only_expr, + .anno = annotation_idx, + } }, region); + mb_canonicailzed_stmt = CanonicalizedStatement{ .idx = stmt_idx, .free_vars = null }; + stmts_processed = .one; + } }, else => { // If the next stmt does not match this annotation, // create a Def with an e_anno_only body - // Create the pattern for this def - const pattern = Pattern{ - .assign = .{ - .ident = name_ident, - }, - }; - const pattern_idx = try self.env.addPattern(pattern, region); - - // Introduce the name to scope - switch (try self.scopeIntroduceInternal(self.env.gpa, .ident, name_ident, pattern_idx, false, true)) { - .success => {}, - .shadowing_warning => |shadowed_pattern_idx| { - const original_region = self.env.store.getPatternRegion(shadowed_pattern_idx); - try self.env.pushDiagnostic(Diagnostic{ .shadowing_warning = .{ + // Check if a placeholder already exists (from Phase 1.5.5) + const pattern_idx = if (self.isPlaceholder(name_ident)) placeholder_check2: { + // Reuse the existing placeholder pattern + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + const existing_pattern = current_scope.idents.get(name_ident) orelse { + const pattern = Pattern{ + .assign = .{ + .ident = name_ident, + }, + }; + break :placeholder_check2 try self.env.addPattern(pattern, region); + }; + _ = self.placeholder_idents.remove(name_ident); + break :placeholder_check2 existing_pattern; + } else create_new2: { + const pattern = Pattern{ + .assign = .{ .ident = name_ident, - .region = region, - .original_region = original_region, - } }); - }, - else => {}, - } + }, + }; + const new_pattern_idx = try self.env.addPattern(pattern, region); + + switch (try self.scopeIntroduceInternal(self.env.gpa, .ident, name_ident, new_pattern_idx, false, true)) { + .success => {}, + .shadowing_warning => |shadowed_pattern_idx| { + const original_region = self.env.store.getPatternRegion(shadowed_pattern_idx); + try self.env.pushDiagnostic(Diagnostic{ .shadowing_warning = .{ + .ident = name_ident, + .region = region, + .original_region = original_region, + } }); + }, + else => {}, + } + break :create_new2 new_pattern_idx; + }; // Create the e_anno_only expression const anno_only_expr = try self.env.addExpr(Expr{ .e_anno_only = .{} }, region); @@ -7270,27 +7483,42 @@ pub fn canonicalizeBlockStatement(self: *Self, ast_stmt: AST.Statement, ast_stmt // If the next stmt does not match this annotation, // create a Def with an e_anno_only body - // Create the pattern for this def - const pattern = Pattern{ - .assign = .{ - .ident = name_ident, - }, - }; - const pattern_idx = try self.env.addPattern(pattern, region); - - // Introduce the name to scope - switch (try self.scopeIntroduceInternal(self.env.gpa, .ident, name_ident, pattern_idx, false, true)) { - .success => {}, - .shadowing_warning => |shadowed_pattern_idx| { - const original_region = self.env.store.getPatternRegion(shadowed_pattern_idx); - try self.env.pushDiagnostic(Diagnostic{ .shadowing_warning = .{ + // Check if a placeholder already exists (from Phase 1.5.5) + const pattern_idx = if (self.isPlaceholder(name_ident)) placeholder_check3: { + // Reuse the existing placeholder pattern + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + const existing_pattern = current_scope.idents.get(name_ident) orelse { + const pattern = Pattern{ + .assign = .{ + .ident = name_ident, + }, + }; + break :placeholder_check3 try self.env.addPattern(pattern, region); + }; + _ = self.placeholder_idents.remove(name_ident); + break :placeholder_check3 existing_pattern; + } else create_new3: { + const pattern = Pattern{ + .assign = .{ .ident = name_ident, - .region = region, - .original_region = original_region, - } }); - }, - else => {}, - } + }, + }; + const new_pattern_idx = try self.env.addPattern(pattern, region); + + switch (try self.scopeIntroduceInternal(self.env.gpa, .ident, name_ident, new_pattern_idx, false, true)) { + .success => {}, + .shadowing_warning => |shadowed_pattern_idx| { + const original_region = self.env.store.getPatternRegion(shadowed_pattern_idx); + try self.env.pushDiagnostic(Diagnostic{ .shadowing_warning = .{ + .ident = name_ident, + .region = region, + .original_region = original_region, + } }); + }, + else => {}, + } + break :create_new3 new_pattern_idx; + }; // Create the e_anno_only expression const anno_only_expr = try self.env.addExpr(Expr{ .e_anno_only = .{} }, region); diff --git a/test/snapshots/fuzz_crash/fuzz_crash_023.md b/test/snapshots/fuzz_crash/fuzz_crash_023.md index e546ad9ab1..08e407831e 100644 --- a/test/snapshots/fuzz_crash/fuzz_crash_023.md +++ b/test/snapshots/fuzz_crash/fuzz_crash_023.md @@ -269,6 +269,7 @@ DOES NOT EXIST - fuzz_crash_023.md:193:4:193:13 UNUSED VARIABLE - fuzz_crash_023.md:164:2:164:18 UNUSED VARIABLE - fuzz_crash_023.md:165:2:165:14 UNUSED VARIABLE - fuzz_crash_023.md:178:2:178:8 +UNUSED VARIABLE - fuzz_crash_023.md:178:47:178:71 UNUSED VARIABLE - fuzz_crash_023.md:180:2:180:17 UNUSED VARIABLE - fuzz_crash_023.md:188:2:188:15 UNUSED VARIABLE - fuzz_crash_023.md:189:2:189:23 @@ -846,6 +847,18 @@ The unused variable is declared here: ^^^^^^ +**UNUSED VARIABLE** +Variable `qux` is not used anywhere in your code. + +If you don't need this variable, prefix it with an underscore like `_qux` to suppress this warning. +The unused variable is declared here: +**fuzz_crash_023.md:178:47:178:71:** +```roc + record = { foo: 123, bar: "Hello", ;az: tag, qux: Ok(world), punned } +``` + ^^^^^^^^^^^^^^^^^^^^^^^^ + + **UNUSED VARIABLE** Variable `multiline_tuple` is not used anywhere in your code. @@ -2211,6 +2224,11 @@ expect { (p-applied-tag))) (value (e-num (value "1000")))))))))) + (d-let + (p-assign (ident "qux")) + (e-anno-only) + (annotation + (ty-malformed))) (d-let (p-assign (ident "main!")) (e-closure @@ -2310,6 +2328,9 @@ expect { (p-assign (ident "tag")))) (s-expr (e-runtime-error (tag "expr_not_canonicalized"))) + (s-let + (p-assign (ident "qux")) + (e-anno-only)) (s-let (p-assign (ident "tuple")) (e-tuple @@ -2554,6 +2575,7 @@ expect { (patt (type "Bool -> num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) (patt (type "Num(Int(Unsigned64)) -> Num(Int(Unsigned64))")) (patt (type "[Red][Blue, Green]_others, _arg -> Error")) + (patt (type "Error")) (patt (type "List(Error) -> Error")) (patt (type "{}")) (patt (type "Error"))) @@ -2600,6 +2622,7 @@ expect { (expr (type "Bool -> num where [num.from_int_digits : List(U8) -> Try(num, [OutOfRange])]")) (expr (type "Num(Int(Unsigned64)) -> Num(Int(Unsigned64))")) (expr (type "[Red][Blue, Green]_others, _arg -> Error")) + (expr (type "Error")) (expr (type "List(Error) -> Error")) (expr (type "{}")) (expr (type "Error")))) diff --git a/test/snapshots/nominal/nominal_associated_alias_within_block.md b/test/snapshots/nominal/nominal_associated_alias_within_block.md index 6e1b60b4a5..60da9d276d 100644 --- a/test/snapshots/nominal/nominal_associated_alias_within_block.md +++ b/test/snapshots/nominal/nominal_associated_alias_within_block.md @@ -85,15 +85,15 @@ external = Foo.defaultBaz ~~~clojure (can-ir (d-let - (p-assign (ident "Foo.defaultBaz")) - (e-nominal (nominal "Foo.Bar") + (p-assign (ident "nominal_associated_alias_within_block.Foo.defaultBaz")) + (e-nominal (nominal "nominal_associated_alias_within_block.Foo.Bar") (e-tag (name "X"))) (annotation (ty-lookup (name "Foo.Baz") (local)))) (d-let (p-assign (ident "external")) (e-lookup-local - (p-assign (ident "Foo.defaultBaz"))) + (p-assign (ident "nominal_associated_alias_within_block.Foo.defaultBaz"))) (annotation (ty-lookup (name "Foo.Baz") (local)))) (s-nominal-decl @@ -101,29 +101,29 @@ external = Foo.defaultBaz (ty-tag-union (ty-tag-name (name "Whatever")))) (s-nominal-decl - (ty-header (name "Foo.Bar")) + (ty-header (name "nominal_associated_alias_within_block.Foo.Bar")) (ty-tag-union (ty-tag-name (name "X")) (ty-tag-name (name "Y")) (ty-tag-name (name "Z")))) (s-alias-decl - (ty-header (name "Foo.Baz")) + (ty-header (name "nominal_associated_alias_within_block.Foo.Baz")) (ty-lookup (name "Foo.Bar") (local)))) ~~~ # TYPES ~~~clojure (inferred-types (defs - (patt (type "Foo.Baz")) - (patt (type "Foo.Baz"))) + (patt (type "nominal_associated_alias_within_block.Foo.Baz")) + (patt (type "nominal_associated_alias_within_block.Foo.Baz"))) (type_decls (nominal (type "Foo") (ty-header (name "Foo"))) - (nominal (type "Foo.Bar") - (ty-header (name "Foo.Bar"))) - (alias (type "Foo.Baz") - (ty-header (name "Foo.Baz")))) + (nominal (type "nominal_associated_alias_within_block.Foo.Bar") + (ty-header (name "nominal_associated_alias_within_block.Foo.Bar"))) + (alias (type "nominal_associated_alias_within_block.Foo.Baz") + (ty-header (name "nominal_associated_alias_within_block.Foo.Baz")))) (expressions - (expr (type "Foo.Baz")) - (expr (type "Foo.Baz")))) + (expr (type "nominal_associated_alias_within_block.Foo.Baz")) + (expr (type "nominal_associated_alias_within_block.Foo.Baz")))) ~~~ diff --git a/test/snapshots/nominal/nominal_associated_type_alias.md b/test/snapshots/nominal/nominal_associated_type_alias.md index bc17622bf2..7e61cf3bde 100644 --- a/test/snapshots/nominal/nominal_associated_type_alias.md +++ b/test/snapshots/nominal/nominal_associated_type_alias.md @@ -76,7 +76,7 @@ useMyBar = Foo.Bar.X (can-ir (d-let (p-assign (ident "useMyBar")) - (e-nominal (nominal "Foo.Bar") + (e-nominal (nominal "nominal_associated_type_alias.Foo.Bar") (e-tag (name "X"))) (annotation (ty-lookup (name "MyBar") (local)))) @@ -85,7 +85,7 @@ useMyBar = Foo.Bar.X (ty-tag-union (ty-tag-name (name "Whatever")))) (s-nominal-decl - (ty-header (name "Foo.Bar")) + (ty-header (name "nominal_associated_type_alias.Foo.Bar")) (ty-tag-union (ty-tag-name (name "X")) (ty-tag-name (name "Y")) @@ -102,8 +102,8 @@ useMyBar = Foo.Bar.X (type_decls (nominal (type "Foo") (ty-header (name "Foo"))) - (nominal (type "Foo.Bar") - (ty-header (name "Foo.Bar"))) + (nominal (type "nominal_associated_type_alias.Foo.Bar") + (ty-header (name "nominal_associated_type_alias.Foo.Bar"))) (alias (type "MyBar") (ty-header (name "MyBar")))) (expressions diff --git a/test/snapshots/nominal/nominal_associated_value_alias.md b/test/snapshots/nominal/nominal_associated_value_alias.md index a6cc6247db..a915622393 100644 --- a/test/snapshots/nominal/nominal_associated_value_alias.md +++ b/test/snapshots/nominal/nominal_associated_value_alias.md @@ -74,12 +74,12 @@ result = myBar ~~~clojure (can-ir (d-let - (p-assign (ident "Foo.bar")) + (p-assign (ident "nominal_associated_value_alias.Foo.bar")) (e-num (value "42"))) (d-let (p-assign (ident "myBar")) (e-lookup-local - (p-assign (ident "Foo.bar"))) + (p-assign (ident "nominal_associated_value_alias.Foo.bar"))) (annotation (ty-lookup (name "U64") (builtin)))) (d-let diff --git a/test/snapshots/nominal/nominal_associated_vs_module.md b/test/snapshots/nominal/nominal_associated_vs_module.md index 6f2966099f..86b223880c 100644 --- a/test/snapshots/nominal/nominal_associated_vs_module.md +++ b/test/snapshots/nominal/nominal_associated_vs_module.md @@ -93,7 +93,7 @@ useBar = Something (ty-tag-union (ty-tag-name (name "Whatever")))) (s-nominal-decl - (ty-header (name "Foo.Bar")) + (ty-header (name "nominal_associated_vs_module.Foo.Bar")) (ty-tag-union (ty-tag-name (name "Something"))))) ~~~ @@ -101,12 +101,12 @@ useBar = Something ~~~clojure (inferred-types (defs - (patt (type "Foo.Bar"))) + (patt (type "nominal_associated_vs_module.Foo.Bar"))) (type_decls (nominal (type "Foo") (ty-header (name "Foo"))) - (nominal (type "Foo.Bar") - (ty-header (name "Foo.Bar")))) + (nominal (type "nominal_associated_vs_module.Foo.Bar") + (ty-header (name "nominal_associated_vs_module.Foo.Bar")))) (expressions - (expr (type "Foo.Bar")))) + (expr (type "nominal_associated_vs_module.Foo.Bar")))) ~~~ diff --git a/test/snapshots/nominal/nominal_associated_with_final_expression.md b/test/snapshots/nominal/nominal_associated_with_final_expression.md index 1027264d5e..ada1b3f2de 100644 --- a/test/snapshots/nominal/nominal_associated_with_final_expression.md +++ b/test/snapshots/nominal/nominal_associated_with_final_expression.md @@ -59,7 +59,7 @@ Foo := [A, B, C].{ ~~~clojure (can-ir (d-let - (p-assign (ident "Foo.x")) + (p-assign (ident "nominal_associated_with_final_expression.Foo.x")) (e-num (value "5"))) (s-nominal-decl (ty-header (name "Foo")) diff --git a/test/snapshots/nominal/nominal_deeply_nested_tag.md b/test/snapshots/nominal/nominal_deeply_nested_tag.md index 2798850cf8..ddca910133 100644 --- a/test/snapshots/nominal/nominal_deeply_nested_tag.md +++ b/test/snapshots/nominal/nominal_deeply_nested_tag.md @@ -78,7 +78,7 @@ x = Foo.Bar.Baz.X (can-ir (d-let (p-assign (ident "x")) - (e-nominal (nominal "Foo.Bar.Baz") + (e-nominal (nominal "nominal_deeply_nested_tag.Foo.Bar.Baz") (e-tag (name "X"))) (annotation (ty-lookup (name "Foo.Bar.Baz") (local)))) @@ -87,11 +87,11 @@ x = Foo.Bar.Baz.X (ty-tag-union (ty-tag-name (name "Whatever")))) (s-nominal-decl - (ty-header (name "Foo.Bar")) + (ty-header (name "nominal_deeply_nested_tag.Foo.Bar")) (ty-tag-union (ty-tag-name (name "Something")))) (s-nominal-decl - (ty-header (name "Foo.Bar.Baz")) + (ty-header (name "nominal_deeply_nested_tag.Foo.Bar.Baz")) (ty-tag-union (ty-tag-name (name "X")) (ty-tag-name (name "Y")) @@ -101,14 +101,14 @@ x = Foo.Bar.Baz.X ~~~clojure (inferred-types (defs - (patt (type "Foo.Bar.Baz"))) + (patt (type "nominal_deeply_nested_tag.Foo.Bar.Baz"))) (type_decls (nominal (type "Foo") (ty-header (name "Foo"))) - (nominal (type "Foo.Bar") - (ty-header (name "Foo.Bar"))) - (nominal (type "Foo.Bar.Baz") - (ty-header (name "Foo.Bar.Baz")))) + (nominal (type "nominal_deeply_nested_tag.Foo.Bar") + (ty-header (name "nominal_deeply_nested_tag.Foo.Bar"))) + (nominal (type "nominal_deeply_nested_tag.Foo.Bar.Baz") + (ty-header (name "nominal_deeply_nested_tag.Foo.Bar.Baz")))) (expressions - (expr (type "Foo.Bar.Baz")))) + (expr (type "nominal_deeply_nested_tag.Foo.Bar.Baz")))) ~~~ diff --git a/test/snapshots/nominal/nominal_four_level_nested_tag.md b/test/snapshots/nominal/nominal_four_level_nested_tag.md index daca2687a2..861124ef5a 100644 --- a/test/snapshots/nominal/nominal_four_level_nested_tag.md +++ b/test/snapshots/nominal/nominal_four_level_nested_tag.md @@ -91,7 +91,7 @@ value = Foo.Bar.Baz.Qux.Y (can-ir (d-let (p-assign (ident "value")) - (e-nominal (nominal "Foo.Bar.Baz.Qux") + (e-nominal (nominal "nominal_four_level_nested_tag.Foo.Bar.Baz.Qux") (e-tag (name "Y"))) (annotation (ty-lookup (name "Foo.Bar.Baz.Qux") (local)))) @@ -100,15 +100,15 @@ value = Foo.Bar.Baz.Qux.Y (ty-tag-union (ty-tag-name (name "A")))) (s-nominal-decl - (ty-header (name "Foo.Bar")) + (ty-header (name "nominal_four_level_nested_tag.Foo.Bar")) (ty-tag-union (ty-tag-name (name "B")))) (s-nominal-decl - (ty-header (name "Foo.Bar.Baz")) + (ty-header (name "nominal_four_level_nested_tag.Foo.Bar.Baz")) (ty-tag-union (ty-tag-name (name "C")))) (s-nominal-decl - (ty-header (name "Foo.Bar.Baz.Qux")) + (ty-header (name "nominal_four_level_nested_tag.Foo.Bar.Baz.Qux")) (ty-tag-union (ty-tag-name (name "X")) (ty-tag-name (name "Y")) @@ -118,16 +118,16 @@ value = Foo.Bar.Baz.Qux.Y ~~~clojure (inferred-types (defs - (patt (type "Foo.Bar.Baz.Qux"))) + (patt (type "nominal_four_level_nested_tag.Foo.Bar.Baz.Qux"))) (type_decls (nominal (type "Foo") (ty-header (name "Foo"))) - (nominal (type "Foo.Bar") - (ty-header (name "Foo.Bar"))) - (nominal (type "Foo.Bar.Baz") - (ty-header (name "Foo.Bar.Baz"))) - (nominal (type "Foo.Bar.Baz.Qux") - (ty-header (name "Foo.Bar.Baz.Qux")))) + (nominal (type "nominal_four_level_nested_tag.Foo.Bar") + (ty-header (name "nominal_four_level_nested_tag.Foo.Bar"))) + (nominal (type "nominal_four_level_nested_tag.Foo.Bar.Baz") + (ty-header (name "nominal_four_level_nested_tag.Foo.Bar.Baz"))) + (nominal (type "nominal_four_level_nested_tag.Foo.Bar.Baz.Qux") + (ty-header (name "nominal_four_level_nested_tag.Foo.Bar.Baz.Qux")))) (expressions - (expr (type "Foo.Bar.Baz.Qux")))) + (expr (type "nominal_four_level_nested_tag.Foo.Bar.Baz.Qux")))) ~~~ diff --git a/test/snapshots/nominal/nominal_nested_type_ref.md b/test/snapshots/nominal/nominal_nested_type_ref.md index 1ec77095fe..9a3adc519f 100644 --- a/test/snapshots/nominal/nominal_nested_type_ref.md +++ b/test/snapshots/nominal/nominal_nested_type_ref.md @@ -65,7 +65,7 @@ x = Foo.Bar.X (can-ir (d-let (p-assign (ident "x")) - (e-nominal (nominal "Foo.Bar") + (e-nominal (nominal "nominal_nested_type_ref.Foo.Bar") (e-tag (name "X"))) (annotation (ty-lookup (name "Foo.Bar") (local)))) @@ -74,7 +74,7 @@ x = Foo.Bar.X (ty-tag-union (ty-tag-name (name "Whatever")))) (s-nominal-decl - (ty-header (name "Foo.Bar")) + (ty-header (name "nominal_nested_type_ref.Foo.Bar")) (ty-tag-union (ty-tag-name (name "X")) (ty-tag-name (name "Y")) @@ -84,12 +84,12 @@ x = Foo.Bar.X ~~~clojure (inferred-types (defs - (patt (type "Foo.Bar"))) + (patt (type "nominal_nested_type_ref.Foo.Bar"))) (type_decls (nominal (type "Foo") (ty-header (name "Foo"))) - (nominal (type "Foo.Bar") - (ty-header (name "Foo.Bar")))) + (nominal (type "nominal_nested_type_ref.Foo.Bar") + (ty-header (name "nominal_nested_type_ref.Foo.Bar")))) (expressions - (expr (type "Foo.Bar")))) + (expr (type "nominal_nested_type_ref.Foo.Bar")))) ~~~ diff --git a/test/snapshots/nominal/nominal_simple_nested_tag.md b/test/snapshots/nominal/nominal_simple_nested_tag.md index 130da115ab..9878867430 100644 --- a/test/snapshots/nominal/nominal_simple_nested_tag.md +++ b/test/snapshots/nominal/nominal_simple_nested_tag.md @@ -60,14 +60,14 @@ x = Foo.Bar.X (can-ir (d-let (p-assign (ident "x")) - (e-nominal (nominal "Foo.Bar") + (e-nominal (nominal "nominal_simple_nested_tag.Foo.Bar") (e-tag (name "X")))) (s-nominal-decl (ty-header (name "Foo")) (ty-tag-union (ty-tag-name (name "Whatever")))) (s-nominal-decl - (ty-header (name "Foo.Bar")) + (ty-header (name "nominal_simple_nested_tag.Foo.Bar")) (ty-tag-union (ty-tag-name (name "X")) (ty-tag-name (name "Y")) @@ -77,12 +77,12 @@ x = Foo.Bar.X ~~~clojure (inferred-types (defs - (patt (type "Foo.Bar"))) + (patt (type "nominal_simple_nested_tag.Foo.Bar"))) (type_decls (nominal (type "Foo") (ty-header (name "Foo"))) - (nominal (type "Foo.Bar") - (ty-header (name "Foo.Bar")))) + (nominal (type "nominal_simple_nested_tag.Foo.Bar") + (ty-header (name "nominal_simple_nested_tag.Foo.Bar")))) (expressions - (expr (type "Foo.Bar")))) + (expr (type "nominal_simple_nested_tag.Foo.Bar")))) ~~~ From 1b1f4777ecb52aa84f40fa7fc2e9cd682d053b2c Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Sat, 8 Nov 2025 15:58:24 -0500 Subject: [PATCH 33/38] Delete an obsolete comment --- src/canonicalize/Can.zig | 1 - 1 file changed, 1 deletion(-) diff --git a/src/canonicalize/Can.zig b/src/canonicalize/Can.zig index f1da9afaeb..62be1d2418 100644 --- a/src/canonicalize/Can.zig +++ b/src/canonicalize/Can.zig @@ -1307,7 +1307,6 @@ pub fn canonicalizeFile( } } }, - // .type_anno handling removed - will add back with better solution else => {}, } } From 06d0543f7bfc1f95de758fd463391924a9cc6421 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Sat, 8 Nov 2025 17:19:34 -0500 Subject: [PATCH 34/38] More fixes --- src/canonicalize/Can.zig | 120 ++++++++++++++++++++++++++------------- 1 file changed, 81 insertions(+), 39 deletions(-) diff --git a/src/canonicalize/Can.zig b/src/canonicalize/Can.zig index 62be1d2418..b84acde148 100644 --- a/src/canonicalize/Can.zig +++ b/src/canonicalize/Can.zig @@ -1280,36 +1280,55 @@ pub fn canonicalizeFile( } } - // Phase 1.5.5: Create placeholders for top-level decls and type annos + // Phase 1.5.5: Create placeholders for top-level decls // This ensures they're available when processing associated blocks // IMPORTANT: Only do this for type-modules, not apps/hosted/etc // Apps and other module types process their decls normally without placeholders - if (self.env.module_kind == .type_module) { - const top_level_stmts = self.parse_ir.store.statementSlice(file.statements); - for (top_level_stmts) |stmt_id| { - const stmt = self.parse_ir.store.getStatement(stmt_id); - switch (stmt) { - .decl => |decl| { - const pattern = self.parse_ir.store.getPattern(decl.pattern); - if (pattern == .ident) { - const pattern_ident_tok = pattern.ident.ident_tok; - if (self.parse_ir.tokens.resolveIdentifier(pattern_ident_tok)) |decl_ident| { - const region = self.parse_ir.tokenizedRegionToRegion(decl.region); + switch (self.env.module_kind) { + .type_module => { + const top_level_stmts = self.parse_ir.store.statementSlice(file.statements); + for (top_level_stmts) |stmt_id| { + const stmt = self.parse_ir.store.getStatement(stmt_id); + switch (stmt) { + .decl => |decl| { + const pattern = self.parse_ir.store.getPattern(decl.pattern); + if (pattern == .ident) { + const pattern_ident_tok = pattern.ident.ident_tok; + if (self.parse_ir.tokens.resolveIdentifier(pattern_ident_tok)) |decl_ident| { + const region = self.parse_ir.tokenizedRegionToRegion(decl.region); + const placeholder_pattern = Pattern{ + .assign = .{ + .ident = decl_ident, + }, + }; + const placeholder_pattern_idx = try self.env.addPattern(placeholder_pattern, region); + try self.placeholder_idents.put(self.env.gpa, decl_ident, {}); + const current_scope = &self.scopes.items[self.scopes.items.len - 1]; + try current_scope.idents.put(self.env.gpa, decl_ident, placeholder_pattern_idx); + } + } + }, + .type_anno => |type_anno| { + // Also create placeholders for top-level type annotations (like list_get_unsafe) + // These are anno-only defs that need to be available in associated blocks + if (self.parse_ir.tokens.resolveIdentifier(type_anno.name)) |anno_ident| { + const region = self.parse_ir.tokenizedRegionToRegion(type_anno.region); const placeholder_pattern = Pattern{ .assign = .{ - .ident = decl_ident, + .ident = anno_ident, }, }; const placeholder_pattern_idx = try self.env.addPattern(placeholder_pattern, region); - try self.placeholder_idents.put(self.env.gpa, decl_ident, {}); + try self.placeholder_idents.put(self.env.gpa, anno_ident, {}); const current_scope = &self.scopes.items[self.scopes.items.len - 1]; - try current_scope.idents.put(self.env.gpa, decl_ident, placeholder_pattern_idx); + try current_scope.idents.put(self.env.gpa, anno_ident, placeholder_pattern_idx); } - } - }, - else => {}, + }, + else => {}, + } } - } + }, + else => {}, } // Phase 1.6: Now process all deferred type declaration associated blocks @@ -1668,27 +1687,50 @@ fn createAnnoOnlyDef( where_clauses: ?WhereClause.Span, region: Region, ) std.mem.Allocator.Error!CIR.Def.Idx { - // Create the pattern for this def - const pattern = Pattern{ - .assign = .{ - .ident = ident, - }, - }; - const pattern_idx = try self.env.addPattern(pattern, region); - - // Introduce the identifier to scope so it can be referenced - switch (try self.scopeIntroduceInternal(self.env.gpa, .ident, ident, pattern_idx, false, true)) { - .success => {}, - .shadowing_warning => |shadowed_pattern_idx| { - const original_region = self.env.store.getPatternRegion(shadowed_pattern_idx); - try self.env.pushDiagnostic(Diagnostic{ .shadowing_warning = .{ + // Check if a placeholder exists for this identifier (from multi-phase canonicalization) + const pattern_idx = if (self.isPlaceholder(ident)) placeholder_check: { + // Use scopeLookup to search up the scope chain for the placeholder + switch (self.scopeLookup(.ident, ident)) { + .found => |existing_pattern| { + // Note: We don't remove from placeholder_idents here. The calling code + // (processAssociatedItemsSecondPass) will call updatePlaceholder to do that. + break :placeholder_check existing_pattern; + }, + .not_found => { + // Placeholder is tracked but not found in any scope - this shouldn't happen + // Create a new pattern as fallback + const pattern = Pattern{ + .assign = .{ + .ident = ident, + }, + }; + break :placeholder_check try self.env.addPattern(pattern, region); + }, + } + } else create_new: { + // No placeholder - create new pattern and introduce to scope + const pattern = Pattern{ + .assign = .{ .ident = ident, - .region = region, - .original_region = original_region, - } }); - }, - else => {}, - } + }, + }; + const new_pattern_idx = try self.env.addPattern(pattern, region); + + // Introduce the identifier to scope so it can be referenced + switch (try self.scopeIntroduceInternal(self.env.gpa, .ident, ident, new_pattern_idx, false, true)) { + .success => {}, + .shadowing_warning => |shadowed_pattern_idx| { + const original_region = self.env.store.getPatternRegion(shadowed_pattern_idx); + try self.env.pushDiagnostic(Diagnostic{ .shadowing_warning = .{ + .ident = ident, + .region = region, + .original_region = original_region, + } }); + }, + else => {}, + } + break :create_new new_pattern_idx; + }; // Note: We don't update placeholders here. For associated items, the calling code // (processAssociatedItemsSecondPass) will update all three identifiers (qualified, From 781166eeb79604ad5cb9c070c47e89471d0afdd4 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Sat, 8 Nov 2025 17:56:23 -0500 Subject: [PATCH 35/38] zig fmt --- src/canonicalize/Can.zig | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/canonicalize/Can.zig b/src/canonicalize/Can.zig index b84acde148..701d1b7e56 100644 --- a/src/canonicalize/Can.zig +++ b/src/canonicalize/Can.zig @@ -1024,9 +1024,9 @@ fn processAssociatedItemsFirstPass( // Check if the fully qualified name starts with the module name const user_facing_text = if (std.mem.startsWith(u8, fully_qualified_text, module_prefix) and - fully_qualified_text.len > module_prefix.len and - fully_qualified_text[module_prefix.len] == '.') - fully_qualified_text[module_prefix.len + 1..] // Skip "module." + fully_qualified_text.len > module_prefix.len and + fully_qualified_text[module_prefix.len] == '.') + fully_qualified_text[module_prefix.len + 1 ..] // Skip "module." else fully_qualified_text; // No module prefix, use as-is @@ -1074,9 +1074,9 @@ fn processAssociatedItemsFirstPass( const module_prefix = self.env.module_name; const user_facing_text = if (std.mem.startsWith(u8, fully_qualified_text, module_prefix) and - fully_qualified_text.len > module_prefix.len and - fully_qualified_text[module_prefix.len] == '.') - fully_qualified_text[module_prefix.len + 1..] + fully_qualified_text.len > module_prefix.len and + fully_qualified_text[module_prefix.len] == '.') + fully_qualified_text[module_prefix.len + 1 ..] else fully_qualified_text; From ba3e667e5b3056a1bfc777958737f6ceff835dce Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Sat, 8 Nov 2025 18:13:28 -0500 Subject: [PATCH 36/38] Fix Ubuntu CI --- src/canonicalize/Can.zig | 137 +++++++++++++----- .../multiline_without_comma/everything.md | 22 +-- test/snapshots/fuzz_crash/fuzz_crash_042.md | 22 +-- .../pass/underscore_in_regular_annotations.md | 27 ++++ 4 files changed, 148 insertions(+), 60 deletions(-) diff --git a/src/canonicalize/Can.zig b/src/canonicalize/Can.zig index 701d1b7e56..6fc2c02515 100644 --- a/src/canonicalize/Can.zig +++ b/src/canonicalize/Can.zig @@ -1280,51 +1280,77 @@ pub fn canonicalizeFile( } } - // Phase 1.5.5: Create placeholders for top-level decls - // This ensures they're available when processing associated blocks - // IMPORTANT: Only do this for type-modules, not apps/hosted/etc - // Apps and other module types process their decls normally without placeholders + // Phase 1.5.5: Process anno-only top-level type annotations EARLY + // For type-modules, anno-only top-level type annotations (like list_get_unsafe) need to be + // processed before associated blocks so they can be referenced inside those blocks + // IMPORTANT: Only process anno-only (no matching decl), and only for type-modules switch (self.env.module_kind) { .type_module => { const top_level_stmts = self.parse_ir.store.statementSlice(file.statements); - for (top_level_stmts) |stmt_id| { + var i: usize = 0; + while (i < top_level_stmts.len) : (i += 1) { + const stmt_id = top_level_stmts[i]; const stmt = self.parse_ir.store.getStatement(stmt_id); - switch (stmt) { - .decl => |decl| { - const pattern = self.parse_ir.store.getPattern(decl.pattern); - if (pattern == .ident) { - const pattern_ident_tok = pattern.ident.ident_tok; - if (self.parse_ir.tokens.resolveIdentifier(pattern_ident_tok)) |decl_ident| { - const region = self.parse_ir.tokenizedRegionToRegion(decl.region); - const placeholder_pattern = Pattern{ - .assign = .{ - .ident = decl_ident, - }, - }; - const placeholder_pattern_idx = try self.env.addPattern(placeholder_pattern, region); - try self.placeholder_idents.put(self.env.gpa, decl_ident, {}); - const current_scope = &self.scopes.items[self.scopes.items.len - 1]; - try current_scope.idents.put(self.env.gpa, decl_ident, placeholder_pattern_idx); + if (stmt == .type_anno) { + const ta = stmt.type_anno; + const name_ident = self.parse_ir.tokens.resolveIdentifier(ta.name) orelse continue; + + // Check if there's a matching decl (skipping malformed statements) + const has_matching_decl = blk: { + var next_i = i + 1; + while (next_i < top_level_stmts.len) : (next_i += 1) { + const next_stmt = self.parse_ir.store.getStatement(top_level_stmts[next_i]); + // Skip malformed statements + if (next_stmt == .malformed) continue; + // Check if this is a matching decl + if (next_stmt == .decl) { + const next_pattern = self.parse_ir.store.getPattern(next_stmt.decl.pattern); + if (next_pattern == .ident) { + if (self.parse_ir.tokens.resolveIdentifier(next_pattern.ident.ident_tok)) |decl_ident| { + break :blk name_ident.idx == decl_ident.idx; + } + } } + // Found a non-malformed, non-matching statement + break :blk false; } - }, - .type_anno => |type_anno| { - // Also create placeholders for top-level type annotations (like list_get_unsafe) - // These are anno-only defs that need to be available in associated blocks - if (self.parse_ir.tokens.resolveIdentifier(type_anno.name)) |anno_ident| { - const region = self.parse_ir.tokenizedRegionToRegion(type_anno.region); - const placeholder_pattern = Pattern{ - .assign = .{ - .ident = anno_ident, - }, - }; - const placeholder_pattern_idx = try self.env.addPattern(placeholder_pattern, region); - try self.placeholder_idents.put(self.env.gpa, anno_ident, {}); - const current_scope = &self.scopes.items[self.scopes.items.len - 1]; - try current_scope.idents.put(self.env.gpa, anno_ident, placeholder_pattern_idx); + // Reached end of statements + break :blk false; + }; + + // Skip if there's a matching decl - it will be processed normally + if (has_matching_decl) continue; + + const region = self.parse_ir.tokenizedRegionToRegion(ta.region); + + // Extract type variables and canonicalize the annotation + const type_vars_top: u32 = @intCast(self.scratch_idents.top()); + try self.extractTypeVarIdentsFromASTAnno(ta.anno, type_vars_top); + const type_var_scope = self.scopeEnterTypeVar(); + defer self.scopeExitTypeVar(type_var_scope); + const type_anno_idx = try self.canonicalizeTypeAnno(ta.anno, .inline_anno); + + // Canonicalize where clauses if present + const where_clauses = if (ta.where) |where_coll| blk: { + const where_slice = self.parse_ir.store.whereClauseSlice(.{ .span = self.parse_ir.store.getCollection(where_coll).span }); + const where_start = self.env.store.scratchWhereClauseTop(); + for (where_slice) |where_idx| { + const canonicalized_where = try self.canonicalizeWhereClause(where_idx, .inline_anno); + try self.env.store.addScratchWhereClause(canonicalized_where); } - }, - else => {}, + break :blk try self.env.store.whereClauseSpanFrom(where_start); + } else null; + + // Create the anno-only def immediately + const def_idx = try self.createAnnoOnlyDef(name_ident, type_anno_idx, where_clauses, region); + try self.env.store.addScratchDef(def_idx); + + // If exposed, register it + const ident_text = self.env.getIdent(name_ident); + if (self.exposed_ident_texts.contains(ident_text)) { + const def_idx_u16: u16 = @intCast(@intFromEnum(def_idx)); + try self.env.setExposedNodeIndexById(name_ident, def_idx_u16); + } } } }, @@ -1485,6 +1511,41 @@ pub fn canonicalizeFile( continue; }; + // For type-modules, check if this is an anno-only annotation that was already processed in Phase 1.5.5 + // We need to check if there's a matching decl - if there isn't, this was processed early + switch (self.env.module_kind) { + .type_module => { + // Check if there's a matching decl (skipping malformed statements) + const has_matching_decl = blk: { + var check_i = i + 1; + while (check_i < ast_stmt_idxs.len) : (check_i += 1) { + const check_stmt = self.parse_ir.store.getStatement(ast_stmt_idxs[check_i]); + // Skip malformed statements + if (check_stmt == .malformed) continue; + // Check if this is a matching decl + if (check_stmt == .decl) { + const check_pattern = self.parse_ir.store.getPattern(check_stmt.decl.pattern); + if (check_pattern == .ident) { + if (self.parse_ir.tokens.resolveIdentifier(check_pattern.ident.ident_tok)) |decl_ident| { + break :blk name_ident.idx == decl_ident.idx; + } + } + } + // Found a non-malformed, non-matching statement + break :blk false; + } + // Reached end of statements + break :blk false; + }; + + // Skip if this is anno-only (no matching decl) - it was processed in Phase 1.5.5 + if (!has_matching_decl) { + continue; + } + }, + else => {}, + } + // First, make the top of our scratch list const type_vars_top: u32 = @intCast(self.scratch_idents.top()); diff --git a/test/snapshots/formatting/multiline_without_comma/everything.md b/test/snapshots/formatting/multiline_without_comma/everything.md index d4209d51d0..49bdfa5597 100644 --- a/test/snapshots/formatting/multiline_without_comma/everything.md +++ b/test/snapshots/formatting/multiline_without_comma/everything.md @@ -1174,6 +1174,17 @@ g : e -> e where module(e).A, module(e).B ^^ +**MALFORMED WHERE CLAUSE** +This where clause could not be parsed correctly. + +**everything.md:56:12:56:17:** +```roc +g : e -> e where module(e).A, module(e).B +``` + ^^^^^ + +Check the syntax of your where clause. + **WHERE CLAUSE NOT ALLOWED IN TYPE DECLARATION** You cannot define a `where` clause inside a type declaration. @@ -1222,17 +1233,6 @@ import I2 exposing [ ``` -**MALFORMED WHERE CLAUSE** -This where clause could not be parsed correctly. - -**everything.md:56:12:56:17:** -```roc -g : e -> e where module(e).A, module(e).B -``` - ^^^^^ - -Check the syntax of your where clause. - **UNUSED VARIABLE** Variable `b` is not used anywhere in your code. diff --git a/test/snapshots/fuzz_crash/fuzz_crash_042.md b/test/snapshots/fuzz_crash/fuzz_crash_042.md index 49c7597570..6a40737f32 100644 --- a/test/snapshots/fuzz_crash/fuzz_crash_042.md +++ b/test/snapshots/fuzz_crash/fuzz_crash_042.md @@ -23,17 +23,6 @@ import u.R}g:r->R.a.E ^ -**MODULE NOT FOUND** -The module `u.R` was not found in this Roc project. - -You're attempting to use this module here: -**fuzz_crash_042.md:1:1:1:11:** -```roc -import u.R}g:r->R.a.E -``` -^^^^^^^^^^ - - **MODULE NOT IMPORTED** There is no module with the name `R.a` imported into this Roc file. @@ -45,6 +34,17 @@ import u.R}g:r->R.a.E ^^^^^ +**MODULE NOT FOUND** +The module `u.R` was not found in this Roc project. + +You're attempting to use this module here: +**fuzz_crash_042.md:1:1:1:11:** +```roc +import u.R}g:r->R.a.E +``` +^^^^^^^^^^ + + # TOKENS ~~~zig KwImport,LowerIdent,NoSpaceDotUpperIdent,CloseCurly,LowerIdent,OpColon,LowerIdent,OpArrow,UpperIdent,NoSpaceDotLowerIdent,NoSpaceDotUpperIdent, diff --git a/test/snapshots/pass/underscore_in_regular_annotations.md b/test/snapshots/pass/underscore_in_regular_annotations.md index 31f635ea1e..424066712d 100644 --- a/test/snapshots/pass/underscore_in_regular_annotations.md +++ b/test/snapshots/pass/underscore_in_regular_annotations.md @@ -77,6 +77,24 @@ process = |list| "processed" ^^^^ +**DUPLICATE DEFINITION** +The name `transform` is being redeclared in this scope. + +The redeclaration is here: +**underscore_in_regular_annotations.md:29:1:29:10:** +```roc +transform = |_, b| b +``` +^^^^^^^^^ + +But `transform` was already defined here: +**underscore_in_regular_annotations.md:28:1:28:21:** +```roc +transform : _a -> _b -> _b +``` +^^^^^^^^^^^^^^^^^^^^ + + # TOKENS ~~~zig LowerIdent,OpColon,Underscore,OpArrow,Underscore, @@ -245,6 +263,13 @@ transform = |_, b| b # CANONICALIZE ~~~clojure (can-ir + (d-let + (p-assign (ident "transform")) + (e-anno-only) + (annotation + (ty-fn (effectful false) + (ty-rigid-var (name "_a")) + (ty-rigid-var (name "_b"))))) (d-let (p-assign (ident "main")) (e-lambda @@ -360,6 +385,7 @@ transform = |_, b| b ~~~clojure (inferred-types (defs + (patt (type "_a -> _b")) (patt (type "_arg -> _ret")) (patt (type "a -> a")) (patt (type "List(_elem) -> Str")) @@ -368,6 +394,7 @@ transform = |_, b| b (patt (type "a -> b, List(a) -> List(b)")) (patt (type "_arg, c -> c"))) (expressions + (expr (type "_a -> _b")) (expr (type "_arg -> _ret")) (expr (type "a -> a")) (expr (type "List(_elem) -> Str")) From 06a159c2f46039a30b0c460b4fc56ec8922417d5 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Sat, 8 Nov 2025 21:09:26 -0500 Subject: [PATCH 37/38] Fix stuff --- src/build/builtin_compiler/main.zig | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/build/builtin_compiler/main.zig b/src/build/builtin_compiler/main.zig index fb2c1dd85d..87b64783fe 100644 --- a/src/build/builtin_compiler/main.zig +++ b/src/build/builtin_compiler/main.zig @@ -644,7 +644,8 @@ fn compileModule( const box_ident = try module_env.insertIdent(base.Ident.for_text("Box")); // Use provided bool_stmt and try_stmt if available, otherwise use undefined - const common_idents: Check.CommonIdents = .{ + // For Builtin module, these will be found after canonicalization and updated before type checking + var common_idents: Check.CommonIdents = .{ .module_name = module_ident, .list = list_ident, .box = box_ident, @@ -770,6 +771,22 @@ fn compileModule( eval_order_ptr.* = eval_order; module_env.evaluation_order = eval_order_ptr; } + + // Find Bool and Try statements before type checking + // When compiling Builtin, bool_stmt and try_stmt are initially undefined, + // but they must be set before type checking begins + const found_bool_stmt = findTypeDeclaration(module_env, "Bool") catch { + std.debug.print("Error: Could not find Bool type in Builtin module\n", .{}); + return error.TypeDeclarationNotFound; + }; + const found_try_stmt = findTypeDeclaration(module_env, "Try") catch { + std.debug.print("Error: Could not find Try type in Builtin module\n", .{}); + return error.TypeDeclarationNotFound; + }; + + // Update common_idents with the found statement indices + common_idents.bool_stmt = found_bool_stmt; + common_idents.try_stmt = found_try_stmt; } // 6. Type check From d0e1d2dd6acb46a1435418802dfa7722e66856cd Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Sat, 8 Nov 2025 21:24:36 -0500 Subject: [PATCH 38/38] Fix Ubuntu CI panic and update snapshots --- .../multiline_without_comma/everything.md | 2 +- test/snapshots/fuzz_crash/fuzz_crash_042.md | 2 +- .../pass/underscore_in_regular_annotations.md | 27 ------------------- 3 files changed, 2 insertions(+), 29 deletions(-) diff --git a/test/snapshots/formatting/multiline_without_comma/everything.md b/test/snapshots/formatting/multiline_without_comma/everything.md index 49bdfa5597..abd7875e90 100644 --- a/test/snapshots/formatting/multiline_without_comma/everything.md +++ b/test/snapshots/formatting/multiline_without_comma/everything.md @@ -197,11 +197,11 @@ PARSE ERROR - everything.md:56:37:56:38 PARSE ERROR - everything.md:56:38:56:39 PARSE ERROR - everything.md:56:39:56:40 PARSE ERROR - everything.md:56:40:56:42 +MALFORMED WHERE CLAUSE - everything.md:56:12:56:17 WHERE CLAUSE NOT ALLOWED IN TYPE DECLARATION - everything.md:12:1:13:7 UNDECLARED TYPE - everything.md:43:5:43:6 MODULE NOT FOUND - everything.md:2:1:5:2 MODULE NOT FOUND - everything.md:6:1:9:2 -MALFORMED WHERE CLAUSE - everything.md:56:12:56:17 UNUSED VARIABLE - everything.md:88:5:88:6 UNUSED VARIABLE - everything.md:93:4:93:5 UNUSED VARIABLE - everything.md:98:5:98:6 diff --git a/test/snapshots/fuzz_crash/fuzz_crash_042.md b/test/snapshots/fuzz_crash/fuzz_crash_042.md index 6a40737f32..537b108063 100644 --- a/test/snapshots/fuzz_crash/fuzz_crash_042.md +++ b/test/snapshots/fuzz_crash/fuzz_crash_042.md @@ -9,8 +9,8 @@ import u.R}g:r->R.a.E ~~~ # EXPECTED PARSE ERROR - fuzz_crash_042.md:1:11:1:12 -MODULE NOT FOUND - fuzz_crash_042.md:1:1:1:11 MODULE NOT IMPORTED - fuzz_crash_042.md:1:17:1:22 +MODULE NOT FOUND - fuzz_crash_042.md:1:1:1:11 # PROBLEMS **PARSE ERROR** A parsing error occurred: `statement_unexpected_token` diff --git a/test/snapshots/pass/underscore_in_regular_annotations.md b/test/snapshots/pass/underscore_in_regular_annotations.md index 424066712d..31f635ea1e 100644 --- a/test/snapshots/pass/underscore_in_regular_annotations.md +++ b/test/snapshots/pass/underscore_in_regular_annotations.md @@ -77,24 +77,6 @@ process = |list| "processed" ^^^^ -**DUPLICATE DEFINITION** -The name `transform` is being redeclared in this scope. - -The redeclaration is here: -**underscore_in_regular_annotations.md:29:1:29:10:** -```roc -transform = |_, b| b -``` -^^^^^^^^^ - -But `transform` was already defined here: -**underscore_in_regular_annotations.md:28:1:28:21:** -```roc -transform : _a -> _b -> _b -``` -^^^^^^^^^^^^^^^^^^^^ - - # TOKENS ~~~zig LowerIdent,OpColon,Underscore,OpArrow,Underscore, @@ -263,13 +245,6 @@ transform = |_, b| b # CANONICALIZE ~~~clojure (can-ir - (d-let - (p-assign (ident "transform")) - (e-anno-only) - (annotation - (ty-fn (effectful false) - (ty-rigid-var (name "_a")) - (ty-rigid-var (name "_b"))))) (d-let (p-assign (ident "main")) (e-lambda @@ -385,7 +360,6 @@ transform = |_, b| b ~~~clojure (inferred-types (defs - (patt (type "_a -> _b")) (patt (type "_arg -> _ret")) (patt (type "a -> a")) (patt (type "List(_elem) -> Str")) @@ -394,7 +368,6 @@ transform = |_, b| b (patt (type "a -> b, List(a) -> List(b)")) (patt (type "_arg, c -> c"))) (expressions - (expr (type "_a -> _b")) (expr (type "_arg -> _ret")) (expr (type "a -> a")) (expr (type "List(_elem) -> Str"))