Skip to content

Commit cf267de

Browse files
committed
Merge remote-tracking branch 'origin/main' into builtin-str2
2 parents 4462c6e + 127fd2a commit cf267de

File tree

14 files changed

+652
-396
lines changed

14 files changed

+652
-396
lines changed

src/canonicalize/Can.zig

Lines changed: 411 additions & 55 deletions
Large diffs are not rendered by default.

src/canonicalize/Diagnostic.zig

Lines changed: 60 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,11 @@ pub const Diagnostic = union(enum) {
118118
type_name: Ident.Idx,
119119
region: Region,
120120
},
121+
type_from_missing_module: struct {
122+
module_name: Ident.Idx,
123+
type_name: Ident.Idx,
124+
region: Region,
125+
},
121126
module_not_imported: struct {
122127
module_name: Ident.Idx,
123128
region: Region,
@@ -269,6 +274,7 @@ pub const Diagnostic = union(enum) {
269274
.module_not_found => |d| d.region,
270275
.value_not_exposed => |d| d.region,
271276
.type_not_exposed => |d| d.region,
277+
.type_from_missing_module => |d| d.region,
272278
.module_not_imported => |d| d.region,
273279
.too_many_exports => |d| d.region,
274280
.undeclared_type => |d| d.region,
@@ -889,9 +895,19 @@ pub const Diagnostic = union(enum) {
889895
) !Report {
890896
var report = Report.init(allocator, "UNDECLARED TYPE", .runtime_error);
891897
const owned_type_name = try report.addOwnedString(type_name);
892-
try report.document.addReflowingText("The type ");
893-
try report.document.addType(owned_type_name);
894-
try report.document.addReflowingText(" is not declared in this scope.");
898+
899+
// Check if this looks like a qualified type (contains dots)
900+
const has_dots = std.mem.indexOfScalar(u8, type_name, '.') != null;
901+
902+
if (has_dots) {
903+
try report.document.addReflowingText("Cannot resolve qualified type ");
904+
try report.document.addType(owned_type_name);
905+
try report.document.addReflowingText(".");
906+
} else {
907+
try report.document.addReflowingText("The type ");
908+
try report.document.addType(owned_type_name);
909+
try report.document.addReflowingText(" is not declared in this scope.");
910+
}
895911
try report.document.addLineBreak();
896912
try report.document.addLineBreak();
897913

@@ -1411,13 +1427,32 @@ pub const Diagnostic = union(enum) {
14111427

14121428
const owned_module = try report.addOwnedString(module_name);
14131429
const owned_type = try report.addOwnedString(type_name);
1414-
try report.document.addReflowingText("The ");
1415-
try report.document.addModuleName(owned_module);
1416-
try report.document.addReflowingText(" module does not expose anything named ");
1417-
try report.document.addType(owned_type);
1418-
try report.document.addReflowingText(".");
1419-
try report.document.addLineBreak();
1420-
try report.document.addReflowingText("Make sure the module exports this type, or use a type that is exposed.");
1430+
1431+
// Check if trying to access a type with the same name as the module (e.g., Result.Result)
1432+
const is_same_name = std.mem.eql(u8, module_name, type_name);
1433+
1434+
if (is_same_name) {
1435+
// Special message for Result.Result, Color.Color, etc.
1436+
const qualified_name = try std.fmt.allocPrint(allocator, "{s}.{s}", .{ module_name, type_name });
1437+
defer allocator.free(qualified_name);
1438+
const owned_qualified = try report.addOwnedString(qualified_name);
1439+
1440+
try report.document.addReflowingText("There is no ");
1441+
try report.document.addType(owned_qualified);
1442+
try report.document.addReflowingText(" type.");
1443+
try report.document.addLineBreak();
1444+
try report.document.addLineBreak();
1445+
} else {
1446+
// Standard message for other cases (e.g., Color.RGB where Color is a nominal type)
1447+
const qualified_name = try std.fmt.allocPrint(allocator, "{s}.{s}", .{ module_name, type_name });
1448+
defer allocator.free(qualified_name);
1449+
const owned_qualified = try report.addOwnedString(qualified_name);
1450+
1451+
try report.document.addType(owned_qualified);
1452+
try report.document.addReflowingText(" does not exist.");
1453+
try report.document.addLineBreak();
1454+
try report.document.addLineBreak();
1455+
}
14211456

14221457
const owned_filename = try report.addOwnedString(filename);
14231458
try report.document.addSourceRegion(
@@ -1428,6 +1463,21 @@ pub const Diagnostic = union(enum) {
14281463
line_starts,
14291464
);
14301465

1466+
// Add tip at the end
1467+
try report.document.addLineBreak();
1468+
if (is_same_name) {
1469+
try report.document.addReflowingText("There is a ");
1470+
try report.document.addModuleName(owned_module);
1471+
try report.document.addReflowingText(" module, but it does not have a ");
1472+
try report.document.addType(owned_type);
1473+
try report.document.addReflowingText(" type nested inside it.");
1474+
} else {
1475+
try report.document.addType(owned_module);
1476+
try report.document.addReflowingText(" is a valid type, but it does not have an associated ");
1477+
try report.document.addType(owned_type);
1478+
try report.document.addReflowingText(".");
1479+
}
1480+
14311481
return report;
14321482
}
14331483

src/canonicalize/Node.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ pub const Tag = enum {
188188
diag_module_not_found,
189189
diag_value_not_exposed,
190190
diag_type_not_exposed,
191+
diag_type_from_missing_module,
191192
diag_module_not_imported,
192193
diag_too_many_exports,
193194
diag_nominal_type_redeclared,

src/canonicalize/NodeStore.zig

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ pub fn deinit(store: *NodeStore) void {
128128
/// when adding/removing variants from ModuleEnv unions. Update these when modifying the unions.
129129
///
130130
/// Count of the diagnostic nodes in the ModuleEnv
131-
pub const MODULEENV_DIAGNOSTIC_NODE_COUNT = 56;
131+
pub const MODULEENV_DIAGNOSTIC_NODE_COUNT = 57;
132132
/// Count of the expression nodes in the ModuleEnv
133133
pub const MODULEENV_EXPR_NODE_COUNT = 33;
134134
/// Count of the statement nodes in the ModuleEnv
@@ -2831,6 +2831,12 @@ pub fn addDiagnostic(store: *NodeStore, reason: CIR.Diagnostic) Allocator.Error!
28312831
node.data_1 = @as(u32, @bitCast(r.module_name));
28322832
node.data_2 = @as(u32, @bitCast(r.type_name));
28332833
},
2834+
.type_from_missing_module => |r| {
2835+
node.tag = .diag_type_from_missing_module;
2836+
region = r.region;
2837+
node.data_1 = @as(u32, @bitCast(r.module_name));
2838+
node.data_2 = @as(u32, @bitCast(r.type_name));
2839+
},
28342840
.module_not_imported => |r| {
28352841
node.tag = .diag_module_not_imported;
28362842
region = r.region;
@@ -3083,6 +3089,11 @@ pub fn getDiagnostic(store: *const NodeStore, diagnostic: CIR.Diagnostic.Idx) CI
30833089
.type_name = @as(base.Ident.Idx, @bitCast(node.data_2)),
30843090
.region = store.getRegionAt(node_idx),
30853091
} },
3092+
.diag_type_from_missing_module => return CIR.Diagnostic{ .type_from_missing_module = .{
3093+
.module_name = @as(base.Ident.Idx, @bitCast(node.data_1)),
3094+
.type_name = @as(base.Ident.Idx, @bitCast(node.data_2)),
3095+
.region = store.getRegionAt(node_idx),
3096+
} },
30863097
.diag_module_not_imported => return CIR.Diagnostic{ .module_not_imported = .{
30873098
.module_name = @as(base.Ident.Idx, @bitCast(node.data_1)),
30883099
.region = store.getRegionAt(node_idx),

src/canonicalize/Scope.zig

Lines changed: 51 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,37 @@ const collections = @import("collections");
77
const CIR = @import("CIR.zig");
88

99
const Ident = base.Ident;
10+
const Region = base.Region;
1011

1112
const Scope = @This();
1213

14+
/// Represents a type binding for a type imported from an external module.
15+
/// Contains all necessary information to resolve the type from the imported module.
16+
pub const ExternalTypeBinding = struct {
17+
module_ident: Ident.Idx,
18+
original_ident: Ident.Idx,
19+
target_node_idx: ?u16,
20+
import_idx: ?CIR.Import.Idx,
21+
origin_region: Region,
22+
/// True if the module was attempted to be imported but was not found.
23+
/// This allows us to emit a more specific diagnostic when the type is used.
24+
module_not_found: bool,
25+
};
26+
27+
/// A unified type binding that can represent either a locally declared type or an externally imported type.
28+
/// This is the single source of truth for all type resolution in a scope.
29+
pub const TypeBinding = union(enum) {
30+
local_nominal: CIR.Statement.Idx,
31+
local_alias: CIR.Statement.Idx,
32+
associated_nominal: CIR.Statement.Idx,
33+
external_nominal: ExternalTypeBinding,
34+
};
35+
1336
/// Maps an Ident to a Pattern in the Can IR
1437
idents: std.AutoHashMapUnmanaged(Ident.Idx, CIR.Pattern.Idx),
1538
aliases: std.AutoHashMapUnmanaged(Ident.Idx, CIR.Pattern.Idx),
16-
/// Maps type names to their type declaration statements
17-
type_decls: std.AutoHashMapUnmanaged(Ident.Idx, CIR.Statement.Idx),
18-
/// Maps unqualified type names to their fully qualified Statement.Idx (for associated types)
19-
/// Example: within Foo's associated block, "Bar" -> statement for "Foo.Bar"
20-
type_aliases: std.AutoHashMapUnmanaged(Ident.Idx, CIR.Statement.Idx),
39+
/// Canonical bindings for type names (local, auto-imported, and imported types)
40+
type_bindings: std.AutoHashMapUnmanaged(Ident.Idx, TypeBinding),
2141
/// Maps type variables to their type annotation indices
2242
type_vars: std.AutoHashMapUnmanaged(Ident.Idx, CIR.TypeAnno.Idx),
2343
/// Maps module alias names to their full module names
@@ -33,8 +53,7 @@ pub fn init(is_function_boundary: bool) Scope {
3353
return Scope{
3454
.idents = std.AutoHashMapUnmanaged(Ident.Idx, CIR.Pattern.Idx){},
3555
.aliases = std.AutoHashMapUnmanaged(Ident.Idx, CIR.Pattern.Idx){},
36-
.type_decls = std.AutoHashMapUnmanaged(Ident.Idx, CIR.Statement.Idx){},
37-
.type_aliases = std.AutoHashMapUnmanaged(Ident.Idx, CIR.Statement.Idx){},
56+
.type_bindings = std.AutoHashMapUnmanaged(Ident.Idx, TypeBinding){},
3857
.type_vars = std.AutoHashMapUnmanaged(Ident.Idx, CIR.TypeAnno.Idx){},
3958
.module_aliases = std.AutoHashMapUnmanaged(Ident.Idx, Ident.Idx){},
4059
.exposed_items = std.AutoHashMapUnmanaged(Ident.Idx, ExposedItemInfo){},
@@ -47,8 +66,7 @@ pub fn init(is_function_boundary: bool) Scope {
4766
pub fn deinit(self: *Scope, gpa: std.mem.Allocator) void {
4867
self.idents.deinit(gpa);
4968
self.aliases.deinit(gpa);
50-
self.type_decls.deinit(gpa);
51-
self.type_aliases.deinit(gpa);
69+
self.type_bindings.deinit(gpa);
5270
self.type_vars.deinit(gpa);
5371
self.module_aliases.deinit(gpa);
5472
self.exposed_items.deinit(gpa);
@@ -157,20 +175,18 @@ pub const ImportedModuleIntroduceResult = union(enum) {
157175
};
158176

159177
/// Item kinds in a scope
160-
pub const ItemKind = enum { ident, alias, type_decl, type_var, module_alias, exposed_item };
178+
pub const ItemKind = enum { ident, alias, type_var, module_alias, exposed_item };
161179

162180
/// Get the appropriate map for the given item kind
163181
pub fn items(scope: *Scope, comptime item_kind: ItemKind) switch (item_kind) {
164182
.ident, .alias => *std.AutoHashMapUnmanaged(Ident.Idx, CIR.Pattern.Idx),
165-
.type_decl => *std.AutoHashMapUnmanaged(Ident.Idx, CIR.Statement.Idx),
166183
.type_var => *std.AutoHashMapUnmanaged(Ident.Idx, CIR.TypeAnno.Idx),
167184
.module_alias => *std.AutoHashMapUnmanaged(Ident.Idx, Ident.Idx),
168185
.exposed_item => *std.AutoHashMapUnmanaged(Ident.Idx, ExposedItemInfo),
169186
} {
170187
return switch (item_kind) {
171188
.ident => &scope.idents,
172189
.alias => &scope.aliases,
173-
.type_decl => &scope.type_decls,
174190
.type_var => &scope.type_vars,
175191
.module_alias => &scope.module_aliases,
176192
.exposed_item => &scope.exposed_items,
@@ -180,15 +196,13 @@ pub fn items(scope: *Scope, comptime item_kind: ItemKind) switch (item_kind) {
180196
/// Get the appropriate map for the given item kind (const version)
181197
pub fn itemsConst(scope: *const Scope, comptime item_kind: ItemKind) switch (item_kind) {
182198
.ident, .alias => *const std.AutoHashMapUnmanaged(Ident.Idx, CIR.Pattern.Idx),
183-
.type_decl => *const std.AutoHashMapUnmanaged(Ident.Idx, CIR.Statement.Idx),
184199
.type_var => *const std.AutoHashMapUnmanaged(Ident.Idx, CIR.TypeAnno.Idx),
185200
.module_alias => *const std.AutoHashMapUnmanaged(Ident.Idx, Ident.Idx),
186201
.exposed_item => *const std.AutoHashMapUnmanaged(Ident.Idx, ExposedItemInfo),
187202
} {
188203
return switch (item_kind) {
189204
.ident => &scope.idents,
190205
.alias => &scope.aliases,
191-
.type_decl => &scope.type_decls,
192206
.type_var => &scope.type_vars,
193207
.module_alias => &scope.module_aliases,
194208
.exposed_item => &scope.exposed_items,
@@ -198,7 +212,6 @@ pub fn itemsConst(scope: *const Scope, comptime item_kind: ItemKind) switch (ite
198212
/// Put an item in the scope, panics on OOM
199213
pub fn put(scope: *Scope, gpa: std.mem.Allocator, comptime item_kind: ItemKind, name: Ident.Idx, value: switch (item_kind) {
200214
.ident, .alias => CIR.Pattern.Idx,
201-
.type_decl => CIR.Statement.Idx,
202215
.type_var => CIR.TypeAnno.Idx,
203216
.module_alias => Ident.Idx,
204217
.exposed_item => ExposedItemInfo,
@@ -214,13 +227,14 @@ pub fn introduceTypeDecl(
214227
type_decl: CIR.Statement.Idx,
215228
parent_lookup_fn: ?fn (Ident.Idx) ?CIR.Statement.Idx,
216229
) std.mem.Allocator.Error!TypeIntroduceResult {
217-
// Check if already exists in current scope by comparing text content
218-
var iter = scope.type_decls.iterator();
219-
while (iter.next()) |entry| {
220-
if (name.idx == entry.key_ptr.idx) {
221-
// Type redeclaration is an error, not just a warning
222-
return TypeIntroduceResult{ .redeclared_error = entry.value_ptr.* };
223-
}
230+
// Check if type already exists in this scope
231+
if (scope.type_bindings.getPtr(name)) |existing| {
232+
return switch (existing.*) {
233+
.local_nominal => |stmt| TypeIntroduceResult{ .redeclared_error = stmt },
234+
.local_alias => |stmt| TypeIntroduceResult{ .type_alias_redeclared = stmt },
235+
.associated_nominal => |stmt| TypeIntroduceResult{ .nominal_type_redeclared = stmt },
236+
.external_nominal => TypeIntroduceResult{ .nominal_type_redeclared = type_decl },
237+
};
224238
}
225239

226240
// Check for shadowing in parent scopes and issue warnings
@@ -229,7 +243,8 @@ pub fn introduceTypeDecl(
229243
shadowed_stmt = lookup_fn(name);
230244
}
231245

232-
try scope.put(gpa, .type_decl, name, type_decl);
246+
// Add type binding (single source of truth)
247+
try scope.type_bindings.put(gpa, name, TypeBinding{ .local_nominal = type_decl });
233248

234249
if (shadowed_stmt) |stmt| {
235250
return TypeIntroduceResult{ .shadowing_warning = stmt };
@@ -238,26 +253,6 @@ pub fn introduceTypeDecl(
238253
return TypeIntroduceResult{ .success = {} };
239254
}
240255

241-
/// Lookup a type declaration in the scope hierarchy
242-
/// TODO: Optimize lookup performance - currently O(n) due to text comparison
243-
/// TODO: Consider caching or using a more efficient data structure for type lookup
244-
/// TODO: Support for nominal vs structural type distinction (future := operator)
245-
pub fn lookupTypeDecl(scope: *const Scope, name: Ident.Idx) TypeLookupResult {
246-
// Search by comparing text content, not identifier index
247-
var iter = scope.type_decls.iterator();
248-
while (iter.next()) |entry| {
249-
if (name.idx == entry.key_ptr.idx) {
250-
return TypeLookupResult{ .found = entry.value_ptr.* };
251-
}
252-
}
253-
return TypeLookupResult{ .not_found = {} };
254-
}
255-
256-
/// Look up an unqualified type alias (for associated types)
257-
pub fn lookupTypeAlias(scope: *const Scope, name: Ident.Idx) ?CIR.Statement.Idx {
258-
return scope.type_aliases.get(name);
259-
}
260-
261256
/// Introduce an unqualified type alias (for associated types)
262257
/// Maps an unqualified name to a fully qualified type declaration
263258
pub fn introduceTypeAlias(
@@ -266,7 +261,9 @@ pub fn introduceTypeAlias(
266261
unqualified_name: Ident.Idx,
267262
qualified_type_decl: CIR.Statement.Idx,
268263
) !void {
269-
try scope.type_aliases.put(gpa, unqualified_name, qualified_type_decl);
264+
try scope.type_bindings.put(gpa, unqualified_name, TypeBinding{
265+
.associated_nominal = qualified_type_decl,
266+
});
270267
}
271268

272269
/// Update an existing type declaration in the scope
@@ -278,17 +275,17 @@ pub fn updateTypeDecl(
278275
name: Ident.Idx,
279276
new_type_decl: CIR.Statement.Idx,
280277
) std.mem.Allocator.Error!void {
281-
// Find the existing entry by comparing text content
282-
var iter = scope.type_decls.iterator();
283-
while (iter.next()) |entry| {
284-
if (name.idx == entry.key_ptr.idx) {
285-
// Update the existing entry with the new statement index
286-
entry.value_ptr.* = new_type_decl;
287-
return;
288-
}
278+
if (scope.type_bindings.getPtr(name)) |binding_ptr| {
279+
const current = binding_ptr.*;
280+
binding_ptr.* = switch (current) {
281+
.local_nominal => TypeBinding{ .local_nominal = new_type_decl },
282+
.local_alias => TypeBinding{ .local_alias = new_type_decl },
283+
.associated_nominal => TypeBinding{ .associated_nominal = new_type_decl },
284+
.external_nominal => current,
285+
};
286+
} else {
287+
try scope.type_bindings.put(gpa, name, TypeBinding{ .local_nominal = new_type_decl });
289288
}
290-
// If not found, add it as a new entry
291-
try scope.put(gpa, .type_decl, name, new_type_decl);
292289
}
293290

294291
/// Introduce a type variable into the scope

src/canonicalize/test/node_store_test.zig

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -790,6 +790,14 @@ test "NodeStore round trip - Diagnostics" {
790790
},
791791
});
792792

793+
try diagnostics.append(gpa, CIR.Diagnostic{
794+
.type_from_missing_module = .{
795+
.module_name = rand_ident_idx(),
796+
.type_name = rand_ident_idx(),
797+
.region = rand_region(),
798+
},
799+
});
800+
793801
// Test the round-trip for all diagnostics
794802
for (diagnostics.items) |diagnostic| {
795803
const idx = try store.addDiagnostic(diagnostic);

src/parse/AST.zig

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -822,6 +822,9 @@ pub const Statement = union(enum) {
822822
qualifier_tok: ?Token.Idx,
823823
alias_tok: ?Token.Idx,
824824
exposes: ExposedItem.Span,
825+
/// True when importing like `import json.Parser.Config` where Config is auto-exposed
826+
/// but Parser should not become an alias (unlike `import json.Parser exposing [Config]`)
827+
nested_import: bool,
825828
region: TokenizedRegion,
826829
},
827830
type_decl: struct {

src/parse/NodeStore.zig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1172,6 +1172,7 @@ pub fn getStatement(store: *const NodeStore, statement_idx: AST.Statement.Idx) A
11721172
.start = exposes_start,
11731173
.len = exposes_len,
11741174
} },
1175+
.nested_import = false,
11751176
.region = node.region,
11761177
} };
11771178
},

0 commit comments

Comments
 (0)