@@ -561,6 +561,12 @@ fn processTypeDeclFirstPass(
561561 if (! defer_associated_blocks ) {
562562 if (type_decl .associated ) | assoc | {
563563 try self .processAssociatedBlock (qualified_name_idx , type_header .name , assoc );
564+
565+ // For type modules, expose all associated items after processing
566+ // This populates exposed_items with fully qualified names
567+ if (self .env .module_kind == .type_module ) {
568+ try self .exposeAssociatedItems (qualified_name_idx , type_decl );
569+ }
564570 }
565571 }
566572}
@@ -916,6 +922,12 @@ fn processAssociatedItemsSecondPass(
916922 const def_idx_u16 : u16 = @intCast (@intFromEnum (def_idx ));
917923 try self .env .setExposedNodeIndexById (decl_ident , def_idx_u16 );
918924
925+ // Also register by the type-qualified name for type modules
926+ // This is needed so lookups like "Builtin.Str.is_empty" work
927+ if (self .env .module_kind == .type_module ) {
928+ try self .env .setExposedNodeIndexById (qualified_idx , def_idx_u16 );
929+ }
930+
919931 // Make the real pattern available in current scope (replaces placeholder)
920932 // We already added unqualified and type-qualified names earlier,
921933 // but need to update them to point to the real pattern instead of placeholder.
@@ -962,6 +974,12 @@ fn processAssociatedItemsSecondPass(
962974 const def_idx_u16 : u16 = @intCast (@intFromEnum (def_idx ));
963975 try self .env .setExposedNodeIndexById (name_ident , def_idx_u16 );
964976
977+ // Also register by the type-qualified name for type modules
978+ // This is needed so lookups like "Builtin.Str.is_empty" work
979+ if (self .env .module_kind == .type_module ) {
980+ try self .env .setExposedNodeIndexById (qualified_idx , def_idx_u16 );
981+ }
982+
965983 // Make the real pattern available in current scope (replaces placeholder)
966984 const def_cir = self .env .store .getDef (def_idx );
967985 const pattern_idx = def_cir .pattern ;
@@ -1908,7 +1926,25 @@ pub fn validateForChecking(self: *Self) std.mem.Allocator.Error!void {
19081926 // Store the matching type ident in module_kind if found
19091927 if (matching_type_ident ) | type_ident | {
19101928 main_type_ident .* = type_ident ;
1911- // The main type and associated items are already exposed in canonicalize()
1929+
1930+ // Expose all associated items for the main type
1931+ // This populates exposed_items with fully qualified names like "Builtin.Str.is_empty"
1932+ const file = self .parse_ir .store .getFile ();
1933+ for (self .parse_ir .store .statementSlice (file .statements )) | stmt_id | {
1934+ const stmt = self .parse_ir .store .getStatement (stmt_id );
1935+ if (stmt == .type_decl ) {
1936+ const type_decl = stmt .type_decl ;
1937+ const header = self .parse_ir .store .getTypeHeader (type_decl .header ) catch continue ;
1938+ const type_name_ident = self .parse_ir .tokens .resolveIdentifier (header .name ) orelse continue ;
1939+
1940+ // Check if this is the main type
1941+ if (@as (u32 , @bitCast (type_name_ident )) == @as (u32 , @bitCast (type_ident ))) {
1942+ // Expose all associated items of the main type
1943+ try self .exposeAssociatedItems (type_ident , type_decl );
1944+ break ;
1945+ }
1946+ }
1947+ }
19121948 }
19131949
19141950 // Valid if either we have a valid main! or a matching type declaration
@@ -2275,8 +2311,13 @@ fn populateExports(self: *Self) std.mem.Allocator.Error!void {
22752311 const pattern = self .env .store .getPattern (def .pattern );
22762312
22772313 if (pattern == .assign ) {
2278- const is_exposed = self .env .common .exposed_items .containsById (self .env .gpa , @bitCast (pattern .assign .ident ));
2279- // Check if this definition's identifier is in the exposed items
2314+ // For type modules, use the deterministic "nested under main type" rule
2315+ // For regular modules, check the exposed_items map
2316+ const is_exposed = if (self .env .module_kind == .type_module )
2317+ self .env .isExposedInTypeModule (pattern .assign .ident )
2318+ else
2319+ self .env .common .exposed_items .containsById (self .env .gpa , @bitCast (pattern .assign .ident ));
2320+
22802321 if (is_exposed ) {
22812322 // Add this definition to the exports scratch space
22822323 try self .env .store .addScratchDef (def_idx );
@@ -3498,25 +3539,23 @@ pub fn canonicalizeExpr(
34983539 if (envs_map .get (module_name )) | auto_imported_type | {
34993540 const module_env = auto_imported_type .env ;
35003541
3501- // For nested types (e.g., Bool inside Builtin), build the full qualified name
3502- // For regular module imports (e.g., A), just use the field name directly
3542+ // For auto-imported nested types (e.g., Bool, Str inside Builtin),
3543+ // use fully qualified names because they share a CommonEnv.
3544+ // For user-defined type modules, use unqualified names.
35033545 const lookup_name : []const u8 = if (auto_imported_type .statement_idx ) | _ | blk_name : {
3504- // Auto-imported nested type: build "Builtin.Bool.not" using the module's actual name
3505- // Associated items are stored with the full qualified parent type name
3546+ // Auto-imported nested type: build "Builtin.Str.is_empty"
3547+ // This prevents collisions in the shared exposed_items map
35063548 const parent_qualified_idx = try self .env .insertQualifiedIdent (module_env .module_name , module_text );
35073549 const parent_qualified_text = self .env .getIdent (parent_qualified_idx );
35083550 const fully_qualified_idx = try self .env .insertQualifiedIdent (parent_qualified_text , field_text );
35093551 break :blk_name self .env .getIdent (fully_qualified_idx );
35103552 } else field_text ;
35113553
3512- // Look up the associated item by its qualified name
3513- const qname_ident = module_env .common .findIdent (lookup_name ) orelse {
3514- // Identifier not found - just return null
3515- // The error will be handled by the code below that checks target_node_idx_opt
3554+ if (module_env .common .findIdent (lookup_name )) | target_ident | {
3555+ break :blk module_env .getExposedNodeIndexById (target_ident );
3556+ } else {
35163557 break :blk null ;
3517- };
3518- const node_idx = module_env .getExposedNodeIndexById (qname_ident );
3519- break :blk node_idx ;
3558+ }
35203559 } else {
35213560 break :blk null ;
35223561 }
@@ -8565,7 +8604,14 @@ fn checkScopeForUnusedVariables(self: *Self, scope: *const Scope) std.mem.Alloca
85658604 }
85668605
85678606 // Skip if this identifier is exposed (implicitly used in type modules)
8568- if (self .env .common .exposed_items .containsById (self .env .gpa , @bitCast (ident_idx ))) {
8607+ // For type modules, use the deterministic "nested under main type" rule
8608+ // For regular modules, check the exposed_items map
8609+ const is_exposed = if (self .env .module_kind == .type_module )
8610+ self .env .isExposedInTypeModule (ident_idx )
8611+ else
8612+ self .env .common .exposed_items .containsById (self .env .gpa , @bitCast (ident_idx ));
8613+
8614+ if (is_exposed ) {
85698615 continue ;
85708616 }
85718617
@@ -9663,7 +9709,8 @@ fn exposeAssociatedItems(self: *Self, parent_name: Ident.Idx, type_decl: std.met
96639709 const decl_text = self .env .getIdent (decl_ident );
96649710 const qualified_idx = try self .env .insertQualifiedIdent (parent_text , decl_text );
96659711
9666- // Expose the declaration
9712+ // Node index was already set during definition processing
9713+ // Just ensure qualified name is in exposed_items
96679714 try self .env .addExposedById (qualified_idx );
96689715 }
96699716 }
@@ -9676,9 +9723,8 @@ fn exposeAssociatedItems(self: *Self, parent_name: Ident.Idx, type_decl: std.met
96769723 const anno_text = self .env .getIdent (anno_ident );
96779724 const qualified_idx = try self .env .insertQualifiedIdent (parent_text , anno_text );
96789725
9679- // Expose the qualified name
9680- // The unqualified name is added to scope but doesn't need to be in exposed_items
9681- // because lookups use the qualified name
9726+ // Node index was already set during definition processing
9727+ // Just ensure qualified name is in exposed_items
96829728 try self .env .addExposedById (qualified_idx );
96839729 }
96849730 },
0 commit comments