From d5e5e2bd7de5da8299c6cdc040ef52254dbdcfe3 Mon Sep 17 00:00:00 2001 From: Richard Feldman Date: Sat, 8 Nov 2025 11:21:25 -0500 Subject: [PATCH] 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 1904f0646f..0fb042bf49 100644 --- a/src/canonicalize/Can.zig +++ b/src/canonicalize/Can.zig @@ -46,6 +46,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), @@ -561,154 +562,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 @@ -796,7 +815,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) { @@ -888,10 +907,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 } @@ -927,10 +950,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); } @@ -1031,6 +1058,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]; @@ -1047,7 +1078,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 = .{ @@ -1056,6 +1086,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]; @@ -1165,13 +1199,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 @@ -1179,59 +1215,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| { @@ -1254,8 +1240,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) { @@ -1263,13 +1249,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; @@ -4842,13 +4843,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 @@ -8338,8 +8339,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, @@ -8347,14 +8356,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 "{}")))) ~~~