diff --git a/src/base/CommonEnv.zig b/src/base/CommonEnv.zig index bad52f14c7a..3971f50cf21 100644 --- a/src/base/CommonEnv.zig +++ b/src/base/CommonEnv.zig @@ -190,7 +190,7 @@ pub fn setNodeIndexById(self: *CommonEnv, gpa: std.mem.Allocator, ident_idx: Ide pub fn getRegionInfo(self: *const CommonEnv, region: Region) !RegionInfo { return RegionInfo.position( self.source, - self.line_starts.items.items, + self.line_starts.items(), region.start.offset, region.end.offset, ); @@ -213,7 +213,7 @@ pub fn calcRegionInfo(self: *const CommonEnv, region: Region) RegionInfo { const info = RegionInfo.position( source, - self.line_starts.items.items, + self.line_starts.items(), region.start.offset, region.end.offset, ) catch { @@ -256,7 +256,7 @@ pub fn calcLineStarts(self: *CommonEnv, gpa: std.mem.Allocator) !void { /// Returns all line start positions for source code position mapping. pub fn getLineStartsAll(self: *const CommonEnv) []const u32 { - return self.line_starts.items.items; + return self.line_starts.items(); } /// Get the source text for a given region @@ -267,9 +267,10 @@ pub fn getSource(self: *const CommonEnv, region: Region) []const u8 { /// Get the source line for a given region pub fn getSourceLine(self: *const CommonEnv, region: Region) ![]const u8 { const region_info = try self.getRegionInfo(region); - const line_start = self.line_starts.items.items[region_info.start_line_idx]; - const line_end = if (region_info.start_line_idx + 1 < self.line_starts.items.items.len) - self.line_starts.items.items[region_info.start_line_idx + 1] + const line_starts_slice = self.line_starts.items(); + const line_start = line_starts_slice[region_info.start_line_idx]; + const line_end = if (region_info.start_line_idx + 1 < line_starts_slice.len) + line_starts_slice[region_info.start_line_idx + 1] else self.source.len; @@ -329,10 +330,10 @@ test "CommonEnv.Serialized roundtrip" { try testing.expectEqualStrings("world", env.getIdent(world_idx)); try testing.expectEqual(@as(usize, 1), env.exposed_items.count()); - try testing.expectEqual(@as(usize, 3), env.line_starts.len()); - try testing.expectEqual(@as(u32, 0), env.line_starts.items.items[0]); - try testing.expectEqual(@as(u32, 10), env.line_starts.items.items[1]); - try testing.expectEqual(@as(u32, 20), env.line_starts.items.items[2]); + try testing.expectEqual(@as(u32, 3), env.line_starts.len()); + try testing.expectEqual(@as(u32, 0), env.line_starts.items()[0]); + try testing.expectEqual(@as(u32, 10), env.line_starts.items()[1]); + try testing.expectEqual(@as(u32, 20), env.line_starts.items()[2]); try testing.expectEqualStrings(source, env.source); } @@ -474,11 +475,11 @@ test "CommonEnv.Serialized roundtrip with large data" { try testing.expectEqualStrings("string_literal_24", env.getString(string_indices.items[24])); // Verify line starts - try testing.expectEqual(@as(u32, 0), env.line_starts.items.items[0]); + try testing.expectEqual(@as(u32, 0), env.line_starts.items()[0]); // Calculate the actual expected value for the second line start const first_line = "Line 0: This is a test line with some content\n"; const expected_second_line_start = first_line.len; - try testing.expectEqual(@as(u32, expected_second_line_start), env.line_starts.items.items[1]); + try testing.expectEqual(@as(u32, expected_second_line_start), env.line_starts.items()[1]); } test "CommonEnv.Serialized roundtrip with special characters" { diff --git a/src/base/RegionInfo.zig b/src/base/RegionInfo.zig index 7b163970648..a8822829581 100644 --- a/src/base/RegionInfo.zig +++ b/src/base/RegionInfo.zig @@ -124,17 +124,17 @@ test "lineIdx" { _ = try line_starts.append(gpa, 20); _ = try line_starts.append(gpa, 30); - try std.testing.expectEqual(0, RegionInfo.lineIdx(line_starts.items.items, 0)); - try std.testing.expectEqual(0, RegionInfo.lineIdx(line_starts.items.items, 5)); - try std.testing.expectEqual(0, RegionInfo.lineIdx(line_starts.items.items, 9)); - try std.testing.expectEqual(1, RegionInfo.lineIdx(line_starts.items.items, 10)); - try std.testing.expectEqual(1, RegionInfo.lineIdx(line_starts.items.items, 15)); - try std.testing.expectEqual(1, RegionInfo.lineIdx(line_starts.items.items, 19)); - try std.testing.expectEqual(2, RegionInfo.lineIdx(line_starts.items.items, 20)); - try std.testing.expectEqual(2, RegionInfo.lineIdx(line_starts.items.items, 25)); - try std.testing.expectEqual(2, RegionInfo.lineIdx(line_starts.items.items, 29)); - try std.testing.expectEqual(3, RegionInfo.lineIdx(line_starts.items.items, 30)); - try std.testing.expectEqual(3, RegionInfo.lineIdx(line_starts.items.items, 35)); + try std.testing.expectEqual(0, RegionInfo.lineIdx(line_starts.items(), 0)); + try std.testing.expectEqual(0, RegionInfo.lineIdx(line_starts.items(), 5)); + try std.testing.expectEqual(0, RegionInfo.lineIdx(line_starts.items(), 9)); + try std.testing.expectEqual(1, RegionInfo.lineIdx(line_starts.items(), 10)); + try std.testing.expectEqual(1, RegionInfo.lineIdx(line_starts.items(), 15)); + try std.testing.expectEqual(1, RegionInfo.lineIdx(line_starts.items(), 19)); + try std.testing.expectEqual(2, RegionInfo.lineIdx(line_starts.items(), 20)); + try std.testing.expectEqual(2, RegionInfo.lineIdx(line_starts.items(), 25)); + try std.testing.expectEqual(2, RegionInfo.lineIdx(line_starts.items(), 29)); + try std.testing.expectEqual(3, RegionInfo.lineIdx(line_starts.items(), 30)); + try std.testing.expectEqual(3, RegionInfo.lineIdx(line_starts.items(), 35)); } test "columnIdx" { @@ -146,12 +146,12 @@ test "columnIdx" { _ = try line_starts.append(gpa, 10); _ = try line_starts.append(gpa, 20); - try std.testing.expectEqual(0, RegionInfo.columnIdx(line_starts.items.items, 0, 0)); - try std.testing.expectEqual(5, RegionInfo.columnIdx(line_starts.items.items, 0, 5)); - try std.testing.expectEqual(9, RegionInfo.columnIdx(line_starts.items.items, 0, 9)); + try std.testing.expectEqual(0, RegionInfo.columnIdx(line_starts.items(), 0, 0)); + try std.testing.expectEqual(5, RegionInfo.columnIdx(line_starts.items(), 0, 5)); + try std.testing.expectEqual(9, RegionInfo.columnIdx(line_starts.items(), 0, 9)); - try std.testing.expectEqual(0, RegionInfo.columnIdx(line_starts.items.items, 1, 10)); - try std.testing.expectEqual(5, RegionInfo.columnIdx(line_starts.items.items, 1, 15)); + try std.testing.expectEqual(0, RegionInfo.columnIdx(line_starts.items(), 1, 10)); + try std.testing.expectEqual(5, RegionInfo.columnIdx(line_starts.items(), 1, 15)); } test "getLineText" { @@ -165,10 +165,10 @@ test "getLineText" { _ = try line_starts.append(gpa, 6); _ = try line_starts.append(gpa, 12); - try std.testing.expectEqualStrings("line0", RegionInfo.getLineText(source, line_starts.items.items, 0, 0)); - try std.testing.expectEqualStrings("line1", RegionInfo.getLineText(source, line_starts.items.items, 1, 1)); - try std.testing.expectEqualStrings("line0\nline1", RegionInfo.getLineText(source, line_starts.items.items, 0, 1)); - try std.testing.expectEqualStrings("line2", RegionInfo.getLineText(source, line_starts.items.items, 2, 2)); + try std.testing.expectEqualStrings("line0", RegionInfo.getLineText(source, line_starts.items(), 0, 0)); + try std.testing.expectEqualStrings("line1", RegionInfo.getLineText(source, line_starts.items(), 1, 1)); + try std.testing.expectEqualStrings("line0\nline1", RegionInfo.getLineText(source, line_starts.items(), 0, 1)); + try std.testing.expectEqualStrings("line2", RegionInfo.getLineText(source, line_starts.items(), 2, 2)); } test "get" { @@ -182,17 +182,17 @@ test "get" { _ = try line_starts.append(gpa, 6); _ = try line_starts.append(gpa, 12); - const info1 = try RegionInfo.position(source, line_starts.items.items, 2, 4); + const info1 = try RegionInfo.position(source, line_starts.items(), 2, 4); try std.testing.expectEqual(0, info1.start_line_idx); try std.testing.expectEqual(2, info1.start_col_idx); try std.testing.expectEqual(0, info1.end_line_idx); try std.testing.expectEqual(4, info1.end_col_idx); - try std.testing.expectEqualStrings("line0", info1.calculateLineText(source, line_starts.items.items)); + try std.testing.expectEqualStrings("line0", info1.calculateLineText(source, line_starts.items())); - const info2 = try RegionInfo.position(source, line_starts.items.items, 8, 10); + const info2 = try RegionInfo.position(source, line_starts.items(), 8, 10); try std.testing.expectEqual(1, info2.start_line_idx); try std.testing.expectEqual(2, info2.start_col_idx); try std.testing.expectEqual(1, info2.end_line_idx); try std.testing.expectEqual(4, info2.end_col_idx); - try std.testing.expectEqualStrings("line1", info2.calculateLineText(source, line_starts.items.items)); + try std.testing.expectEqualStrings("line1", info2.calculateLineText(source, line_starts.items())); } diff --git a/src/base/SmallStringInterner.zig b/src/base/SmallStringInterner.zig index 64d8537b17f..1fb7132f837 100644 --- a/src/base/SmallStringInterner.zig +++ b/src/base/SmallStringInterner.zig @@ -56,9 +56,9 @@ pub fn initCapacity(gpa: std.mem.Allocator, capacity: usize) std.mem.Allocator.E // Initialize hash table with all zeros (Idx.unused) self.hash_table = try collections.SafeList(Idx).initCapacity(gpa, hash_table_capacity); - try self.hash_table.items.ensureTotalCapacityPrecise(gpa, hash_table_capacity); - self.hash_table.items.items.len = hash_table_capacity; - @memset(self.hash_table.items.items, .unused); + try self.hash_table.ensureTotalCapacityPrecise(gpa, hash_table_capacity); + self.hash_table.setLen(@intCast(hash_table_capacity)); + @memset(self.hash_table.rawCapacitySlice(), .unused); return self; } @@ -77,8 +77,11 @@ pub fn findStringOrSlot(self: *const SmallStringInterner, string: []const u8) st const table_size = self.hash_table.len(); var slot: usize = @intCast(hash % table_size); + const hash_table_slice = self.hash_table.items(); + const bytes_slice = self.bytes.items(); + while (true) { - const idx_at_slot = self.hash_table.items.items[slot]; + const idx_at_slot = hash_table_slice[slot]; if (idx_at_slot == .unused) { // Empty slot - string not found @@ -92,9 +95,9 @@ pub fn findStringOrSlot(self: *const SmallStringInterner, string: []const u8) st // If the stored string would have had to go past the end of bytes, // they must not be equal. Also if there isn't a null terminator // right where we expect, they must not be equal. - if (stored_end < self.bytes.len() and self.bytes.items.items[stored_end] == 0) { + if (stored_end < self.bytes.len() and bytes_slice[stored_end] == 0) { // With that out of the way, we can safely compare the string contents. - if (std.mem.eql(u8, string, self.bytes.items.items[stored_idx..stored_end])) { + if (std.mem.eql(u8, string, bytes_slice[stored_idx..stored_end])) { // Found the string! return .{ .idx = idx_at_slot, .slot = slot }; } @@ -112,17 +115,17 @@ fn resizeHashTable(self: *SmallStringInterner, gpa: std.mem.Allocator) std.mem.A // Create new hash table initialized to zeros self.hash_table = try collections.SafeList(Idx).initCapacity(gpa, new_size); - try self.hash_table.items.ensureTotalCapacityPrecise(gpa, new_size); - self.hash_table.items.items.len = new_size; - @memset(self.hash_table.items.items, .unused); + try self.hash_table.ensureTotalCapacityPrecise(gpa, new_size); + self.hash_table.setLen(@intCast(new_size)); + @memset(self.hash_table.rawCapacitySlice(), .unused); // Rehash all existing entries - for (old_table.items.items) |idx| { + for (old_table.items()) |idx| { if (idx != .unused) { // Get the string for this index const string = self.getText(idx); const result = self.findStringOrSlot(string); - self.hash_table.items.items[@intCast(result.slot)] = idx; + self.hash_table.items()[@intCast(result.slot)] = idx; } } @@ -150,7 +153,7 @@ pub fn insert(self: *SmallStringInterner, gpa: std.mem.Allocator, string: []cons _ = try self.bytes.append(gpa, 0); // Add to hash table - self.hash_table.items.items[@intCast(result.slot)] = new_offset; + self.hash_table.items()[@intCast(result.slot)] = new_offset; self.entry_count += 1; return new_offset; @@ -165,7 +168,7 @@ pub fn contains(self: *const SmallStringInterner, string: []const u8) bool { /// Get a reference to the text for an interned string. pub fn getText(self: *const SmallStringInterner, idx: Idx) []u8 { - const bytes_slice = self.bytes.items.items; + const bytes_slice = self.bytes.items(); const start = @intFromEnum(idx); return std.mem.sliceTo(bytes_slice[start..], 0); diff --git a/src/base/StringLiteral.zig b/src/base/StringLiteral.zig index 9c3da1c6370..c683f99610a 100644 --- a/src/base/StringLiteral.zig +++ b/src/base/StringLiteral.zig @@ -67,8 +67,9 @@ pub const Store = struct { /// Get a string literal's text from this `Store`. pub fn get(self: *const Store, idx: Idx) []u8 { const idx_u32: u32 = @intCast(@intFromEnum(idx)); - const str_len = std.mem.bytesAsValue(u32, self.buffer.items.items[idx_u32 - 4 .. idx_u32]).*; - return self.buffer.items.items[idx_u32 .. idx_u32 + str_len]; + const buffer_slice = self.buffer.items(); + const str_len = std.mem.bytesAsValue(u32, buffer_slice[idx_u32 - 4 .. idx_u32]).*; + return buffer_slice[idx_u32 .. idx_u32 + str_len]; } /// Serialize this Store to the given CompactWriter. The resulting Store diff --git a/src/build/builtin_compiler/main.zig b/src/build/builtin_compiler/main.zig index a4c64fc0243..365d0cb3428 100644 --- a/src/build/builtin_compiler/main.zig +++ b/src/build/builtin_compiler/main.zig @@ -400,7 +400,7 @@ fn replaceStrIsEmptyWithLowLevel(env: *ModuleEnv) !std.ArrayList(CIR.Def.Idx) { const extra_start = def_node.data_1; // Update the expr field (at extra_start + 1) - env.store.extra_data.items.items[extra_start + 1] = @intFromEnum(expr_idx); + env.store.extra_data.items()[extra_start + 1] = @intFromEnum(expr_idx); // Track this replaced def index try new_def_indices.append(gpa, def_idx); @@ -839,8 +839,7 @@ fn compileModule( // Copy old def indices var i: u32 = 0; while (i < old_def_count) : (i += 1) { - const idx = @as(collections.SafeList(u32).Idx, @enumFromInt(old_span.start + i)); - const old_def_idx = module_env.store.extra_data.get(idx).*; + const old_def_idx = module_env.store.extra_data.items()[old_span.start + i]; _ = try module_env.store.extra_data.append(gpa, old_def_idx); } diff --git a/src/canonicalize/CIR.zig b/src/canonicalize/CIR.zig index 58a055d2bb2..119b184a69d 100644 --- a/src/canonicalize/CIR.zig +++ b/src/canonicalize/CIR.zig @@ -714,22 +714,28 @@ pub fn fromF64(f: f64) ?RocDec { /// Represents an import statement in a module pub const Import = struct { + /// Import.Idx uses 0-based indexing (consistent with SafeMultiList) pub const Idx = enum(u32) { _ }; /// Sentinel value indicating unresolved import (max u32) pub const UNRESOLVED_MODULE: u32 = std.math.maxInt(u32); + /// Named struct types for SafeMultiList storage + pub const ImportEntry = struct { str_idx: base.StringLiteral.Idx }; + pub const IdentEntry = struct { ident_idx: base.Ident.Idx }; + pub const ResolvedEntry = struct { module_idx: u32 }; + pub const Store = struct { /// Map from interned string idx to Import.Idx for deduplication map: std.AutoHashMapUnmanaged(base.StringLiteral.Idx, Import.Idx) = .{}, - /// List of interned string IDs indexed by Import.Idx - imports: collections.SafeList(base.StringLiteral.Idx) = .{}, + /// List of interned string IDs indexed by Import.Idx (0-based) + imports: collections.SafeMultiList(ImportEntry) = .{}, /// List of interned ident IDs indexed by Import.Idx (parallel to imports) /// Used for efficient index-based lookups instead of string comparison - import_idents: collections.SafeList(base.Ident.Idx) = .{}, + import_idents: collections.SafeMultiList(IdentEntry) = .{}, /// Resolved module indices, parallel to imports list /// Each entry is either a valid module index or UNRESOLVED_MODULE - resolved_modules: collections.SafeList(u32) = .{}, + resolved_modules: collections.SafeMultiList(ResolvedEntry) = .{}, pub fn init() Store { return .{}; @@ -742,6 +748,11 @@ pub const Import = struct { self.resolved_modules.deinit(allocator); } + /// Get the number of imports + pub fn len(self: *const Store) u32 { + return self.imports.len(); + } + /// Get or create an Import.Idx for the given module name. /// The module name is first checked against existing imports by comparing strings. /// New imports are initially unresolved (UNRESOLVED_MODULE). @@ -756,31 +767,31 @@ pub const Import = struct { /// If ident_idx is provided, it will be stored for index-based lookups. pub fn getOrPutWithIdent(self: *Store, allocator: std.mem.Allocator, strings: *base.StringLiteral.Store, module_name: []const u8, ident_idx: ?base.Ident.Idx) !Import.Idx { // First check if we already have this module name by comparing strings - for (self.imports.items.items, 0..) |existing_string_idx, i| { + const str_idx_slice = self.imports.field(.str_idx); + for (str_idx_slice, 0..) |existing_string_idx, i| { const existing_name = strings.get(existing_string_idx); if (std.mem.eql(u8, existing_name, module_name)) { // Found existing import with same name // Update ident if provided and not already set if (ident_idx) |ident| { - if (i < self.import_idents.len()) { - const current = self.import_idents.items.items[i]; - if (current.isNone()) { - self.import_idents.items.items[i] = ident; - } + const list_idx: @TypeOf(self.import_idents).Idx = @enumFromInt(i); + const current = self.import_idents.get(list_idx).ident_idx; + if (current.isNone()) { + self.import_idents.set(list_idx, .{ .ident_idx = ident }); } } - return @as(Import.Idx, @enumFromInt(i)); + return @enumFromInt(i); } } // Not found - create new import const string_idx = try strings.insert(allocator, module_name); - const idx = @as(Import.Idx, @enumFromInt(self.imports.len())); + const idx: Import.Idx = @enumFromInt(self.imports.len()); // Add to both the list and the map, with unresolved module initially - _ = try self.imports.append(allocator, string_idx); - _ = try self.import_idents.append(allocator, ident_idx orelse base.Ident.Idx.NONE); - _ = try self.resolved_modules.append(allocator, Import.UNRESOLVED_MODULE); + _ = try self.imports.append(allocator, .{ .str_idx = string_idx }); + _ = try self.import_idents.append(allocator, .{ .ident_idx = ident_idx orelse base.Ident.Idx.NONE }); + _ = try self.resolved_modules.append(allocator, .{ .module_idx = Import.UNRESOLVED_MODULE }); try self.map.put(allocator, string_idx, idx); return idx; @@ -790,7 +801,7 @@ pub const Import = struct { pub fn getIdentIdx(self: *const Store, import_idx: Import.Idx) ?base.Ident.Idx { const idx = @intFromEnum(import_idx); if (idx >= self.import_idents.len()) return null; - const ident = self.import_idents.items.items[idx]; + const ident = self.import_idents.get(@enumFromInt(idx)).ident_idx; if (ident.isNone()) return null; return ident; } @@ -799,7 +810,7 @@ pub const Import = struct { pub fn getResolvedModule(self: *const Store, import_idx: Import.Idx) ?u32 { const idx = @intFromEnum(import_idx); if (idx >= self.resolved_modules.len()) return null; - const resolved = self.resolved_modules.items.items[idx]; + const resolved = self.resolved_modules.get(@enumFromInt(idx)).module_idx; if (resolved == Import.UNRESOLVED_MODULE) return null; return resolved; } @@ -808,7 +819,7 @@ pub const Import = struct { pub fn setResolvedModule(self: *Store, import_idx: Import.Idx, module_idx: u32) void { const idx = @intFromEnum(import_idx); if (idx < self.resolved_modules.len()) { - self.resolved_modules.items.items[idx] = module_idx; + self.resolved_modules.set(@enumFromInt(idx), .{ .module_idx = module_idx }); } } @@ -821,16 +832,41 @@ pub const Import = struct { /// /// For each import, this finds the module in available_modules whose module_name /// matches the import name and sets the resolved index accordingly. + /// + /// Import name matching rules: + /// - "Builtin" matches "Builtin" (exact match) + /// - "pf.Stdout" matches "Stdout.roc" (strip package prefix, compare base name) + /// - "Stdout" matches "Stdout.roc" (compare base name without .roc) pub fn resolveImports(self: *Store, env: anytype, available_modules: []const *const @import("ModuleEnv.zig")) void { - const import_count: usize = @intCast(self.imports.len()); + const import_count = self.imports.len(); for (0..import_count) |i| { const import_idx: Import.Idx = @enumFromInt(i); - const str_idx = self.imports.items.items[i]; + const list_idx: @TypeOf(self.imports).Idx = @enumFromInt(i); + const str_idx = self.imports.get(list_idx).str_idx; const import_name = env.common.getString(str_idx); + // Extract the base module name from import_name + // e.g., "pf.Stdout" -> "Stdout", "Builtin" -> "Builtin" + const import_base = if (std.mem.lastIndexOfScalar(u8, import_name, '.')) |dot_pos| + import_name[dot_pos + 1 ..] + else + import_name; + // Find matching module in available_modules by comparing module names for (available_modules, 0..) |module_env, module_idx| { - if (std.mem.eql(u8, module_env.module_name, import_name)) { + const module_name = module_env.module_name; + + // Extract the base module name from module_name + // e.g., "Stdout.roc" -> "Stdout", "Builtin" -> "Builtin" + const module_base = if (std.mem.endsWith(u8, module_name, ".roc")) + module_name[0 .. module_name.len - 4] + else + module_name; + + // Try exact match first, then base name match + if (std.mem.eql(u8, module_name, import_name) or + std.mem.eql(u8, module_base, import_base)) + { self.setResolvedModule(import_idx, @intCast(module_idx)); break; } @@ -873,9 +909,9 @@ pub const Import = struct { // Placeholder to match Store size - not serialized // Reserve space for hashmap (3 pointers for unmanaged hashmap internals) map: [3]u64, - imports: collections.SafeList(base.StringLiteral.Idx).Serialized, - import_idents: collections.SafeList(base.Ident.Idx).Serialized, - resolved_modules: collections.SafeList(u32).Serialized, + imports: collections.SafeMultiList(Import.ImportEntry).Serialized, + import_idents: collections.SafeMultiList(Import.IdentEntry).Serialized, + resolved_modules: collections.SafeMultiList(Import.ResolvedEntry).Serialized, /// Serialize a Store into this Serialized struct, appending data to the writer pub fn serialize( @@ -884,11 +920,11 @@ pub const Import = struct { allocator: std.mem.Allocator, writer: *CompactWriter, ) std.mem.Allocator.Error!void { - // Serialize the imports SafeList + // Serialize the imports SafeMultiList try self.imports.serialize(&store.imports, allocator, writer); - // Serialize the import_idents SafeList + // Serialize the import_idents SafeMultiList try self.import_idents.serialize(&store.import_idents, allocator, writer); - // Serialize the resolved_modules SafeList + // Serialize the resolved_modules SafeMultiList try self.resolved_modules.serialize(&store.resolved_modules, allocator, writer); // Set map to all zeros; the space needs to be here, @@ -910,13 +946,14 @@ pub const Import = struct { }; // Pre-allocate the exact capacity needed for the map - const import_count = store.imports.items.items.len; + const import_count = store.imports.len(); try store.map.ensureTotalCapacity(allocator, @intCast(import_count)); // Repopulate the map - we know there's enough capacity since we // are deserializing from a Serialized struct - for (store.imports.items.items, 0..) |string_idx, i| { - const import_idx = @as(Import.Idx, @enumFromInt(i)); + const str_idx_slice = store.imports.field(.str_idx); + for (str_idx_slice, 0..) |string_idx, i| { + const import_idx: Import.Idx = @enumFromInt(i); store.map.putAssumeCapacityNoClobber(string_idx, import_idx); } diff --git a/src/canonicalize/Can.zig b/src/canonicalize/Can.zig index 85d73df0f7c..239bb40251f 100644 --- a/src/canonicalize/Can.zig +++ b/src/canonicalize/Can.zig @@ -2803,7 +2803,7 @@ fn bringIngestedFileIntoScope( // scope.introduce(self: *Scope, comptime item_kind: Level.ItemKind, ident: Ident.Idx) - for (import.exposing.items.items) |exposed| { + for (import.exposing.items()) |exposed| { const exposed_ident = switch (exposed) { .Value => |ident| ident, .Type => |ident| ident, @@ -4069,8 +4069,8 @@ pub fn canonicalizeExpr( } // Check if this is a required identifier from the platform's `requires` clause - const requires_items = self.env.requires_types.items.items; - for (requires_items, 0..) |req, idx| { + const requires_items = self.env.requires_types.items(); + for (requires_items, 1..) |req, idx| { if (req.ident == ident) { // Found a required identifier - create a lookup expression for it const expr_idx = try self.env.addExpr(CIR.Expr{ .e_lookup_required = .{ @@ -6825,21 +6825,21 @@ fn processCollectedTypeVars(self: *Self) std.mem.Allocator.Error!void { defer self.scratch_type_var_problems.clearFrom(problems_start); // Process from the end to avoid index shifting - while (self.scratch_type_var_validation.items.items.len > 0) { + while (self.scratch_type_var_validation.items().len > 0) { // Pop the last item - const last_idx = self.scratch_type_var_validation.items.items.len - 1; - const first_ident = self.scratch_type_var_validation.items.items[last_idx]; + const last_idx = self.scratch_type_var_validation.items().len - 1; + const first_ident = self.scratch_type_var_validation.items()[last_idx]; self.scratch_type_var_validation.items.shrinkRetainingCapacity(last_idx); var found_another = false; // Check if there are any other occurrences of this variable var i: usize = 0; - while (i < self.scratch_type_var_validation.items.items.len) { - if (self.scratch_type_var_validation.items.items[i].idx == first_ident.idx) { + while (i < self.scratch_type_var_validation.items().len) { + if (self.scratch_type_var_validation.items()[i].idx == first_ident.idx) { found_another = true; // Remove this occurrence by swapping with the last element and shrinking - const last = self.scratch_type_var_validation.items.items.len - 1; - self.scratch_type_var_validation.items.items[i] = self.scratch_type_var_validation.items.items[last]; + const last = self.scratch_type_var_validation.items().len - 1; + self.scratch_type_var_validation.items()[i] = self.scratch_type_var_validation.items()[last]; self.scratch_type_var_validation.items.shrinkRetainingCapacity(last); } else { i += 1; diff --git a/src/canonicalize/Expression.zig b/src/canonicalize/Expression.zig index 1a1a793886a..7a821970e86 100644 --- a/src/canonicalize/Expression.zig +++ b/src/canonicalize/Expression.zig @@ -779,8 +779,8 @@ pub const Expr = union(enum) { const attrs = tree.beginNode(); const module_idx_int = @intFromEnum(e.module_idx); - std.debug.assert(module_idx_int < ir.imports.imports.items.items.len); - const string_lit_idx = ir.imports.imports.items.items[module_idx_int]; + std.debug.assert(module_idx_int < ir.imports.len()); + const string_lit_idx = ir.imports.imports.field(.str_idx)[module_idx_int]; const module_name = ir.common.strings.get(string_lit_idx); // Special case: Builtin module is an implementation detail, print as (builtin) if (std.mem.eql(u8, module_name, "Builtin")) { @@ -801,10 +801,10 @@ pub const Expr = union(enum) { try ir.appendRegionInfoToSExprTreeFromRegion(tree, region); const attrs = tree.beginNode(); - const requires_items = ir.requires_types.items.items; + const requires_items = ir.requires_types.items(); const idx = e.requires_idx.toU32(); - if (idx < requires_items.len) { - const required_type = requires_items[idx]; + if (idx > 0 and idx <= requires_items.len) { + const required_type = requires_items[idx - 1]; const ident_name = ir.getIdent(required_type.ident); try tree.pushStringPair("required-ident", ident_name); } @@ -975,8 +975,8 @@ pub const Expr = union(enum) { const attrs = tree.beginNode(); const module_idx_int = @intFromEnum(e.module_idx); - std.debug.assert(module_idx_int < ir.imports.imports.items.items.len); - const string_lit_idx = ir.imports.imports.items.items[module_idx_int]; + std.debug.assert(module_idx_int < ir.imports.len()); + const string_lit_idx = ir.imports.imports.field(.str_idx)[module_idx_int]; const module_name = ir.common.strings.get(string_lit_idx); // Special case: Builtin module is an implementation detail, print as (builtin) if (std.mem.eql(u8, module_name, "Builtin")) { diff --git a/src/canonicalize/HostedCompiler.zig b/src/canonicalize/HostedCompiler.zig index 5ee2096879a..679e5d0e7ca 100644 --- a/src/canonicalize/HostedCompiler.zig +++ b/src/canonicalize/HostedCompiler.zig @@ -120,7 +120,7 @@ pub fn replaceAnnoOnlyWithHosted(env: *ModuleEnv) !std.ArrayList(CIR.Def.Idx) { const def_node = env.store.nodes.get(def_node_idx); const extra_start = def_node.data_1; - env.store.extra_data.items.items[extra_start + 1] = @intFromEnum(expr_idx); + env.store.extra_data.items()[extra_start + 1] = @intFromEnum(expr_idx); // Verify the def still has its annotation after modification const modified_def = env.store.getDef(def_idx); diff --git a/src/canonicalize/ModuleEnv.zig b/src/canonicalize/ModuleEnv.zig index 7286e445f02..7a6e89acd23 100644 --- a/src/canonicalize/ModuleEnv.zig +++ b/src/canonicalize/ModuleEnv.zig @@ -1936,9 +1936,15 @@ pub fn nodeIdxFrom(idx: anytype) Node.Idx { return @enumFromInt(@intFromEnum(idx)); } -/// Convert a type into a type var +/// Convert a node index (0-based) into a type var (1-based) pub fn varFrom(idx: anytype) TypeVar { - return @enumFromInt(@intFromEnum(idx)); + return @enumFromInt(@intFromEnum(idx) + 1); +} + +/// Convert a type var (1-based) back to a node index (0-based) +/// This is the inverse of varFrom. +pub fn nodeIdxFromVar(var_: TypeVar) Node.Idx { + return @enumFromInt(@intFromEnum(var_) - 1); } /// Adds an identifier to the list of exposed items by its identifier index. @@ -2242,7 +2248,7 @@ pub fn pushToSExprTree(self: *Self, maybe_expr_idx: ?CIR.Expr.Idx, tree: *SExprT try self.store.getStatement(stmt_idx).pushToSExprTree(self, tree, stmt_idx); } - for (0..@intCast(self.external_decls.len())) |i| { + for (1..@intCast(self.external_decls.len() + 1)) |i| { const external_decl = self.external_decls.get(@enumFromInt(i)); try external_decl.pushToSExprTree(self, tree); } diff --git a/src/canonicalize/NodeStore.zig b/src/canonicalize/NodeStore.zig index f68a9c55d74..4b2760d4d6c 100644 --- a/src/canonicalize/NodeStore.zig +++ b/src/canonicalize/NodeStore.zig @@ -182,9 +182,10 @@ comptime { std.debug.assert(pattern_fields.len == MODULEENV_PATTERN_NODE_COUNT); } -/// Helper function to get a region by node index, handling the type conversion +/// Helper function to get a region by node index, handling the type conversion. +/// Node indices are 0-based (from SafeMultiList), but Region indices are 1-based (from SafeList). pub fn getRegionAt(store: *const NodeStore, node_idx: Node.Idx) Region { - const idx: Region.Idx = @enumFromInt(@intFromEnum(node_idx)); + const idx: Region.Idx = @enumFromInt(@intFromEnum(node_idx) + 1); return store.regions.get(idx).*; } @@ -235,7 +236,7 @@ pub fn getStatement(store: *const NodeStore, statement: CIR.Statement.Idx) CIR.S .expr = @enumFromInt(node.data_2), .anno = blk: { const extra_start = node.data_3; - const extra_data = store.extra_data.items.items[extra_start..]; + const extra_data = store.extra_data.items()[extra_start..]; const has_anno = extra_data[0] != 0; if (has_anno) { break :blk @as(CIR.Annotation.Idx, @enumFromInt(extra_data[1])); @@ -251,7 +252,7 @@ pub fn getStatement(store: *const NodeStore, statement: CIR.Statement.Idx) CIR.S .expr = @enumFromInt(node.data_2), .anno = blk: { const extra_start = node.data_3; - const extra_data = store.extra_data.items.items[extra_start..]; + const extra_data = store.extra_data.items()[extra_start..]; const has_anno = extra_data[0] != 0; if (has_anno) { break :blk @as(CIR.Annotation.Idx, @enumFromInt(extra_data[1])); @@ -295,7 +296,7 @@ pub fn getStatement(store: *const NodeStore, statement: CIR.Statement.Idx) CIR.S } }, .statement_import => { const extra_start = node.data_2; - const extra_data = store.extra_data.items.items[extra_start..]; + const extra_data = store.extra_data.items()[extra_start..]; const alias_data = extra_data[0]; const qualifier_data = extra_data[1]; @@ -333,7 +334,7 @@ pub fn getStatement(store: *const NodeStore, statement: CIR.Statement.Idx) CIR.S }, .statement_type_anno => { const extra_start = node.data_1; - const extra_data = store.extra_data.items.items[extra_start..]; + const extra_data = store.extra_data.items()[extra_start..]; const anno: CIR.TypeAnno.Idx = @enumFromInt(extra_data[0]); const name: Ident.Idx = @bitCast(extra_data[1]); @@ -397,7 +398,7 @@ pub fn getExpr(store: *const NodeStore, expr: CIR.Expr.Idx) CIR.Expr { // Read i128 from extra_data (stored as 4 u32s in data_1) const val_kind: CIR.IntValue.IntKind = @enumFromInt(node.data_2); - const value_as_u32s = store.extra_data.items.items[node.data_3..][0..4]; + const value_as_u32s = store.extra_data.items()[node.data_3..][0..4]; // Retrieve type variable from data_2 and requirements from data_3 return CIR.Expr{ @@ -424,7 +425,7 @@ pub fn getExpr(store: *const NodeStore, expr: CIR.Expr.Idx) CIR.Expr { .expr_call => { // Retrieve args span from extra_data const extra_start = node.data_2; - const extra_data = store.extra_data.items.items[extra_start..]; + const extra_data = store.extra_data.items()[extra_start..]; const args_start = extra_data[0]; const args_len = extra_data[1]; @@ -452,7 +453,7 @@ pub fn getExpr(store: *const NodeStore, expr: CIR.Expr.Idx) CIR.Expr { .expr_dec => { // Get value from extra_data const extra_data_idx = node.data_1; - const value_as_u32s = store.extra_data.items.items[extra_data_idx..][0..4]; + const value_as_u32s = store.extra_data.items()[extra_data_idx..][0..4]; const value_as_i128: i128 = @bitCast(value_as_u32s.*); return CIR.Expr{ @@ -515,7 +516,7 @@ pub fn getExpr(store: *const NodeStore, expr: CIR.Expr.Idx) CIR.Expr { const target_node_idx: u16 = @intCast(node.data_2); const extra_data_idx = node.data_3; - const extra_data = store.extra_data.items.items[extra_data_idx..][0..2]; + const extra_data = store.extra_data.items()[extra_data_idx..][0..2]; const backing_expr: CIR.Expr.Idx = @enumFromInt(extra_data[0]); const backing_type: CIR.Expr.NominalBackingType = @enumFromInt(extra_data[1]); @@ -540,7 +541,7 @@ pub fn getExpr(store: *const NodeStore, expr: CIR.Expr.Idx) CIR.Expr { .expr_closure => { // Retrieve closure data from extra_data const extra_start = node.data_1; - const extra_data = store.extra_data.items.items[extra_start..]; + const extra_data = store.extra_data.items()[extra_start..]; const lambda_idx = extra_data[0]; const capture_start = extra_data[1]; @@ -556,7 +557,7 @@ pub fn getExpr(store: *const NodeStore, expr: CIR.Expr.Idx) CIR.Expr { .expr_lambda => { // Retrieve lambda data from extra_data const extra_start = node.data_1; - const extra_data = store.extra_data.items.items[extra_start..]; + const extra_data = store.extra_data.items()[extra_start..]; const args_start = extra_data[0]; const args_len = extra_data[1]; @@ -585,7 +586,7 @@ pub fn getExpr(store: *const NodeStore, expr: CIR.Expr.Idx) CIR.Expr { }, .expr_record => { const extra_start = node.data_1; - const extra_data = store.extra_data.items.items[extra_start..]; + const extra_data = store.extra_data.items()[extra_start..]; const fields_start = extra_data[0]; const fields_len = extra_data[1]; @@ -602,7 +603,7 @@ pub fn getExpr(store: *const NodeStore, expr: CIR.Expr.Idx) CIR.Expr { }, .expr_match => { const extra_start = node.data_1; - const extra_data = store.extra_data.items.items[extra_start..]; + const extra_data = store.extra_data.items()[extra_start..]; const cond = @as(CIR.Expr.Idx, @enumFromInt(extra_data[0])); const branches_start = extra_data[1]; @@ -619,7 +620,7 @@ pub fn getExpr(store: *const NodeStore, expr: CIR.Expr.Idx) CIR.Expr { }, .expr_zero_argument_tag => { const extra_start = node.data_1; - const extra_data = store.extra_data.items.items[extra_start..]; + const extra_data = store.extra_data.items()[extra_start..]; const closure_name = @as(Ident.Idx, @bitCast(extra_data[0])); const variant_var = @as(types.Var, @enumFromInt(extra_data[1])); @@ -676,7 +677,7 @@ pub fn getExpr(store: *const NodeStore, expr: CIR.Expr.Idx) CIR.Expr { const symbol_name: base.Ident.Idx = @bitCast(node.data_1); const index = node.data_2; const extra_start = node.data_3; - const extra_data = store.extra_data.items.items[extra_start..]; + const extra_data = store.extra_data.items()[extra_start..]; const args_start = extra_data[0]; const args_len = extra_data[1]; @@ -693,7 +694,7 @@ pub fn getExpr(store: *const NodeStore, expr: CIR.Expr.Idx) CIR.Expr { // Retrieve low-level lambda data from extra_data const op: CIR.Expr.LowLevel = @enumFromInt(node.data_1); const extra_start = node.data_2; - const extra_data = store.extra_data.items.items[extra_start..]; + const extra_data = store.extra_data.items()[extra_start..]; const args_start = extra_data[0]; const args_len = extra_data[1]; @@ -712,7 +713,7 @@ pub fn getExpr(store: *const NodeStore, expr: CIR.Expr.Idx) CIR.Expr { }, .expr_if_then_else => { const extra_start = node.data_1; - const extra_data = store.extra_data.items.items[extra_start..]; + const extra_data = store.extra_data.items()[extra_start..]; const branches_span_start: u32 = extra_data[0]; const branches_span_end: u32 = extra_data[1]; @@ -732,7 +733,7 @@ pub fn getExpr(store: *const NodeStore, expr: CIR.Expr.Idx) CIR.Expr { .expr_dot_access => { // Read extra data: field_name_region (2 u32s) + optional args (1 u32) const extra_start = node.data_3; - const extra_data = store.extra_data.items.items[extra_start..]; + const extra_data = store.extra_data.items()[extra_start..]; const field_name_region = base.Region{ .start = .{ .offset = extra_data[0] }, .end = .{ .offset = extra_data[1] }, @@ -840,7 +841,7 @@ pub fn getMatchBranch(store: *const NodeStore, branch: CIR.Expr.Match.Branch.Idx // Retrieve when branch data from extra_data const extra_start = node.data_1; - const extra_data = store.extra_data.items.items[extra_start..]; + const extra_data = store.extra_data.items()[extra_start..]; const patterns: CIR.Expr.Match.BranchPattern.Span = .{ .span = .{ .start = extra_data[0], .len = extra_data[1] } }; const value_idx: CIR.Expr.Idx = @enumFromInt(extra_data[2]); @@ -870,7 +871,7 @@ pub fn getMatchBranchPattern(store: *const NodeStore, branch_pat: CIR.Expr.Match /// Returns a slice of match branches from the given span. pub fn matchBranchSlice(store: *const NodeStore, span: CIR.Expr.Match.Branch.Span) []CIR.Expr.Match.Branch.Idx { - const slice = store.extra_data.items.items[span.span.start..(span.span.start + span.span.len)]; + const slice = store.extra_data.items()[span.span.start..(span.span.start + span.span.len)]; const result: []CIR.Expr.Match.Branch.Idx = @ptrCast(@alignCast(slice)); return result; } @@ -886,7 +887,7 @@ pub fn getWhereClause(store: *const NodeStore, whereClause: CIR.WhereClause.Idx) const method_name = @as(Ident.Idx, @bitCast(node.data_2)); const extra_start = node.data_3; - const extra_data = store.extra_data.items.items[extra_start..]; + const extra_data = store.extra_data.items()[extra_start..]; const args_start = extra_data[0]; const args_len = extra_data[1]; @@ -966,7 +967,7 @@ pub fn getPattern(store: *const NodeStore, pattern_idx: CIR.Pattern.Idx) CIR.Pat const target_node_idx: u16 = @intCast(node.data_2); const extra_data_idx = node.data_3; - const extra_data = store.extra_data.items.items[extra_data_idx..][0..2]; + const extra_data = store.extra_data.items()[extra_data_idx..][0..2]; const backing_pattern: CIR.Pattern.Idx = @enumFromInt(extra_data[0]); const backing_type: CIR.Expr.NominalBackingType = @enumFromInt(extra_data[1]); @@ -991,7 +992,7 @@ pub fn getPattern(store: *const NodeStore, pattern_idx: CIR.Pattern.Idx) CIR.Pat }, .pattern_list => { const extra_start = node.data_1; - const extra_data = store.extra_data.items.items[extra_start..]; + const extra_data = store.extra_data.items()[extra_start..]; const patterns_start = extra_data[0]; const patterns_len = extra_data[1]; @@ -1029,7 +1030,7 @@ pub fn getPattern(store: *const NodeStore, pattern_idx: CIR.Pattern.Idx) CIR.Pat const val_kind: CIR.IntValue.IntKind = @enumFromInt(node.data_2); const extra_data_idx = node.data_3; - const value_as_u32s = store.extra_data.items.items[extra_data_idx..][0..4]; + const value_as_u32s = store.extra_data.items()[extra_data_idx..][0..4]; const value_as_i128: i128 = @bitCast(value_as_u32s.*); return CIR.Pattern{ @@ -1053,7 +1054,7 @@ pub fn getPattern(store: *const NodeStore, pattern_idx: CIR.Pattern.Idx) CIR.Pat }, .pattern_dec_literal => { const extra_data_idx = node.data_1; - const value_as_u32s = store.extra_data.items.items[extra_data_idx..][0..4]; + const value_as_u32s = store.extra_data.items()[extra_data_idx..][0..4]; const value_as_i128: i128 = @bitCast(value_as_u32s.*); const has_suffix = node.data_2 != 0; @@ -1118,7 +1119,7 @@ pub fn getTypeAnno(store: *const NodeStore, typeAnno: CIR.TypeAnno.Idx) CIR.Type const name: Ident.Idx = @bitCast(node.data_1); const args_start = node.data_2; - const extra_data = store.extra_data.items.items[node.data_3..]; + const extra_data = store.extra_data.items()[node.data_3..]; const args_len = extra_data[0]; const base_enum: CIR.TypeAnno.LocalOrExternal.Tag = @enumFromInt(extra_data[1]); const type_base: CIR.TypeAnno.LocalOrExternal = blk: { @@ -1155,7 +1156,7 @@ pub fn getTypeAnno(store: *const NodeStore, typeAnno: CIR.TypeAnno.Idx) CIR.Type const name: Ident.Idx = @bitCast(node.data_1); const base_enum: CIR.TypeAnno.LocalOrExternal.Tag = @enumFromInt(node.data_2); - const extra_data = store.extra_data.items.items[node.data_3..]; + const extra_data = store.extra_data.items()[node.data_3..]; const type_base: CIR.TypeAnno.LocalOrExternal = blk: { switch (base_enum) { .builtin => { @@ -1194,8 +1195,8 @@ pub fn getTypeAnno(store: *const NodeStore, typeAnno: CIR.TypeAnno.Idx) CIR.Type } }, .ty_fn => { const extra_data_idx = node.data_3; - const effectful = store.extra_data.items.items[extra_data_idx] != 0; - const ret: CIR.TypeAnno.Idx = @enumFromInt(store.extra_data.items.items[extra_data_idx + 1]); + const effectful = store.extra_data.items()[extra_data_idx] != 0; + const ret: CIR.TypeAnno.Idx = @enumFromInt(store.extra_data.items()[extra_data_idx + 1]); return CIR.TypeAnno{ .@"fn" = .{ .args = .{ .span = .{ .start = node.data_1, .len = node.data_2 } }, .ret = ret, @@ -1252,7 +1253,7 @@ pub fn getAnnotation(store: *const NodeStore, annotation: CIR.Annotation.Idx) CI const where_flag = node.data_2; const where_clause = if (where_flag == 1) blk: { const extra_start = node.data_3; - const extra_data = store.extra_data.items.items[extra_start..]; + const extra_data = store.extra_data.items()[extra_start..]; const where_start = extra_data[0]; const where_len = extra_data[1]; @@ -2335,7 +2336,7 @@ pub fn getDef(store: *const NodeStore, def_idx: CIR.Def.Idx) CIR.Def { std.debug.assert(node.tag == .def); const extra_start = node.data_1; - const extra_data = store.extra_data.items.items[extra_start..]; + const extra_data = store.extra_data.items()[extra_start..]; const pattern: CIR.Pattern.Idx = @enumFromInt(extra_data[0]); const expr: CIR.Expr.Idx = @enumFromInt(extra_data[1]); @@ -2363,7 +2364,7 @@ pub fn setDefExpr(store: *NodeStore, def_idx: CIR.Def.Idx, new_expr: CIR.Expr.Id const extra_start = node.data_1; // The expr field is at offset 1 in the extra_data layout for Def // Layout: [0]=pattern, [1]=expr, [2-3]=kind, [4]=annotation - store.extra_data.items.items[extra_start + 1] = @intFromEnum(new_expr); + store.extra_data.items()[extra_start + 1] = @intFromEnum(new_expr); } /// Retrieves a capture from the store. @@ -2399,7 +2400,7 @@ pub fn getRecordDestruct(store: *const NodeStore, idx: CIR.Pattern.RecordDestruc // Retrieve kind from extra_data if it exists const kind = blk: { const extra_start = node.data_3; - const extra_data = store.extra_data.items.items[extra_start..][0..2]; + const extra_data = store.extra_data.items()[extra_start..][0..2]; const kind_tag = extra_data[0]; break :blk switch (kind_tag) { @@ -2655,7 +2656,7 @@ pub fn clearScratchDefsFrom(store: *NodeStore, start: u32) void { /// Creates a slice corresponding to a span. pub fn sliceFromSpan(store: *const NodeStore, comptime T: type, span: base.DataSpan) []T { - return @ptrCast(store.extra_data.items.items[span.start..][0..span.len]); + return @ptrCast(store.extra_data.items()[span.start..][0..span.len]); } /// Returns a slice of definitions from the store. @@ -2705,12 +2706,12 @@ pub fn sliceMatchBranchPatterns(store: *const NodeStore, span: CIR.Expr.Match.Br /// Creates a slice corresponding to a span. pub fn firstFromSpan(store: *const NodeStore, comptime T: type, span: base.DataSpan) T { - return @as(T, @enumFromInt(store.extra_data.items.items[span.start])); + return @as(T, @enumFromInt(store.extra_data.items()[span.start])); } /// Creates a slice corresponding to a span. pub fn lastFromSpan(store: *const NodeStore, comptime T: type, span: base.DataSpan) T { - return @as(T, @enumFromInt(store.extra_data.items.items[span.start + span.len - 1])); + return @as(T, @enumFromInt(store.extra_data.items()[span.start + span.len - 1])); } /// Retrieve a slice of IfBranch Idx's from a span @@ -3186,7 +3187,7 @@ pub fn getDiagnostic(store: *const NodeStore, diagnostic: CIR.Diagnostic.Idx) CI .region = store.getRegionAt(node_idx), } }, .diag_redundant_exposed => { - const extra_data = store.extra_data.items.items[node.data_2..]; + const extra_data = store.extra_data.items()[node.data_2..]; const original_start = extra_data[0]; const original_end = extra_data[1]; return CIR.Diagnostic{ .redundant_exposed = .{ @@ -3374,7 +3375,7 @@ pub fn getDiagnostic(store: *const NodeStore, diagnostic: CIR.Diagnostic.Idx) CI }, } }, .diag_type_shadowed_warning => { - const extra_data = store.extra_data.items.items[node.data_3..]; + const extra_data = store.extra_data.items()[node.data_3..]; const original_start = extra_data[0]; const original_end = extra_data[1]; return CIR.Diagnostic{ .type_shadowed_warning = .{ @@ -3388,7 +3389,7 @@ pub fn getDiagnostic(store: *const NodeStore, diagnostic: CIR.Diagnostic.Idx) CI } }; }, .diag_type_parameter_conflict => { - const extra_data = store.extra_data.items.items[node.data_3..]; + const extra_data = store.extra_data.items()[node.data_3..]; const original_start = extra_data[0]; const original_end = extra_data[1]; return CIR.Diagnostic{ .type_parameter_conflict = .{ @@ -3688,14 +3689,14 @@ test "NodeStore basic CompactWriter roundtrip" { // Verify extra_data try testing.expectEqual(@as(usize, 4), deserialized.extra_data.len()); - const retrieved_u32s = deserialized.extra_data.items.items[0..4]; + const retrieved_u32s = deserialized.extra_data.items()[0..4]; const retrieved_bytes: [16]u8 = @bitCast(retrieved_u32s.*); const retrieved_value: i128 = @bitCast(retrieved_bytes); try testing.expectEqual(@as(i128, 42), retrieved_value); // Verify regions try testing.expectEqual(@as(usize, 1), deserialized.regions.len()); - const retrieved_region = deserialized.regions.get(@enumFromInt(0)); + const retrieved_region = deserialized.regions.get(@enumFromInt(1)); try testing.expectEqual(region.start.offset, retrieved_region.start.offset); try testing.expectEqual(region.end.offset, retrieved_region.end.offset); } @@ -3799,14 +3800,14 @@ test "NodeStore multiple nodes CompactWriter roundtrip" { // Verify float node and extra data const retrieved_float = deserialized.nodes.get(@enumFromInt(2)); try testing.expectEqual(Node.Tag.expr_frac_f64, retrieved_float.tag); - const retrieved_float_u32s = deserialized.extra_data.items.items[0..2]; + const retrieved_float_u32s = deserialized.extra_data.items()[0..2]; const retrieved_float_u64: u64 = @bitCast(retrieved_float_u32s.*); const retrieved_float_value: f64 = @bitCast(retrieved_float_u64); try testing.expectApproxEqAbs(float_value, retrieved_float_value, 0.0001); // Verify regions try testing.expectEqual(@as(usize, 3), deserialized.regions.len()); - for (regions, 0..) |expected_region, i| { + for (regions, 1..) |expected_region, i| { const retrieved_region = deserialized.regions.get(@enumFromInt(i)); try testing.expectEqual(expected_region.start.offset, retrieved_region.start.offset); try testing.expectEqual(expected_region.end.offset, retrieved_region.end.offset); diff --git a/src/canonicalize/Pattern.zig b/src/canonicalize/Pattern.zig index 7efe075a137..68ee65b3440 100644 --- a/src/canonicalize/Pattern.zig +++ b/src/canonicalize/Pattern.zig @@ -353,8 +353,8 @@ pub const Pattern = union(enum) { try ir.appendRegionInfoToSExprTree(tree, pattern_idx); const module_idx_int = @intFromEnum(n.module_idx); - std.debug.assert(module_idx_int < ir.imports.imports.items.items.len); - const string_lit_idx = ir.imports.imports.items.items[module_idx_int]; + std.debug.assert(module_idx_int < ir.imports.len()); + const string_lit_idx = ir.imports.imports.field(.str_idx)[module_idx_int]; const module_name = ir.common.strings.get(string_lit_idx); // Special case: Builtin module is an implementation detail, print as (builtin) if (std.mem.eql(u8, module_name, "Builtin")) { diff --git a/src/canonicalize/TypeAnnotation.zig b/src/canonicalize/TypeAnnotation.zig index e1da4576716..e886a5878fa 100644 --- a/src/canonicalize/TypeAnnotation.zig +++ b/src/canonicalize/TypeAnnotation.zig @@ -123,8 +123,8 @@ pub const TypeAnno = union(enum) { }, .external => |external| { const module_idx_int = @intFromEnum(external.module_idx); - std.debug.assert(module_idx_int < ir.imports.imports.items.items.len); - const string_lit_idx = ir.imports.imports.items.items[module_idx_int]; + std.debug.assert(module_idx_int < ir.imports.len()); + const string_lit_idx = ir.imports.imports.field(.str_idx)[module_idx_int]; const module_name = ir.common.strings.get(string_lit_idx); // Special case: Builtin module is an implementation detail, print as (builtin) if (std.mem.eql(u8, module_name, "Builtin")) { @@ -192,8 +192,8 @@ pub const TypeAnno = union(enum) { }, .external => |external| { const module_idx_int = @intFromEnum(external.module_idx); - std.debug.assert(module_idx_int < ir.imports.imports.items.items.len); - const string_lit_idx = ir.imports.imports.items.items[module_idx_int]; + std.debug.assert(module_idx_int < ir.imports.len()); + const string_lit_idx = ir.imports.imports.field(.str_idx)[module_idx_int]; const module_name = ir.common.strings.get(string_lit_idx); // Special case: Builtin module is an implementation detail, print as (builtin) if (std.mem.eql(u8, module_name, "Builtin")) { diff --git a/src/canonicalize/test/import_store_test.zig b/src/canonicalize/test/import_store_test.zig index 3a689984735..12e22d53734 100644 --- a/src/canonicalize/test/import_store_test.zig +++ b/src/canonicalize/test/import_store_test.zig @@ -40,8 +40,8 @@ test "Import.Store deduplicates module names" { try testing.expectEqual(@as(usize, 2), store.imports.len()); // Verify we can retrieve the module names through the string store - const str_idx1 = store.imports.items.items[@intFromEnum(idx1)]; - const str_idx3 = store.imports.items.items[@intFromEnum(idx3)]; + const str_idx1 = store.imports.field(.str_idx)[@intFromEnum(idx1)]; + const str_idx3 = store.imports.field(.str_idx)[@intFromEnum(idx3)]; try testing.expectEqualStrings("test.Module", string_store.get(str_idx1)); try testing.expectEqualStrings("other.Module", string_store.get(str_idx3)); } @@ -137,9 +137,9 @@ test "Import.Store basic CompactWriter roundtrip" { try testing.expectEqual(@as(usize, 3), deserialized.imports.len()); // Verify the interned string IDs are stored correctly - const str_idx1 = deserialized.imports.items.items[0]; - const str_idx2 = deserialized.imports.items.items[1]; - const str_idx3 = deserialized.imports.items.items[2]; + const str_idx1 = deserialized.imports.field(.str_idx)[0]; + const str_idx2 = deserialized.imports.field(.str_idx)[1]; + const str_idx3 = deserialized.imports.field(.str_idx)[2]; try testing.expectEqualStrings("json.Json", string_store.get(str_idx1)); try testing.expectEqualStrings("core.List", string_store.get(str_idx2)); @@ -202,8 +202,8 @@ test "Import.Store duplicate imports CompactWriter roundtrip" { try testing.expectEqual(@as(usize, 2), deserialized.imports.len()); // Get the string IDs and verify the strings - const str_idx1 = deserialized.imports.items.items[@intFromEnum(idx1)]; - const str_idx2 = deserialized.imports.items.items[@intFromEnum(idx2)]; + const str_idx1 = deserialized.imports.field(.str_idx)[@intFromEnum(idx1)]; + const str_idx2 = deserialized.imports.field(.str_idx)[@intFromEnum(idx2)]; try testing.expectEqualStrings("test.Module", string_store.get(str_idx1)); try testing.expectEqualStrings("another.Module", string_store.get(str_idx2)); @@ -212,8 +212,8 @@ test "Import.Store duplicate imports CompactWriter roundtrip" { try testing.expectEqual(@as(usize, 2), deserialized.map.count()); // Check that the map has correct entries for the string indices that were deserialized - const str_idx_0 = deserialized.imports.items.items[0]; - const str_idx_1 = deserialized.imports.items.items[1]; + const str_idx_0 = deserialized.imports.field(.str_idx)[0]; + const str_idx_1 = deserialized.imports.field(.str_idx)[1]; try testing.expect(deserialized.map.contains(str_idx_0)); try testing.expect(deserialized.map.contains(str_idx_1)); diff --git a/src/canonicalize/test/import_validation_test.zig b/src/canonicalize/test/import_validation_test.zig index 969878377fd..d76ea4c9755 100644 --- a/src/canonicalize/test/import_validation_test.zig +++ b/src/canonicalize/test/import_validation_test.zig @@ -251,13 +251,13 @@ test "import interner - Import.Idx functionality" { _ = try result.can.canonicalizeFile(); // Check that we have the correct number of unique imports (duplicates are deduplicated) // Expected: List, Dict, Json, Set (4 unique) - try expectEqual(@as(usize, 4), result.parse_env.imports.imports.len()); + try expectEqual(@as(usize, 4), result.parse_env.imports.len()); // Verify each unique module has an Import.Idx var found_list = false; var found_dict = false; var found_json_decode = false; var found_set = false; - for (result.parse_env.imports.imports.items.items) |import_string_idx| { + for (result.parse_env.imports.imports.field(.str_idx)) |import_string_idx| { const module_name = result.parse_env.getString(import_string_idx); if (std.mem.eql(u8, module_name, "List")) { found_list = true; @@ -277,7 +277,7 @@ test "import interner - Import.Idx functionality" { // Test the lookup functionality // Get the Import.Idx for "List" (should be used twice) var list_import_idx: ?CIR.Import.Idx = null; - for (result.parse_env.imports.imports.items.items, 0..) |import_string_idx, idx| { + for (result.parse_env.imports.imports.field(.str_idx), 0..) |import_string_idx, idx| { if (std.mem.eql(u8, result.parse_env.getString(import_string_idx), "List")) { list_import_idx = @enumFromInt(idx); break; @@ -322,12 +322,12 @@ test "import interner - comprehensive usage example" { _ = try result.can.canonicalizeFile(); // Check that we have the correct number of unique imports // Expected: List, Dict, Try (3 unique) - try expectEqual(@as(usize, 3), result.parse_env.imports.imports.len()); + try expectEqual(@as(usize, 3), result.parse_env.imports.len()); // Verify each unique module has an Import.Idx var found_list = false; var found_dict = false; var found_result = false; - for (result.parse_env.imports.imports.items.items, 0..) |import_string_idx, idx| { + for (result.parse_env.imports.imports.field(.str_idx), 0..) |import_string_idx, idx| { if (std.mem.eql(u8, result.parse_env.getString(import_string_idx), "List")) { found_list = true; // Note: We can't verify exposed items count here as Import.Store only stores module names @@ -398,7 +398,7 @@ test "module scopes - imports work in module scope" { try testing.expect(imports.len() >= 2); // List and Dict var has_list = false; var has_dict = false; - for (imports.items.items) |import_string_idx| { + for (imports.field(.str_idx)) |import_string_idx| { const import_name = result.parse_env.getString(import_string_idx); if (std.mem.eql(u8, import_name, "List")) has_list = true; if (std.mem.eql(u8, import_name, "Dict")) has_dict = true; @@ -448,7 +448,7 @@ test "module-qualified lookups with e_lookup_external" { // Verify the module names are correct var has_list = false; var has_dict = false; - for (imports_list.items.items) |import_string_idx| { + for (imports_list.field(.str_idx)) |import_string_idx| { const import_name = result.parse_env.getString(import_string_idx); if (std.mem.eql(u8, import_name, "List")) has_list = true; if (std.mem.eql(u8, import_name, "Dict")) has_dict = true; @@ -536,7 +536,7 @@ test "exposed_items - tracking CIR node indices for exposed items" { // For now, let's verify the imports were registered const imports_list = result.parse_env.imports.imports; var has_mathutils = false; - for (imports_list.items.items) |import_string_idx| { + for (imports_list.field(.str_idx)) |import_string_idx| { const import_name = result.parse_env.getString(import_string_idx); if (std.mem.eql(u8, import_name, "MathUtils")) { has_mathutils = true; @@ -587,7 +587,7 @@ test "exposed_items - tracking CIR node indices for exposed items" { // Verify EmptyModule was imported const imports_list2 = result2.parse_env.imports.imports; var has_empty_module = false; - for (imports_list2.items.items) |import_string_idx| { + for (imports_list2.field(.str_idx)) |import_string_idx| { const import_name = result2.parse_env.getString(import_string_idx); if (std.mem.eql(u8, import_name, "EmptyModule")) { has_empty_module = true; diff --git a/src/check/Check.zig b/src/check/Check.zig index 18a8b4b1573..c215948e21d 100644 --- a/src/check/Check.zig +++ b/src/check/Check.zig @@ -105,6 +105,8 @@ scratch_tags: base.Scratch(types_mod.Tag), scratch_record_fields: base.Scratch(types_mod.RecordField), /// scratch static dispatch constraints used to build up intermediate lists, used for various things scratch_static_dispatch_constraints: base.Scratch(ScratchStaticDispatchConstraint), +/// scratch vars for building header vars (type args converted to Vars) +scratch_header_vars: std.ArrayListUnmanaged(Var), /// Stack of type variables currently being constraint-checked, used to detect recursive constraints /// When a var appears in this stack while we're checking its constraints, we've detected recursion constraint_check_stack: std.ArrayList(Var), @@ -200,6 +202,7 @@ pub fn init( .scratch_tags = try base.Scratch(types_mod.Tag).init(gpa), .scratch_record_fields = try base.Scratch(types_mod.RecordField).init(gpa), .scratch_static_dispatch_constraints = try base.Scratch(ScratchStaticDispatchConstraint).init(gpa), + .scratch_header_vars = std.ArrayListUnmanaged(Var){}, .constraint_check_stack = try std.ArrayList(Var).initCapacity(gpa, 0), .import_cache = ImportCache{}, .constraint_origins = std.AutoHashMap(Var, Var).init(gpa), @@ -228,6 +231,7 @@ pub fn deinit(self: *Self) void { self.scratch_tags.deinit(); self.scratch_record_fields.deinit(); self.scratch_static_dispatch_constraints.deinit(); + self.scratch_header_vars.deinit(self.gpa); self.constraint_check_stack.deinit(self.gpa); self.import_cache.deinit(self.gpa); self.constraint_origins.deinit(); @@ -328,7 +332,7 @@ const Env = struct { fn reset(self: *Env) void { self.var_pool.current_rank = .generalized; self.var_pool.clearRetainingCapacity(); - self.deferred_static_dispatch_constraints.items.clearRetainingCapacity(); + self.deferred_static_dispatch_constraints.clearRetainingCapacity(); } fn rank(self: *const Env) Rank { @@ -396,8 +400,8 @@ fn unifyWithCtx(self: *Self, a: Var, b: Var, env: *Env, ctx: unifier.Conf.Ctx) s // // Note that we choose `b`s region here, since `b` is the "actual" type // (whereas `a` is the "expected" type, like from an annotation) - const region = self.cir.store.getNodeRegion(ModuleEnv.nodeIdxFrom(b)); - for (self.unify_scratch.fresh_vars.items.items) |fresh_var| { + const region = self.cir.store.getNodeRegion(ModuleEnv.nodeIdxFromVar(b)); + for (self.unify_scratch.fresh_vars.items()) |fresh_var| { // Set the rank const fresh_rank = self.types.resolveVar(fresh_var).desc.rank; try env.var_pool.addVarToRank(fresh_var, fresh_rank); @@ -408,7 +412,7 @@ fn unifyWithCtx(self: *Self, a: Var, b: Var, env: *Env, ctx: unifier.Conf.Ctx) s } // Copy any constraints created during unification into our own array - for (self.unify_scratch.deferred_constraints.items.items) |deferred_constraint| { + for (self.unify_scratch.deferred_constraints.items()) |deferred_constraint| { _ = try env.deferred_static_dispatch_constraints.append(self.gpa, deferred_constraint); } @@ -577,12 +581,12 @@ fn instantiateVarHelp( fn fillInRegionsThrough(self: *Self, target_var: Var) Allocator.Error!void { const idx = @intFromEnum(target_var); - if (idx >= self.regions.len()) { - try self.regions.items.ensureTotalCapacity(self.gpa, idx + 1); + if (idx > self.regions.len()) { + try self.regions.ensureTotalCapacity(self.gpa, idx); const empty_region = Region.zero(); - while (self.regions.len() <= idx) { - self.regions.items.appendAssumeCapacity(empty_region); + while (self.regions.len() < idx) { + _ = self.regions.appendAssumeCapacity(empty_region); } } } @@ -1054,7 +1058,7 @@ pub fn checkFile(self: *Self) std.mem.Allocator.Error!void { /// Process the requires_types annotations for platform modules. /// This generates the actual types from the type annotations stored in requires_types. fn processRequiresTypes(self: *Self, env: *Env) std.mem.Allocator.Error!void { - const requires_types_slice = self.cir.requires_types.items.items; + const requires_types_slice = self.cir.requires_types.items(); for (requires_types_slice) |required_type| { // Generate the type from the annotation try self.generateAnnoTypeInPlace(required_type.type_anno, env, .annotation); @@ -1079,7 +1083,7 @@ pub fn checkPlatformRequirements( defer self.env_pool.release(env); // Iterate over the platform's required types - const requires_types_slice = platform_env.requires_types.items.items; + const requires_types_slice = platform_env.requires_types.items(); for (requires_types_slice) |required_type| { // Look up the pre-translated app ident for this platform requirement const app_required_ident = platform_to_app_idents.get(required_type.ident); @@ -1295,7 +1299,10 @@ fn generateAliasDecl( const header_args = self.cir.store.sliceTypeAnnos(header.args); // Next, generate the provided arg types and build the map of rigid variables in the header - const header_vars = try self.generateHeaderVars(header_args, env); + // The returned slice uses scratch_header_vars, so we must store the vars in the types store + // immediately before any recursive calls that might clobber the scratch space. + _ = try self.generateHeaderVars(header_args, env); + const header_vars_range = try self.types.appendVars(self.scratch_header_vars.items); // Now we have a built of list of rigid variables for the decl lhs (header). // With this in hand, we can now generate the type for the lhs (body). @@ -1309,12 +1316,19 @@ fn generateAliasDecl( .num_args = @intCast(header_args.len), } }); + // IMPORTANT: Copy header_vars to scratch space BEFORE calling mkAlias, + // because mkAlias calls appendVar/appendVars which might reallocate types.vars + // and invalidate any slice pointing into it. + self.scratch_header_vars.clearRetainingCapacity(); + try self.scratch_header_vars.appendSlice(self.gpa, self.types.sliceVars(header_vars_range)); + + // Now safe to call mkAlias - it will append backing_var then args, and they'll be contiguous try self.unifyWith( decl_var, try self.types.mkAlias( .{ .ident_idx = header.name }, backing_var, - header_vars, + self.scratch_header_vars.items, ), env, ); @@ -1333,7 +1347,10 @@ fn generateNominalDecl( const header_args = self.cir.store.sliceTypeAnnos(header.args); // Next, generate the provided arg types and build the map of rigid variables in the header - const header_vars = try self.generateHeaderVars(header_args, env); + // The returned slice uses scratch_header_vars, so we must store the vars in the types store + // immediately before any recursive calls that might clobber the scratch space. + _ = try self.generateHeaderVars(header_args, env); + const header_vars_range = try self.types.appendVars(self.scratch_header_vars.items); // Now we have a built of list of rigid variables for the decl lhs (header). // With this in hand, we can now generate the type for the lhs (body). @@ -1347,12 +1364,19 @@ fn generateNominalDecl( .num_args = @intCast(header_args.len), } }); + // IMPORTANT: Copy header_vars to scratch space BEFORE calling mkNominal, + // because mkNominal calls appendVar/appendVars which might reallocate types.vars + // and invalidate any slice pointing into it. + self.scratch_header_vars.clearRetainingCapacity(); + try self.scratch_header_vars.appendSlice(self.gpa, self.types.sliceVars(header_vars_range)); + + // Now safe to call mkNominal - it will append backing_var then args, and they'll be contiguous try self.unifyWith( decl_var, try self.types.mkNominal( .{ .ident_idx = header.name }, backing_var, - header_vars, + self.scratch_header_vars.items, self.builtin_ctx.module_name, ), env, @@ -1365,6 +1389,9 @@ fn generateHeaderVars( header_args: []CIR.TypeAnno.Idx, env: *Env, ) std.mem.Allocator.Error![]Var { + // Clear and rebuild scratch_header_vars to hold the Var values + self.scratch_header_vars.clearRetainingCapacity(); + for (header_args) |header_arg_idx| { const header_arg = self.cir.store.getTypeAnno(header_arg_idx); const header_var = ModuleEnv.varFrom(header_arg_idx); @@ -1383,9 +1410,42 @@ fn generateHeaderVars( try self.unifyWith(header_var, .err, env); }, } + + try self.scratch_header_vars.append(self.gpa, header_var); + } + + return self.scratch_header_vars.items; +} + +/// Convert a slice of node indices to Vars and append to the type store's vars list. +/// This is needed because node indices are 0-based but Vars are 1-based (via varFrom). +/// The @ptrCast approach doesn't work since varFrom adds 1 to each index. +fn idxSliceToVarsRange(self: *Self, idx_slice: anytype) std.mem.Allocator.Error!Var.SafeList.Range { + // Build up vars in scratch, then append to type store + // Note: We don't use scratch_header_vars here because this can be called + // recursively during type generation, and we need to append to type store + // immediately anyway. + var scratch = std.ArrayListUnmanaged(Var){}; + defer scratch.deinit(self.gpa); + for (idx_slice) |idx| { + const var_ = ModuleEnv.varFrom(idx); + try scratch.append(self.gpa, var_); } + return try self.types.appendVars(scratch.items); +} - return @ptrCast(header_args); +/// Convert a slice of node indices to Vars using scratch space. +/// WARNING: The returned slice uses scratch_header_vars and will be invalidated +/// by subsequent calls to this function or generateHeaderVars! +/// This is needed because node indices are 0-based but Vars are 1-based (via varFrom). +fn idxSliceToVars(self: *Self, idx_slice: anytype) std.mem.Allocator.Error![]Var { + // Use scratch space for the result - this is safe to pass to functions that + // append to types.vars because it's a separate allocation + self.scratch_header_vars.clearRetainingCapacity(); + for (idx_slice) |idx| { + try self.scratch_header_vars.append(self.gpa, ModuleEnv.varFrom(idx)); + } + return self.scratch_header_vars.items; } // type gen config // @@ -1460,14 +1520,21 @@ fn generateStaticDispatchConstraintFromWhere(self: *Self, where_idx: CIR.WhereCl for (args_anno_slice) |arg_anno_idx| { try self.generateAnnoTypeInPlace(arg_anno_idx, env, .annotation); } - const anno_arg_vars: []Var = @ptrCast(args_anno_slice); + // Get range first - the args will already be stored in types.vars + const anno_arg_vars_range = try self.idxSliceToVarsRange(args_anno_slice); // Generate return type try self.generateAnnoTypeInPlace(method.ret, env, .annotation); const ret_var = ModuleEnv.varFrom(method.ret); + // Copy args to scratch space before calling mkFunc* because those + // functions call appendVars which might reallocate types.vars + var scratch_args = std.ArrayListUnmanaged(Var){}; + defer scratch_args.deinit(self.gpa); + try scratch_args.appendSlice(self.gpa, self.types.sliceVars(anno_arg_vars_range)); + // Create the function var - const func_content = try self.types.mkFuncUnbound(anno_arg_vars, ret_var); + const func_content = try self.types.mkFuncUnbound(scratch_args.items, ret_var); const func_var = try self.freshFromContent(func_content, env, where_region); // Add to scratch list @@ -1531,7 +1598,10 @@ fn generateAnnoTypeInPlace(self: *Self, anno_idx: CIR.TypeAnno.Idx, env: *Env, c .type_decl => {}, } const static_dispatch_constraints_end = self.types.static_dispatch_constraints.len(); - const static_dispatch_constraints_range = StaticDispatchConstraint.SafeList.Range{ .start = @enumFromInt(static_dispatch_constraints_start), .count = @intCast(static_dispatch_constraints_end - static_dispatch_constraints_start) }; + // With 1-based indexing: if len() was N before appending, elements were at 1..N + // After appending K elements, len() is N+K, new elements are at (N+1)..(N+K) + // So range.start should be N+1 (which is static_dispatch_constraints_start + 1) + const static_dispatch_constraints_range = StaticDispatchConstraint.SafeList.Range{ .start = @enumFromInt(static_dispatch_constraints_start + 1), .count = @intCast(static_dispatch_constraints_end - static_dispatch_constraints_start) }; try self.unifyWith(anno_var, .{ .rigid = Rigid{ .name = rigid.name, @@ -1638,7 +1708,7 @@ fn generateAnnoTypeInPlace(self: *Self, anno_idx: CIR.TypeAnno.Idx, env: *Env, c for (anno_args) |anno_arg| { try self.generateAnnoTypeInPlace(anno_arg, env, ctx); } - const anno_arg_vars: []Var = @ptrCast(anno_args); + const anno_arg_vars = try self.idxSliceToVars(anno_args); switch (a.base) { .builtin => |builtin_type| { @@ -1843,15 +1913,22 @@ fn generateAnnoTypeInPlace(self: *Self, anno_idx: CIR.TypeAnno.Idx, env: *Env, c for (args_anno_slice) |arg_anno_idx| { try self.generateAnnoTypeInPlace(arg_anno_idx, env, ctx); } - const args_var_slice: []Var = @ptrCast(args_anno_slice); + // Get range now - the args are already stored in types.vars + const args_var_range = try self.idxSliceToVarsRange(args_anno_slice); try self.generateAnnoTypeInPlace(func.ret, env, ctx); + // Copy args to scratch space before calling mkFunc* because those + // functions call appendVars which might reallocate types.vars + var scratch_args = std.ArrayListUnmanaged(Var){}; + defer scratch_args.deinit(self.gpa); + try scratch_args.appendSlice(self.gpa, self.types.sliceVars(args_var_range)); + const fn_type = inner_blk: { if (func.effectful) { - break :inner_blk try self.types.mkFuncEffectful(args_var_slice, ModuleEnv.varFrom(func.ret)); + break :inner_blk try self.types.mkFuncEffectful(scratch_args.items, ModuleEnv.varFrom(func.ret)); } else { - break :inner_blk try self.types.mkFuncPure(args_var_slice, ModuleEnv.varFrom(func.ret)); + break :inner_blk try self.types.mkFuncPure(scratch_args.items, ModuleEnv.varFrom(func.ret)); } }; try self.unifyWith(anno_var, fn_type, env); @@ -1879,7 +1956,7 @@ fn generateAnnoTypeInPlace(self: *Self, anno_idx: CIR.TypeAnno.Idx, env: *Env, c for (tag_anno_args_slice) |tag_arg_idx| { try self.generateAnnoTypeInPlace(tag_arg_idx, env, ctx); } - const tag_vars_slice: []Var = @ptrCast(tag_anno_args_slice); + const tag_vars_slice = try self.idxSliceToVars(tag_anno_args_slice); // Add the processed tag to scratch try self.scratch_tags.append(try self.types.mkTag( @@ -1962,7 +2039,8 @@ fn generateAnnoTypeInPlace(self: *Self, anno_idx: CIR.TypeAnno.Idx, env: *Env, c for (elems_anno_slice) |arg_anno_idx| { try self.generateAnnoTypeInPlace(arg_anno_idx, env, ctx); } - const elems_range = try self.types.appendVars(@ptrCast(elems_anno_slice)); + // Use idxSliceToVarsRange to properly convert 0-based indices to 1-based Vars + const elems_range = try self.idxSliceToVarsRange(elems_anno_slice); try self.unifyWith(anno_var, .{ .structure = .{ .tuple = .{ .elems = elems_range } } }, env); }, .parens => |parens| { @@ -2131,9 +2209,8 @@ fn checkPatternHelp( _ = try self.checkPatternHelp(single_elem_ptrn_idx, env, .no_expectation, out_var); } - // Add to types store - // Cast the elems idxs to vars (this works because Anno Idx are 1-1 with type Vars) - break :blk try self.types.appendVars(@ptrCast(elems_slice)); + // Add to types store - convert 0-based indices to 1-based Vars + break :blk try self.idxSliceToVarsRange(elems_slice); }, } }; @@ -2229,9 +2306,8 @@ fn checkPatternHelp( _ = try self.checkPatternHelp(arg_expr_idx, env, .no_expectation, out_var); } - // Add to types store - // Cast the elems idxs to vars (this works because Anno Idx are 1-1 with type Vars) - break :blk try self.types.appendVars(@ptrCast(arg_ptrn_idx_slice)); + // Add to types store (converting indices to vars) + break :blk try self.idxSliceToVarsRange(arg_ptrn_idx_slice); }, } }; @@ -2388,9 +2464,10 @@ fn checkPatternHelp( _ = try self.unify(destruct_var, field_pattern_var, env); // Append it to the scratch records array + // Note: destruct_var is already a Var (from varFrom call above), so use it directly try self.scratch_record_fields.append(types_mod.RecordField{ .name = destruct.label, - .var_ = ModuleEnv.varFrom(destruct_var), + .var_ = destruct_var, }); } @@ -2758,8 +2835,8 @@ fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx, env: *Env, expected: Expected) does_fx = try self.checkExpr(single_elem_expr_idx, env, .no_expectation) or does_fx; } - // Cast the elems idxs to vars (this works because Anno Idx are 1-1 with type Vars) - const elem_vars_slice = try self.types.appendVars(@ptrCast(elems_slice)); + // Convert 0-based indices to 1-based Vars + const elem_vars_slice = try self.idxSliceToVarsRange(elems_slice); // Set the type in the store try self.unifyWith(expr_var, .{ .structure = .{ @@ -2842,7 +2919,7 @@ fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx, env: *Env, expected: Expected) // Create the type const ext_var = try self.fresh(env, expr_region); - const tag = try self.types.mkTag(e.name, @ptrCast(arg_expr_idx_slice)); + const tag = try self.types.mkTag(e.name, try self.idxSliceToVars(arg_expr_idx_slice)); const tag_union_content = try self.types.mkTagUnion(&[_]types_mod.Tag{tag}, ext_var); // Update the expr to point to the new type @@ -3011,10 +3088,10 @@ fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx, env: *Env, expected: Expected) }, .e_lookup_required => |req| { // Look up the type from the platform's requires clause - const requires_items = self.cir.requires_types.items.items; + const requires_items = self.cir.requires_types.items(); const idx = req.requires_idx.toU32(); - if (idx < requires_items.len) { - const required_type = requires_items[idx]; + if (idx > 0 and idx <= requires_items.len) { + const required_type = requires_items[idx - 1]; const type_var = ModuleEnv.varFrom(required_type.type_anno); const instantiated_var = try self.instantiateVar( type_var, @@ -3170,7 +3247,9 @@ fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx, env: *Env, expected: Expected) // expectation checking code at the bottom of this function } } - const arg_vars: []Var = @ptrCast(arg_pattern_idxs); + // Convert arg pattern indices to vars and persist to types store immediately + // before checking the body, which may clobber scratch_header_vars + const arg_vars_range = try self.idxSliceToVarsRange(arg_pattern_idxs); // Check the the body of the expr // If we have an expected function, use that as the expr's expected type @@ -3183,11 +3262,17 @@ fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx, env: *Env, expected: Expected) } const body_var = ModuleEnv.varFrom(lambda.body); + // Copy args to scratch space before calling mkFunc* because those + // functions call appendVars which might reallocate types.vars + var scratch_args = std.ArrayListUnmanaged(Var){}; + defer scratch_args.deinit(self.gpa); + try scratch_args.appendSlice(self.gpa, self.types.sliceVars(arg_vars_range)); + // Create the function type if (does_fx) { - _ = try self.unifyWith(expr_var, try self.types.mkFuncEffectful(arg_vars, body_var), env); + _ = try self.unifyWith(expr_var, try self.types.mkFuncEffectful(scratch_args.items, body_var), env); } else { - _ = try self.unifyWith(expr_var, try self.types.mkFuncUnbound(arg_vars, body_var), env); + _ = try self.unifyWith(expr_var, try self.types.mkFuncUnbound(scratch_args.items, body_var), env); } // Now that we are existing the scope, we must generalize then pop this rank @@ -3374,7 +3459,7 @@ fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx, env: *Env, expected: Expected) // the actual arguments provoided, unify the // inferred function type with the expected function // type to get the regulare error message - const call_arg_vars: []Var = @ptrCast(call_arg_expr_idxs); + const call_arg_vars = try self.idxSliceToVars(call_arg_expr_idxs); const call_func_ret = try self.fresh(env, expr_region); const call_func_content = try self.types.mkFuncUnbound(call_arg_vars, call_func_ret); const call_func_var = try self.freshFromContent(call_func_content, env, expr_region); @@ -3395,7 +3480,7 @@ fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx, env: *Env, expected: Expected) // on how it's being used here. So we create that func // type and unify the function being called against it - const call_arg_vars: []Var = @ptrCast(call_arg_expr_idxs); + const call_arg_vars = try self.idxSliceToVars(call_arg_expr_idxs); const call_func_ret = try self.fresh(env, expr_region); const call_func_content = try self.types.mkFuncUnbound(call_arg_vars, call_func_ret); const call_func_var = try self.freshFromContent(call_func_content, env, expr_region); @@ -3467,7 +3552,7 @@ fn checkExpr(self: *Self, expr_idx: CIR.Expr.Idx, env: *Env, expected: Expected) // method being dispatched on must accept the type of `thing` as // it's first arg. So, we prepend the `receiver_var` to the args list const first_arg_range = try self.types.appendVars(&.{receiver_var}); - const rest_args_range = try self.types.appendVars(@ptrCast(dispatch_arg_expr_idxs)); + const rest_args_range = try self.idxSliceToVarsRange(dispatch_arg_expr_idxs); const dispatch_arg_vars_range = Var.SafeList.Range{ .start = first_arg_range.start, .count = rest_args_range.count + 1, @@ -4477,11 +4562,11 @@ fn resolveVarFromExternal( // Reuse the previously copied type. cached_var else blk: { - // First time importing this type - copy it and cache the result - const imported_var = @as(Var, @enumFromInt(@intFromEnum(target_node_idx))); + // First time importing this type - convert 0-based node index to 1-based Var + const imported_var = ModuleEnv.varFrom(target_node_idx); // Every node should have a corresponding type entry - std.debug.assert(@intFromEnum(imported_var) < other_module_env.types.len()); + std.debug.assert(@intFromEnum(imported_var) <= other_module_env.types.len()); const new_copy = try self.copyVar(imported_var, other_module_env, null); try self.import_cache.put(self.gpa, cache_key, new_copy); @@ -4610,13 +4695,13 @@ fn checkNumeralConstraint( } fn checkDeferredStaticDispatchConstraints(self: *Self, env: *Env) std.mem.Allocator.Error!void { - var deferred_constraint_len = env.deferred_static_dispatch_constraints.items.items.len; + var deferred_constraint_len = env.deferred_static_dispatch_constraints.items().len; var deferred_constraint_index: usize = 0; while (deferred_constraint_index < deferred_constraint_len) : ({ deferred_constraint_index += 1; - deferred_constraint_len = env.deferred_static_dispatch_constraints.items.items.len; + deferred_constraint_len = env.deferred_static_dispatch_constraints.items().len; }) { - const deferred_constraint = env.deferred_static_dispatch_constraints.items.items[deferred_constraint_index]; + const deferred_constraint = env.deferred_static_dispatch_constraints.items()[deferred_constraint_index]; const dispatcher_resolved = self.types.resolveVar(deferred_constraint.var_); const dispatcher_content = dispatcher_resolved.desc.content; @@ -4692,7 +4777,7 @@ fn checkDeferredStaticDispatchConstraints(self: *Self, env: *Env) std.mem.Alloca try self.reportConstraintError( deferred_constraint.var_, constraint, - .{ .missing_method = .nominal }, + .{ .missing_method = .rigid }, env, ); continue; @@ -4817,14 +4902,9 @@ fn checkDeferredStaticDispatchConstraints(self: *Self, env: *Env) std.mem.Alloca } else blk: { // Copy the method from the other module's type store const copied_var = try self.copyVar(def_var, original_env, region); - // For builtin methods, we need to instantiate the copied var to convert - // rigid type variables to flex, so they can unify with the call site - const is_builtin = original_module_ident == self.cir.idents.builtin_module; - if (is_builtin) { - break :blk try self.instantiateVar(copied_var, env, .{ .explicit = region }); - } else { - break :blk copied_var; - } + // Instantiate the copied var to convert rigid type variables to flex, + // so they can unify with the call site + break :blk try self.instantiateVar(copied_var, env, .{ .explicit = region }); }; // Unify the actual function var against the inferred var @@ -4930,7 +5010,6 @@ fn checkDeferredStaticDispatchConstraints(self: *Self, env: *Env) std.mem.Alloca } } else { // If the root type is anything but a nominal type or anonymous structural type, push an error - const constraints = self.types.sliceStaticDispatchConstraints(deferred_constraint.constraints); if (constraints.len > 0) { try self.reportConstraintError( @@ -4948,7 +5027,7 @@ fn checkDeferredStaticDispatchConstraints(self: *Self, env: *Env) std.mem.Alloca } // Now that we've processed all constraints, reset the array - env.deferred_static_dispatch_constraints.items.clearRetainingCapacity(); + env.deferred_static_dispatch_constraints.clearRetainingCapacity(); } /// Check if a structural type supports is_eq. diff --git a/src/check/occurs.zig b/src/check/occurs.zig index 4557b3202b2..2f62f5647ea 100644 --- a/src/check/occurs.zig +++ b/src/check/occurs.zig @@ -93,7 +93,7 @@ pub fn occurs(types_store: *Store, scratch: *Scratch, var_: Var) std.mem.Allocat }; // Reset the marks for all visited nodes - for (scratch.visited.items.items[0..]) |visited_desc_idx| { + for (scratch.visited.items()[0..]) |visited_desc_idx| { types_store.setDescMark(visited_desc_idx, Mark.none); } @@ -324,14 +324,14 @@ pub const Scratch = struct { } pub fn reset(self: *Self) void { - self.seen.items.clearRetainingCapacity(); - self.err_chain.items.clearRetainingCapacity(); - self.err_chain_nominal_vars.items.clearRetainingCapacity(); - self.visited.items.clearRetainingCapacity(); + self.seen.clearRetainingCapacity(); + self.err_chain.clearRetainingCapacity(); + self.err_chain_nominal_vars.clearRetainingCapacity(); + self.visited.clearRetainingCapacity(); } fn hasSeenVar(self: *const Self, var_: Var) bool { - for (self.seen.items.items) |seen_var| { + for (self.seen.items()) |seen_var| { if (seen_var == var_) return true; } return false; @@ -342,7 +342,7 @@ pub const Scratch = struct { } fn popSeen(self: *Self) void { - _ = self.seen.items.pop(); + _ = self.seen.pop(); } fn appendVisited(self: *Self, desc_idx: DescStoreIdx) std.mem.Allocator.Error!void { @@ -358,11 +358,11 @@ pub const Scratch = struct { } fn errChainSlice(self: *const Scratch) []const Var { - return self.err_chain.items.items; + return self.err_chain.items(); } fn errChainNominalVarsSlice(self: *const Scratch) []const Var { - return self.err_chain_nominal_vars.items.items; + return self.err_chain_nominal_vars.items(); } }; @@ -529,7 +529,7 @@ test "occurs: recursive tag union (v = [ Cons(elem, v), Nil ]" { try std.testing.expectEqual(1, err_chain.len); try std.testing.expectEqual(linked_list, err_chain[0]); - for (scratch.visited.items.items[0..]) |visited_desc_idx| { + for (scratch.visited.items()[0..]) |visited_desc_idx| { try std.testing.expectEqual(Mark.none, types_store.getDesc(visited_desc_idx).mark); } } @@ -574,7 +574,7 @@ test "occurs: nested recursive tag union (v = [ Cons(elem, Box(v)) ] )" { try std.testing.expectEqual(err_chain[0], boxed_linked_list); try std.testing.expectEqual(err_chain[1], linked_list); - for (scratch.visited.items.items[0..]) |visited_desc_idx| { + for (scratch.visited.items()[0..]) |visited_desc_idx| { try std.testing.expectEqual(Mark.none, types_store.getDesc(visited_desc_idx).mark); } } @@ -617,7 +617,7 @@ test "occurs: recursive tag union (v = List: [ Cons(Elem, List), Nil ])" { try std.testing.expectEqual(1, err_chain_nominal1.len); try std.testing.expectEqual(nominal_type, err_chain_nominal1[0]); - for (scratch.visited.items.items[0..]) |visited_desc_idx| { + for (scratch.visited.items()[0..]) |visited_desc_idx| { try std.testing.expectEqual(Mark.none, types_store.getDesc(visited_desc_idx).mark); } @@ -635,7 +635,7 @@ test "occurs: recursive tag union (v = List: [ Cons(Elem, List), Nil ])" { try std.testing.expectEqual(1, err_chain_nominal2.len); try std.testing.expectEqual(nominal_type, err_chain_nominal2[0]); - for (scratch.visited.items.items[0..]) |visited_desc_idx| { + for (scratch.visited.items()[0..]) |visited_desc_idx| { try std.testing.expectEqual(Mark.none, types_store.getDesc(visited_desc_idx).mark); } } @@ -691,7 +691,7 @@ test "occurs: recursive tag union with multiple nominals (TypeA := TypeB, TypeB try std.testing.expectEqual(type_b_nominal, err_chain_nominal1[0]); try std.testing.expectEqual(type_a_nominal, err_chain_nominal1[1]); - for (scratch.visited.items.items[0..]) |visited_desc_idx| { + for (scratch.visited.items()[0..]) |visited_desc_idx| { try std.testing.expectEqual(Mark.none, types_store.getDesc(visited_desc_idx).mark); } @@ -711,7 +711,7 @@ test "occurs: recursive tag union with multiple nominals (TypeA := TypeB, TypeB try std.testing.expectEqual(type_a_nominal, err_chain_nominal2[0]); try std.testing.expectEqual(type_b_nominal, err_chain_nominal2[1]); - for (scratch.visited.items.items[0..]) |visited_desc_idx| { + for (scratch.visited.items()[0..]) |visited_desc_idx| { try std.testing.expectEqual(Mark.none, types_store.getDesc(visited_desc_idx).mark); } @@ -731,7 +731,7 @@ test "occurs: recursive tag union with multiple nominals (TypeA := TypeB, TypeB try std.testing.expectEqual(type_b_nominal, err_chain_nominal3[0]); try std.testing.expectEqual(type_a_nominal, err_chain_nominal3[1]); - for (scratch.visited.items.items[0..]) |visited_desc_idx| { + for (scratch.visited.items()[0..]) |visited_desc_idx| { try std.testing.expectEqual(Mark.none, types_store.getDesc(visited_desc_idx).mark); } } diff --git a/src/check/problem.zig b/src/check/problem.zig index 81e65ed954b..f36ee62c28c 100644 --- a/src/check/problem.zig +++ b/src/check/problem.zig @@ -2310,8 +2310,8 @@ pub const ReportBuilder = struct { // Get module name if available const module_idx = @intFromEnum(data.module_idx); - const module_name = if (module_idx < self.can_ir.imports.imports.len()) blk: { - const import_string_idx = self.can_ir.imports.imports.items.items[module_idx]; + const module_name = if (module_idx < self.can_ir.imports.len()) blk: { + const import_string_idx = self.can_ir.imports.imports.field(.str_idx)[module_idx]; const import_name = self.can_ir.getString(import_string_idx); break :blk import_name; } else null; diff --git a/src/check/test/TestEnv.zig b/src/check/test/TestEnv.zig index 670148855e6..1469a7b397b 100644 --- a/src/check/test/TestEnv.zig +++ b/src/check/test/TestEnv.zig @@ -226,8 +226,8 @@ pub fn initWithImport(module_name: []const u8, source: []const u8, other_module_ 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_count = module_env.imports.len(); + for (module_env.imports.imports.field(.str_idx)[0..import_count]) |str_idx| { const import_name = module_env.getString(str_idx); if (std.mem.eql(u8, import_name, other_module_name)) { // Cross-module import - append the other test module's env diff --git a/src/check/test/unify_test.zig b/src/check/test/unify_test.zig index f2c8cdee3b9..bca3e303354 100644 --- a/src/check/test/unify_test.zig +++ b/src/check/test/unify_test.zig @@ -1023,8 +1023,9 @@ test "partitionTags - same tags" { var env = try TestEnv.init(gpa); defer env.deinit(); - const tag_x = try env.mkTag("X", &[_]Var{@enumFromInt(0)}); - const tag_y = try env.mkTag("Y", &[_]Var{@enumFromInt(1)}); + // Var indices are 1-based (0 is sentinel) + const tag_x = try env.mkTag("X", &[_]Var{@enumFromInt(1)}); + const tag_y = try env.mkTag("Y", &[_]Var{@enumFromInt(2)}); const range = try env.scratch.appendSliceGatheredTags(&[_]Tag{ tag_x, tag_y }); @@ -1046,9 +1047,10 @@ test "partitionTags - disjoint fields" { var env = try TestEnv.init(gpa); defer env.deinit(); - const a1 = try env.mkTag("A1", &[_]Var{@enumFromInt(0)}); - const a2 = try env.mkTag("A2", &[_]Var{@enumFromInt(1)}); - const b1 = try env.mkTag("B1", &[_]Var{@enumFromInt(2)}); + // Var indices are 1-based (0 is sentinel) + const a1 = try env.mkTag("A1", &[_]Var{@enumFromInt(1)}); + const a2 = try env.mkTag("A2", &[_]Var{@enumFromInt(2)}); + const b1 = try env.mkTag("B1", &[_]Var{@enumFromInt(3)}); const a_range = try env.scratch.appendSliceGatheredTags(&[_]Tag{ a1, a2 }); const b_range = try env.scratch.appendSliceGatheredTags(&[_]Tag{b1}); @@ -1072,9 +1074,10 @@ test "partitionTags - overlapping tags" { var env = try TestEnv.init(gpa); defer env.deinit(); - const a1 = try env.mkTag("A", &[_]Var{@enumFromInt(0)}); - const both = try env.mkTag("Both", &[_]Var{@enumFromInt(1)}); - const b1 = try env.mkTag("B", &[_]Var{@enumFromInt(2)}); + // Var indices are 1-based (0 is sentinel) + const a1 = try env.mkTag("A", &[_]Var{@enumFromInt(1)}); + const both = try env.mkTag("Both", &[_]Var{@enumFromInt(2)}); + const b1 = try env.mkTag("B", &[_]Var{@enumFromInt(3)}); const a_range = try env.scratch.appendSliceGatheredTags(&[_]Tag{ a1, both }); const b_range = try env.scratch.appendSliceGatheredTags(&[_]Tag{ b1, both }); @@ -1101,9 +1104,10 @@ test "partitionTags - reordering is normalized" { var env = try TestEnv.init(gpa); defer env.deinit(); - const f1 = try env.mkTag("F1", &[_]Var{@enumFromInt(0)}); - const f2 = try env.mkTag("F2", &[_]Var{@enumFromInt(1)}); - const f3 = try env.mkTag("F3", &[_]Var{@enumFromInt(2)}); + // Var indices are 1-based (0 is sentinel) + const f1 = try env.mkTag("F1", &[_]Var{@enumFromInt(1)}); + const f2 = try env.mkTag("F2", &[_]Var{@enumFromInt(2)}); + const f3 = try env.mkTag("F3", &[_]Var{@enumFromInt(3)}); const a_range = try env.scratch.appendSliceGatheredTags(&[_]Tag{ f3, f1, f2 }); const b_range = try env.scratch.appendSliceGatheredTags(&[_]Tag{ f1, f2, f3 }); @@ -1269,7 +1273,7 @@ test "unify - closed tag union extends open" { // check that fresh vars are correct try std.testing.expectEqual(1, env.scratch.fresh_vars.len()); - try std.testing.expectEqual(b_tag_union.ext, env.scratch.fresh_vars.items.items[0]); + try std.testing.expectEqual(b_tag_union.ext, env.scratch.fresh_vars.items()[0]); } // unification - recursion // @@ -1483,7 +1487,8 @@ test "unify - flex with constraints vs structure captures deferred check" { // Check that constraint was captured try std.testing.expectEqual(1, env.scratch.deferred_constraints.len()); - const deferred = env.scratch.deferred_constraints.get(@enumFromInt(0)).*; + // SafeList uses 1-based indexing, so first element is at index 1 + const deferred = env.scratch.deferred_constraints.get(@enumFromInt(1)).*; try std.testing.expectEqual( env.module_env.types.resolveVar(structure_var).var_, env.module_env.types.resolveVar(deferred.var_).var_, @@ -1518,7 +1523,8 @@ test "unify - structure vs flex with constraints captures deferred check (revers // Check that constraint was captured (note: vars might be swapped due to merge order) try std.testing.expectEqual(1, env.scratch.deferred_constraints.len()); - const deferred = env.scratch.deferred_constraints.get(@enumFromInt(0)).*; + // SafeList uses 1-based indexing, so first element is at index 1 + const deferred = env.scratch.deferred_constraints.get(@enumFromInt(1)).*; try std.testing.expectEqual( env.module_env.types.resolveVar(flex_var).var_, env.module_env.types.resolveVar(deferred.var_).var_, @@ -1571,7 +1577,8 @@ test "unify - flex vs nominal type captures constraint" { // Check that constraint was captured try std.testing.expectEqual(1, env.scratch.deferred_constraints.len()); - const deferred = env.scratch.deferred_constraints.get(@enumFromInt(0)).*; + // SafeList uses 1-based indexing, so first element is at index 1 + const deferred = env.scratch.deferred_constraints.get(@enumFromInt(1)).*; try std.testing.expectEqual( env.module_env.types.resolveVar(nominal_var).var_, env.module_env.types.resolveVar(deferred.var_).var_, diff --git a/src/check/unify.zig b/src/check/unify.zig index 74bdb8467e9..eaaf91e2aa2 100644 --- a/src/check/unify.zig +++ b/src/check/unify.zig @@ -1421,9 +1421,12 @@ const Unifier = struct { } fn trackNewVars(self: *Self, start_slots: u64) error{AllocatorError}!void { - var slot = start_slots; + // With 1-based indexing: if len() was N before adding, elements were at indices 1..N + // After adding K elements, len() is N+K, elements are at indices 1..N+K + // So new elements are at indices (N+1)..(N+K), i.e., (start_slots+1)..end_slots (inclusive) const end_slots = self.types_store.len(); - while (slot < end_slots) : (slot += 1) { + var slot = start_slots + 1; // Start at first new index (1-based) + while (slot <= end_slots) : (slot += 1) { const new_var = @as(Var, @enumFromInt(@as(u32, @intCast(slot)))); _ = self.scratch.fresh_vars.append(self.scratch.gpa, new_var) catch return error.AllocatorError; } @@ -1939,9 +1942,9 @@ const Unifier = struct { std.mem.sort(RecordField, b_fields, ident_store, comptime RecordField.sortByNameAsc); // Get the start of index of the new range - const a_fields_start: u32 = @intCast(scratch.only_in_a_fields.len()); - const b_fields_start: u32 = @intCast(scratch.only_in_b_fields.len()); - const both_fields_start: u32 = @intCast(scratch.in_both_fields.len()); + const a_fields_start: u32 = @intCast(scratch.only_in_a_fields.len() + 1); + const b_fields_start: u32 = @intCast(scratch.only_in_b_fields.len() + 1); + const both_fields_start: u32 = @intCast(scratch.in_both_fields.len() + 1); // Iterate over the fields in order, grouping them var a_i: usize = 0; @@ -2345,9 +2348,9 @@ const Unifier = struct { std.mem.sort(Tag, b_tags, ident_store, comptime Tag.sortByNameAsc); // Get the start of index of the new range - const a_tags_start: u32 = @intCast(scratch.only_in_a_tags.len()); - const b_tags_start: u32 = @intCast(scratch.only_in_b_tags.len()); - const both_tags_start: u32 = @intCast(scratch.in_both_tags.len()); + const a_tags_start: u32 = @intCast(scratch.only_in_a_tags.len() + 1); + const b_tags_start: u32 = @intCast(scratch.only_in_b_tags.len() + 1); + const both_tags_start: u32 = @intCast(scratch.in_both_tags.len() + 1); // Iterate over the tags in order, grouping them var a_i: usize = 0; @@ -2471,24 +2474,24 @@ const Unifier = struct { } } - const top: u32 = @intCast(self.types_store.static_dispatch_constraints.len()); + const top: u32 = @intCast(self.types_store.static_dispatch_constraints.len() + 1); // Ensure we have enough memory for the new contiguous list const capacity = partitioned.in_both.len() + partitioned.only_in_a.len() + partitioned.only_in_b.len(); - self.types_store.static_dispatch_constraints.items.ensureUnusedCapacity( + self.types_store.static_dispatch_constraints.ensureUnusedCapacity( self.types_store.gpa, capacity, ) catch return Error.AllocatorError; for (self.scratch.in_both_static_dispatch_constraints.sliceRange(partitioned.in_both)) |two_constraints| { // Here, we append the constraint's b, but since a & b, it doesn't actually matter - self.types_store.static_dispatch_constraints.items.appendAssumeCapacity(two_constraints.b); + _ = self.types_store.static_dispatch_constraints.appendAssumeCapacity(two_constraints.b); } for (self.scratch.only_in_a_static_dispatch_constraints.sliceRange(partitioned.only_in_a)) |only_a| { - self.types_store.static_dispatch_constraints.items.appendAssumeCapacity(only_a); + _ = self.types_store.static_dispatch_constraints.appendAssumeCapacity(only_a); } for (self.scratch.only_in_b_static_dispatch_constraints.sliceRange(partitioned.only_in_b)) |only_b| { - self.types_store.static_dispatch_constraints.items.appendAssumeCapacity(only_b); + _ = self.types_store.static_dispatch_constraints.appendAssumeCapacity(only_b); } return self.types_store.static_dispatch_constraints.rangeToEnd(top); @@ -2566,9 +2569,9 @@ const Unifier = struct { std.mem.sort(StaticDispatchConstraint, b_constraints, ident_store, comptime StaticDispatchConstraint.sortByFnNameAsc); // Get the start of index of the new range - const a_constraints_start: u32 = @intCast(scratch.only_in_a_static_dispatch_constraints.len()); - const b_constraints_start: u32 = @intCast(scratch.only_in_b_static_dispatch_constraints.len()); - const both_constraints_start: u32 = @intCast(scratch.in_both_static_dispatch_constraints.len()); + const a_constraints_start: u32 = @intCast(scratch.only_in_a_static_dispatch_constraints.len() + 1); + const b_constraints_start: u32 = @intCast(scratch.only_in_b_static_dispatch_constraints.len() + 1); + const both_constraints_start: u32 = @intCast(scratch.in_both_static_dispatch_constraints.len() + 1); // Iterate over the fields in order, grouping them var a_i: usize = 0; @@ -2777,18 +2780,18 @@ pub const Scratch = struct { /// Reset the scratch arrays, retaining the allocated memory pub fn reset(self: *Scratch) void { - self.gathered_fields.items.clearRetainingCapacity(); - self.only_in_a_fields.items.clearRetainingCapacity(); - self.only_in_b_fields.items.clearRetainingCapacity(); - self.in_both_fields.items.clearRetainingCapacity(); - self.gathered_tags.items.clearRetainingCapacity(); - self.only_in_a_tags.items.clearRetainingCapacity(); - self.only_in_b_tags.items.clearRetainingCapacity(); - self.in_both_tags.items.clearRetainingCapacity(); - self.deferred_constraints.items.clearRetainingCapacity(); - self.only_in_a_static_dispatch_constraints.items.clearRetainingCapacity(); - self.only_in_b_static_dispatch_constraints.items.clearRetainingCapacity(); - self.in_both_static_dispatch_constraints.items.clearRetainingCapacity(); + self.gathered_fields.clearRetainingCapacity(); + self.only_in_a_fields.clearRetainingCapacity(); + self.only_in_b_fields.clearRetainingCapacity(); + self.in_both_fields.clearRetainingCapacity(); + self.gathered_tags.clearRetainingCapacity(); + self.only_in_a_tags.clearRetainingCapacity(); + self.only_in_b_tags.clearRetainingCapacity(); + self.in_both_tags.clearRetainingCapacity(); + self.deferred_constraints.clearRetainingCapacity(); + self.only_in_a_static_dispatch_constraints.clearRetainingCapacity(); + self.only_in_b_static_dispatch_constraints.clearRetainingCapacity(); + self.in_both_static_dispatch_constraints.clearRetainingCapacity(); self.occurs_scratch.reset(); self.err = null; } @@ -2802,7 +2805,7 @@ pub const Scratch = struct { multi_list: *const RecordFieldSafeMultiList, range: RecordFieldSafeMultiList.Range, ) std.mem.Allocator.Error!RecordFieldSafeList.Range { - const start_int = self.gathered_fields.len(); + const start_int = self.gathered_fields.len() + 1; const record_fields_slice = multi_list.sliceRange(range); for (record_fields_slice.items(.name), record_fields_slice.items(.var_)) |name, var_| { _ = try self.gathered_fields.append( @@ -2820,7 +2823,7 @@ pub const Scratch = struct { multi_list: *const TagSafeMultiList, range: TagSafeMultiList.Range, ) std.mem.Allocator.Error!TagSafeList.Range { - const start_int = self.gathered_tags.len(); + const start_int = self.gathered_tags.len() + 1; const tag_slice = multi_list.sliceRange(range); for (tag_slice.items(.name), tag_slice.items(.args)) |ident, args| { _ = try self.gathered_tags.append( diff --git a/src/cli/main.zig b/src/cli/main.zig index aad89d650f3..654abba0165 100644 --- a/src/cli/main.zig +++ b/src/cli/main.zig @@ -1692,7 +1692,7 @@ pub fn setupSharedMemoryWithModuleEnv(allocs: *Allocators, roc_file_path: []cons var platform_to_app_idents = std.AutoHashMap(base.Ident.Idx, base.Ident.Idx).init(allocs.gpa); defer platform_to_app_idents.deinit(); - for (penv.requires_types.items.items) |required_type| { + for (penv.requires_types.items()) |required_type| { const platform_ident_text = penv.getIdent(required_type.ident); if (app_env.common.findIdent(platform_ident_text)) |app_ident| { try platform_to_app_idents.put(required_type.ident, app_ident); diff --git a/src/collections/safe_list.zig b/src/collections/safe_list.zig index 8dd6abbf1ad..865e670e852 100644 --- a/src/collections/safe_list.zig +++ b/src/collections/safe_list.zig @@ -77,8 +77,11 @@ pub fn SafeRange(comptime Idx: type) type { }; } -/// Wraps a `std.ArrayList` to provide a list that's safer to access -/// with arbitrary indices. +/// A list that's safer to access with arbitrary indices. +/// +/// Internally uses an offset pointer scheme where Idx 0 is reserved as a +/// sentinel value (meaning "missing"). The pointer is stored offset by -1 +/// element so that Idx 1 maps to the first actual element at offset 0. /// /// Use this for values that aren't structs with more than one field. /// Those values would likely be better stored in a SafeMultiList. @@ -94,10 +97,20 @@ pub fn SafeRange(comptime Idx: type) type { /// that hold T's, giving type safety. Also, out-of-bounds errors are /// less likely since indices are only created for valid list entries. pub fn SafeList(comptime T: type) type { - return struct { - items: std.ArrayList(T) = .{}, + return extern struct { + /// Pointer offset by -1 element. When we allocate memory at address A, + /// we store (A - @sizeOf(T)) here. This means Idx 1 maps to A[0], + /// Idx 2 maps to A[1], etc. Idx 0 is reserved as a sentinel. + offset_ptr: [*]T = undefined, + /// Number of actual elements stored (not including the sentinel slot) + length: u32 = 0, + /// Allocated capacity (not including the sentinel slot) + capacity: u32 = 0, - /// An index for an item in the list. + const Self = @This(); + + /// An index for an item in the list. Idx 0 is reserved as a sentinel + /// meaning "missing" and will never be returned by append operations. pub const Idx = enum(u32) { _, @@ -112,8 +125,27 @@ pub fn SafeList(comptime T: type) type { } }; + /// An index that may be empty (Idx 0 represents "missing"). + /// Use get() to convert to an optional Idx. + pub const MaybeIdx = enum(u32) { + none = 0, + _, + + /// Convert to an optional Idx. Returns null if this is the sentinel (0). + pub fn get(self: MaybeIdx) ?Idx { + const int_value = @intFromEnum(self); + if (int_value == 0) return null; + return @enumFromInt(int_value); + } + + /// Create a MaybeIdx from an Idx. + pub fn from(idx: Idx) MaybeIdx { + return @enumFromInt(@intFromEnum(idx)); + } + }; + /// A non-type-safe slice of the list. - pub const Slice = std.ArrayList(T).Slice; + pub const Slice = []T; /// A type-safe range of the list. pub const Range = SafeRange(Idx); @@ -133,12 +165,10 @@ pub fn SafeList(comptime T: type) type { /// Serialize a SafeList into this Serialized struct, appending data to the writer pub fn serialize( self: *Serialized, - safe_list: *const SafeList(T), + safe_list: *const Self, allocator: Allocator, writer: *CompactWriter, ) Allocator.Error!void { - const items = safe_list.items.items; - // Pad to the alignment of the slice elements. try writer.padToAlignment(allocator, @alignOf(T)); @@ -146,39 +176,39 @@ pub fn SafeList(comptime T: type) type { const data_offset = writer.total_bytes; // Append the raw data without further padding. - if (items.len > 0) { + if (safe_list.length > 0) { + // Get the actual data pointer (re-offset from our stored offset_ptr) + const actual_ptr = safe_list.getActualPtr(); try writer.iovecs.append(allocator, .{ - .iov_base = @ptrCast(items.ptr), - .iov_len = items.len * @sizeOf(T), + .iov_base = @ptrCast(actual_ptr), + .iov_len = safe_list.length * @sizeOf(T), }); - writer.total_bytes += items.len * @sizeOf(T); + writer.total_bytes += safe_list.length * @sizeOf(T); } self.offset = @intCast(data_offset); - self.len = items.len; - self.capacity = items.len; + self.len = safe_list.length; + self.capacity = safe_list.length; } /// Deserialize this Serialized struct into a SafeList - pub fn deserialize(self: *Serialized, offset: i64) *SafeList(T) { + pub fn deserialize(self: *Serialized, offset: i64) *Self { // Note: Serialized may be smaller than the runtime struct. // We deserialize by overwriting the Serialized memory with the runtime struct. - const safe_list = @as(*SafeList(T), @ptrFromInt(@intFromPtr(self))); + const safe_list = @as(*Self, @ptrFromInt(@intFromPtr(self))); // Handle empty list case if (self.len == 0) { - safe_list.* = SafeList(T){ - .items = .{}, - }; + safe_list.* = Self{}; } else { // Apply the offset to convert from serialized offset to actual pointer - const items_ptr: [*]T = @ptrFromInt(@as(usize, @intCast(self.offset + offset))); + const actual_ptr: [*]T = @ptrFromInt(@as(usize, @intCast(self.offset + offset))); - safe_list.* = SafeList(T){ - .items = .{ - .items = items_ptr[0..@intCast(self.len)], - .capacity = @intCast(self.capacity), - }, + // Store the offset pointer (actual_ptr - 1 element) + safe_list.* = Self{ + .offset_ptr = @ptrFromInt(@intFromPtr(actual_ptr) -% @sizeOf(T)), + .length = @intCast(self.len), + .capacity = @intCast(self.capacity), }; } @@ -186,83 +216,192 @@ pub fn SafeList(comptime T: type) type { } }; + /// Get the actual pointer to the start of allocated memory. + /// This is offset_ptr + 1 element. + inline fn getActualPtr(self: *const Self) [*]T { + return @ptrFromInt(@intFromPtr(self.offset_ptr) +% @sizeOf(T)); + } + + /// Get a slice of all items in this list. + pub fn items(self: *const Self) []T { + if (self.capacity == 0) return &[_]T{}; + return self.getActualPtr()[0..self.length]; + } + + /// Get a mutable slice including capacity beyond current length. + /// Use with caution - this allows direct manipulation of internal storage. + pub fn rawCapacitySlice(self: *Self) []T { + if (self.capacity == 0) return &[_]T{}; + return self.getActualPtr()[0..self.capacity]; + } + + /// Directly set the length of the list. + /// Use with caution - caller must ensure length <= capacity and + /// that all items in the range [0, new_length) are initialized. + pub fn setLen(self: *Self, new_length: u32) void { + std.debug.assert(new_length <= self.capacity); + self.length = new_length; + } + /// Initialize the `SafeList` with the specified capacity. - pub fn initCapacity(gpa: Allocator, capacity: usize) std.mem.Allocator.Error!SafeList(T) { - return .{ - .items = try std.ArrayList(T).initCapacity(gpa, capacity), + pub fn initCapacity(gpa: Allocator, cap: usize) std.mem.Allocator.Error!Self { + if (cap == 0) { + return Self{}; + } + const actual_ptr = try gpa.alloc(T, cap); + return Self{ + .offset_ptr = @ptrFromInt(@intFromPtr(actual_ptr.ptr) -% @sizeOf(T)), + .length = 0, + .capacity = @intCast(cap), }; } /// Deinitialize the memory of this `SafeList`. - pub fn deinit(self: *SafeList(T), gpa: Allocator) void { - self.items.deinit(gpa); + pub fn deinit(self: *Self, gpa: Allocator) void { + if (self.capacity == 0) return; + // Re-offset the pointer to get the actual allocation address + const actual_ptr = self.getActualPtr(); + gpa.free(actual_ptr[0..self.capacity]); + self.* = Self{}; } /// Get the length of this list. - pub fn len(self: *const SafeList(T)) u64 { - return @intCast(self.items.items.len); + pub fn len(self: *const Self) u32 { + return self.length; } - /// Add an item to the end of this list. - pub fn append(self: *SafeList(T), gpa: Allocator, item: T) std.mem.Allocator.Error!Idx { - const length = self.len(); - try self.items.append(gpa, item); + /// Clear the list while retaining allocated capacity. + pub fn clearRetainingCapacity(self: *Self) void { + self.length = 0; + } - return @enumFromInt(@as(u32, @intCast(length))); + /// Pop the last element from the list. + pub fn pop(self: *Self) ?T { + if (self.length == 0) return null; + const idx = self.length; + self.length -= 1; + return self.offset_ptr[idx]; } - /// Add a new item to the end of this list assuming capacity is sufficient to hold an additional item. - pub fn appendAssumeCapacity(self: *SafeList(T), item: T) Idx { - const length = self.len(); - self.items.appendAssumeCapacity(item); + /// Ensure total capacity for at least `new_capacity` elements. + pub fn ensureTotalCapacity(self: *Self, gpa: Allocator, new_capacity: usize) std.mem.Allocator.Error!void { + if (new_capacity <= self.capacity) return; + + const new_cap: u32 = @intCast(new_capacity); + if (self.capacity == 0) { + const actual_ptr = try gpa.alloc(T, new_cap); + self.offset_ptr = @ptrFromInt(@intFromPtr(actual_ptr.ptr) -% @sizeOf(T)); + self.capacity = new_cap; + } else { + const actual_ptr = self.getActualPtr(); + const old_slice = actual_ptr[0..self.capacity]; + if (gpa.resize(old_slice, new_cap)) { + self.capacity = new_cap; + } else { + const new_slice = try gpa.alloc(T, new_cap); + @memcpy(new_slice[0..self.length], actual_ptr[0..self.length]); + gpa.free(old_slice); + self.offset_ptr = @ptrFromInt(@intFromPtr(new_slice.ptr) -% @sizeOf(T)); + self.capacity = new_cap; + } + } + } - return @enumFromInt(@as(u32, @intCast(length))); + /// Ensure unused capacity for at least `additional` more elements. + pub fn ensureUnusedCapacity(self: *Self, gpa: Allocator, additional: usize) std.mem.Allocator.Error!void { + return self.ensureTotalCapacity(gpa, self.length + additional); + } + + /// Ensure capacity is exactly `new_capacity` elements. + /// Like ensureTotalCapacity but guarantees the exact capacity. + pub fn ensureTotalCapacityPrecise(self: *Self, gpa: Allocator, new_capacity: usize) std.mem.Allocator.Error!void { + return self.ensureTotalCapacity(gpa, new_capacity); + } + + /// Add an item to the end of this list. + /// Returns an Idx starting from 1 (Idx 0 is reserved as sentinel). + pub fn append(self: *Self, gpa: Allocator, item: T) std.mem.Allocator.Error!Idx { + try self.ensureUnusedCapacity(gpa, 1); + return self.appendAssumeCapacity(item); + } + + /// Add a new item to the end of this list assuming capacity is sufficient to hold an additional item. + /// Returns an Idx starting from 1 (Idx 0 is reserved as sentinel). + pub fn appendAssumeCapacity(self: *Self, item: T) Idx { + std.debug.assert(self.length < self.capacity); + const actual_ptr = self.getActualPtr(); + actual_ptr[self.length] = item; + self.length += 1; + // Return index + 1 since Idx 0 is reserved + return @enumFromInt(self.length); } /// Create a range from the provided idx to the end of the list - pub fn rangeToEnd(self: *SafeList(T), start_int: u32) Range { - const len_int = self.len(); - std.debug.assert(start_int <= len_int); - return Range{ .start = @enumFromInt(start_int), .count = @intCast(len_int - start_int) }; + pub fn rangeToEnd(self: *Self, start_int: u32) Range { + // Note: start_int is a raw index (1-based for the first element) + // Length is the count, and the end index would be length + 1 + const end_int = self.length + 1; + std.debug.assert(start_int <= end_int); + return Range{ .start = @enumFromInt(start_int), .count = @intCast(end_int - start_int) }; } /// Add all the items in a slice to the end of this list. - pub fn appendSlice(self: *SafeList(T), gpa: Allocator, items: []const T) std.mem.Allocator.Error!Range { - const start_length = self.len(); - try self.items.appendSlice(gpa, items); - const end_length = self.len(); - return Range{ .start = @enumFromInt(start_length), .count = @intCast(end_length - start_length) }; + pub fn appendSlice(self: *Self, gpa: Allocator, slice: []const T) std.mem.Allocator.Error!Range { + // Start index is current length + 1 (since indices start at 1) + const start_idx = self.length + 1; + try self.ensureUnusedCapacity(gpa, slice.len); + const actual_ptr = self.getActualPtr(); + @memcpy(actual_ptr[self.length..][0..slice.len], slice); + self.length += @intCast(slice.len); + return Range{ .start = @enumFromInt(start_idx), .count = @intCast(slice.len) }; } /// Extend this list with all items generated by an iterator. - pub fn extendFromIter(self: *SafeList(T), gpa: Allocator, iter_extend: anytype) std.mem.Allocator.Error!Range { - const start_length = self.len(); - while (iter_extend.next()) |item| { - try self.items.append(gpa, item); + pub fn extendFromIter(self: *Self, gpa: Allocator, iter_extend: anytype) std.mem.Allocator.Error!Range { + const start_idx = self.length + 1; + var iter_copy = iter_extend; + while (iter_copy.next()) |item| { + _ = try self.append(gpa, item); } - const end_length = self.len(); - return Range{ .start = @enumFromInt(start_length), .count = @intCast(end_length - start_length) }; + const count = self.length + 1 - start_idx; + return Range{ .start = @enumFromInt(start_idx), .count = count }; } - /// Convert a range to a slice - pub fn sliceRange(self: *const SafeList(T), range: Range) Slice { - const start: usize = @intFromEnum(range.start); + /// Convert a range to a slice. + /// Note: Range indices are 1-based, so we subtract 1 to get the actual array index. + pub fn sliceRange(self: *const Self, range: Range) Slice { + // Handle empty range specially + if (range.count == 0) { + return self.items()[0..0]; + } + + // Convert 1-based Range indices to 0-based array indices + const start_idx = @intFromEnum(range.start); + std.debug.assert(start_idx >= 1); + const start: usize = start_idx - 1; const end: usize = start + range.count; std.debug.assert(start <= end); - std.debug.assert(end <= self.items.items.len); + std.debug.assert(end <= self.length); - return self.items.items[start..end]; + return self.items()[start..end]; } /// Get an item from this list without worrying about out-of-bounds errors. - pub fn get(self: *const SafeList(T), id: Idx) *T { - return &self.items.items[@as(usize, @intFromEnum(id))]; + /// Uses wrapping pointer arithmetic since offset_ptr is offset by -1 element. + pub fn get(self: *const Self, id: Idx) *T { + const idx = @intFromEnum(id); + std.debug.assert(idx >= 1 and idx <= self.length); + // offset_ptr[idx] gives us the correct element because: + // offset_ptr = actual_ptr - 1, so offset_ptr[1] = actual_ptr[0] + return &self.offset_ptr[idx]; } /// Set the value of an item in this list without worrying about out-of-bounds errors. - pub fn set(self: *const SafeList(T), id: Idx, value: T) void { - self.items.items[@as(usize, @intFromEnum(id))] = value; + pub fn set(self: *Self, id: Idx, value: T) void { + const idx = @intFromEnum(id); + std.debug.assert(idx >= 1 and idx <= self.length); + self.offset_ptr[idx] = value; } /// Returns a SafeList that has had its pointer converted to an offset. @@ -270,105 +409,111 @@ pub fn SafeList(comptime T: type) type { /// methods on it or dereference its internal "pointers" (which are now /// offsets) is illegal behavior! pub fn serialize( - self: *const SafeList(T), + self: *const Self, allocator: Allocator, writer: *CompactWriter, - ) Allocator.Error!*const SafeList(T) { - const items = self.items.items; - - const offset_self = try writer.appendAlloc(allocator, SafeList(T)); + ) Allocator.Error!*const Self { + const offset_self = try writer.appendAlloc(allocator, Self); - const slice = try writer.appendSlice(allocator, items); + if (self.length > 0) { + const slice = try writer.appendSlice(allocator, self.items()); - offset_self.* = .{ - .items = .{ - .items = slice, - .capacity = items.len, - }, - }; + offset_self.* = Self{ + // Store the offset_ptr as (slice.ptr - 1 element) + .offset_ptr = @ptrFromInt(@intFromPtr(slice.ptr) -% @sizeOf(T)), + .length = self.length, + .capacity = self.length, + }; + } else { + offset_self.* = Self{}; + } return @constCast(offset_self); } /// Add the given offset to the memory addresses of all pointers in `self`. - pub fn relocate(self: *SafeList(T), offset: isize) void { - if (self.items.capacity == 0) return; + pub fn relocate(self: *Self, offset: isize) void { + if (self.capacity == 0) return; - const old_addr: isize = @intCast(@intFromPtr(self.items.items.ptr)); + const old_addr: isize = @intCast(@intFromPtr(self.offset_ptr)); const new_addr = @as(usize, @intCast(old_addr + offset)); - self.items.items.ptr = @as([*]T, @ptrFromInt(new_addr)); + self.offset_ptr = @ptrFromInt(new_addr); } /// An iterator over all the indices in this list. + /// Yields indices starting from 1. pub const IndexIterator = struct { - len: usize, - current: usize, + /// End index (exclusive, 1-based: length + 1) + end: u32, + /// Current index (1-based, starts at 1) + current: u32, pub fn next(self: *IndexIterator) ?Idx { - if (self.len == self.current) { + if (self.current > self.end - 1) { return null; } const curr = self.current; self.current += 1; - const idx: u32 = @truncate(curr); - return @enumFromInt(idx); + return @enumFromInt(curr); } }; /// Iterate over all the indices of the items in this list. - pub fn iterIndices(self: *const SafeList(T)) IndexIterator { + /// Yields indices starting from 1. + pub fn iterIndices(self: *const Self) IndexIterator { return IndexIterator{ - .len = @intCast(self.len()), - .current = 0, + .end = self.length + 1, + .current = 1, }; } - /// An iterator over all the indices in this list. + /// An iterator over all the items in this list. pub const Iterator = struct { - array: *const SafeList(T), - len: u32, - current: Idx, + array: *const Self, + /// End index (exclusive, 1-based) + end: u32, + /// Current index (1-based) + current: u32, pub fn next(self: *Iterator) ?T { - const cur_idx = self.current; - const cur_int = @intFromEnum(cur_idx); - if (self.len == cur_int) { + if (self.current > self.end - 1) { return null; } - self.current = @enumFromInt(cur_int + 1); + const cur_idx: Idx = @enumFromInt(self.current); + self.current += 1; return self.array.get(cur_idx).*; } pub fn count(self: *Iterator) u32 { - return self.len - @intFromEnum(self.current); + if (self.current > self.end - 1) return 0; + return self.end - self.current; } pub fn shift(self: *Iterator) void { - const cur_int = @intFromEnum(self.current); - if (cur_int < self.len) { - self.current = @as(Idx, @enumFromInt(cur_int + 1)); - self.len -= 1; + if (self.current < self.end) { + self.current += 1; + self.end -= 1; } } }; /// Iterate over the elements in a span - pub fn iterRange(self: *const SafeList(T), range: Range) Iterator { + pub fn iterRange(self: *const Self, range: Range) Iterator { return Iterator{ .array = self, - .len = @intFromEnum(range.start) + range.count, - .current = range.start, + .end = @intFromEnum(range.start) + range.count, + .current = @intFromEnum(range.start), }; } /// Iterate over all items in this list. - pub fn iter(self: *const SafeList(T)) Iterator { + pub fn iter(self: *const Self) Iterator { return Iterator{ .array = self, - .len = self.len(), - .current = @enumFromInt(0), + .end = self.length + 1, + .current = 1, }; } }; @@ -734,12 +879,12 @@ test "SafeList(u8) appendSlice" { defer list.deinit(gpa); const rangeA = try list.appendSlice(gpa, &[_]u8{ 'a', 'b', 'c', 'd' }); - try testing.expectEqual(0, @intFromEnum(rangeA.start)); - try testing.expectEqual(4, @intFromEnum(rangeA.end())); + try testing.expectEqual(1, @intFromEnum(rangeA.start)); + try testing.expectEqual(5, @intFromEnum(rangeA.end())); const rangeB = try list.appendSlice(gpa, &[_]u8{ 'd', 'e', 'f', 'g' }); - try testing.expectEqual(4, @intFromEnum(rangeB.start)); - try testing.expectEqual(8, @intFromEnum(rangeB.end())); + try testing.expectEqual(5, @intFromEnum(rangeB.start)); + try testing.expectEqual(9, @intFromEnum(rangeB.end())); } test "SafeList(u8) sliceRange" { @@ -753,7 +898,8 @@ test "SafeList(u8) sliceRange" { try testing.expectEqual('a', sliceA[0]); try testing.expectEqual('d', sliceA[3]); - const rangeB = SafeList(u8).Range{ .start = @enumFromInt(2), .count = 2 }; + // Idx 3 is the third element ('c'), count 2 gives us 'c' and 'd' + const rangeB = SafeList(u8).Range{ .start = @enumFromInt(3), .count = 2 }; const sliceB = list.sliceRange(rangeB); try testing.expectEqual('c', sliceB[0]); try testing.expectEqual('d', sliceB[1]); @@ -961,9 +1107,9 @@ test "SafeList edge cases serialization" { const serialized_ptr = @as(*Container.Serialized, @ptrCast(@alignCast(buffer.ptr))); const deserialized = serialized_ptr.deserialize(@as(i64, @intCast(@intFromPtr(buffer.ptr)))); - try testing.expectEqual(@as(usize, 0), deserialized.list_u32.len()); - try testing.expectEqual(@as(usize, 1), deserialized.list_u8.len()); - try testing.expectEqual(@as(u8, 123), deserialized.list_u8.get(@enumFromInt(0)).*); + try testing.expectEqual(@as(u32, 0), deserialized.list_u32.len()); + try testing.expectEqual(@as(u32, 1), deserialized.list_u8.len()); + try testing.expectEqual(@as(u8, 123), deserialized.list_u8.get(@enumFromInt(1)).*); } } @@ -1048,11 +1194,11 @@ test "SafeList CompactWriter complete roundtrip example" { const deserialized = serialized_ptr.deserialize(@as(i64, @intCast(@intFromPtr(buffer.ptr)))); // Step 8: Verify data is accessible and correct - try testing.expectEqual(@as(usize, 4), deserialized.len()); - try testing.expectEqual(@as(u32, 100), deserialized.get(@enumFromInt(0)).*); - try testing.expectEqual(@as(u32, 200), deserialized.get(@enumFromInt(1)).*); - try testing.expectEqual(@as(u32, 300), deserialized.get(@enumFromInt(2)).*); - try testing.expectEqual(@as(u32, 400), deserialized.get(@enumFromInt(3)).*); + try testing.expectEqual(@as(u32, 4), deserialized.len()); + try testing.expectEqual(@as(u32, 100), deserialized.get(@enumFromInt(1)).*); + try testing.expectEqual(@as(u32, 200), deserialized.get(@enumFromInt(2)).*); + try testing.expectEqual(@as(u32, 300), deserialized.get(@enumFromInt(3)).*); + try testing.expectEqual(@as(u32, 400), deserialized.get(@enumFromInt(4)).*); } test "SafeList CompactWriter multiple lists with different alignments" { @@ -1155,10 +1301,10 @@ test "SafeList CompactWriter multiple lists with different alignments" { offset = std.mem.alignForward(usize, offset, @alignOf(u8)); offset += 3 * @sizeOf(u8); - try testing.expectEqual(@as(usize, 3), deser_u8.len()); - try testing.expectEqual(@as(u8, 10), deser_u8.get(@enumFromInt(0)).*); - try testing.expectEqual(@as(u8, 20), deser_u8.get(@enumFromInt(1)).*); - try testing.expectEqual(@as(u8, 30), deser_u8.get(@enumFromInt(2)).*); + try testing.expectEqual(@as(u32, 3), deser_u8.len()); + try testing.expectEqual(@as(u8, 10), deser_u8.get(@enumFromInt(1)).*); + try testing.expectEqual(@as(u8, 20), deser_u8.get(@enumFromInt(2)).*); + try testing.expectEqual(@as(u8, 30), deser_u8.get(@enumFromInt(3)).*); // 2. Deserialize u16 list offset = std.mem.alignForward(usize, offset, @alignOf(SafeList(u16).Serialized)); @@ -1169,9 +1315,9 @@ test "SafeList CompactWriter multiple lists with different alignments" { offset = std.mem.alignForward(usize, offset, @alignOf(u16)); offset += 2 * @sizeOf(u16); - try testing.expectEqual(@as(usize, 2), deser_u16.len()); - try testing.expectEqual(@as(u16, 1000), deser_u16.get(@enumFromInt(0)).*); - try testing.expectEqual(@as(u16, 2000), deser_u16.get(@enumFromInt(1)).*); + try testing.expectEqual(@as(u32, 2), deser_u16.len()); + try testing.expectEqual(@as(u16, 1000), deser_u16.get(@enumFromInt(1)).*); + try testing.expectEqual(@as(u16, 2000), deser_u16.get(@enumFromInt(2)).*); // 3. Deserialize u32 list offset = std.mem.alignForward(usize, offset, @alignOf(SafeList(u32).Serialized)); @@ -1182,11 +1328,11 @@ test "SafeList CompactWriter multiple lists with different alignments" { offset = std.mem.alignForward(usize, offset, @alignOf(u32)); offset += 4 * @sizeOf(u32); - try testing.expectEqual(@as(usize, 4), deser_u32.len()); - try testing.expectEqual(@as(u32, 100_000), deser_u32.get(@enumFromInt(0)).*); - try testing.expectEqual(@as(u32, 200_000), deser_u32.get(@enumFromInt(1)).*); - try testing.expectEqual(@as(u32, 300_000), deser_u32.get(@enumFromInt(2)).*); - try testing.expectEqual(@as(u32, 400_000), deser_u32.get(@enumFromInt(3)).*); + try testing.expectEqual(@as(u32, 4), deser_u32.len()); + try testing.expectEqual(@as(u32, 100_000), deser_u32.get(@enumFromInt(1)).*); + try testing.expectEqual(@as(u32, 200_000), deser_u32.get(@enumFromInt(2)).*); + try testing.expectEqual(@as(u32, 300_000), deser_u32.get(@enumFromInt(3)).*); + try testing.expectEqual(@as(u32, 400_000), deser_u32.get(@enumFromInt(4)).*); // 4. Deserialize u64 list offset = std.mem.alignForward(usize, offset, @alignOf(SafeList(u64).Serialized)); @@ -1197,22 +1343,22 @@ test "SafeList CompactWriter multiple lists with different alignments" { offset = std.mem.alignForward(usize, offset, @alignOf(u64)); offset += 2 * @sizeOf(u64); - try testing.expectEqual(@as(usize, 2), deser_u64.len()); - try testing.expectEqual(@as(u64, 10_000_000_000), deser_u64.get(@enumFromInt(0)).*); - try testing.expectEqual(@as(u64, 20_000_000_000), deser_u64.get(@enumFromInt(1)).*); + try testing.expectEqual(@as(u32, 2), deser_u64.len()); + try testing.expectEqual(@as(u64, 10_000_000_000), deser_u64.get(@enumFromInt(1)).*); + try testing.expectEqual(@as(u64, 20_000_000_000), deser_u64.get(@enumFromInt(2)).*); // 5. Deserialize struct list offset = std.mem.alignForward(usize, offset, @alignOf(SafeList(AlignedStruct).Serialized)); const s_struct = @as(*SafeList(AlignedStruct).Serialized, @ptrCast(@alignCast(buffer.ptr + offset))); const deser_struct = s_struct.deserialize(@as(i64, @intCast(base_addr))); - try testing.expectEqual(@as(usize, 2), deser_struct.len()); - const item0 = deser_struct.get(@enumFromInt(0)); + try testing.expectEqual(@as(u32, 2), deser_struct.len()); + const item0 = deser_struct.get(@enumFromInt(1)); try testing.expectEqual(@as(u32, 42), item0.x); try testing.expectEqual(@as(u64, 1337), item0.y); try testing.expectEqual(@as(u8, 255), item0.z); - const item1 = deser_struct.get(@enumFromInt(1)); + const item1 = deser_struct.get(@enumFromInt(2)); try testing.expectEqual(@as(u32, 99), item1.x); try testing.expectEqual(@as(u64, 9999), item1.y); try testing.expectEqual(@as(u8, 128), item1.z); @@ -1318,10 +1464,10 @@ test "SafeList CompactWriter interleaved pattern with alignment tracking" { offset = std.mem.alignForward(usize, offset, @alignOf(u8)); offset += 3; // 3 u8 elements - try testing.expectEqual(@as(usize, 3), d1.len()); - try testing.expectEqual(@as(u8, 1), d1.get(@enumFromInt(0)).*); - try testing.expectEqual(@as(u8, 2), d1.get(@enumFromInt(1)).*); - try testing.expectEqual(@as(u8, 3), d1.get(@enumFromInt(2)).*); + try testing.expectEqual(@as(u32, 3), d1.len()); + try testing.expectEqual(@as(u8, 1), d1.get(@enumFromInt(1)).*); + try testing.expectEqual(@as(u8, 2), d1.get(@enumFromInt(2)).*); + try testing.expectEqual(@as(u8, 3), d1.get(@enumFromInt(3)).*); // 2. Second list - u64 offset = std.mem.alignForward(usize, offset, @alignOf(SafeList(u64).Serialized)); @@ -1331,9 +1477,9 @@ test "SafeList CompactWriter interleaved pattern with alignment tracking" { offset = std.mem.alignForward(usize, offset, @alignOf(u64)); offset += 2 * @sizeOf(u64); // 2 u64 elements - try testing.expectEqual(@as(usize, 2), d2.len()); - try testing.expectEqual(@as(u64, 1_000_000), d2.get(@enumFromInt(0)).*); - try testing.expectEqual(@as(u64, 2_000_000), d2.get(@enumFromInt(1)).*); + try testing.expectEqual(@as(u32, 2), d2.len()); + try testing.expectEqual(@as(u64, 1_000_000), d2.get(@enumFromInt(1)).*); + try testing.expectEqual(@as(u64, 2_000_000), d2.get(@enumFromInt(2)).*); // 3. Third list - u16 offset = std.mem.alignForward(usize, offset, @alignOf(SafeList(u16).Serialized)); @@ -1343,19 +1489,19 @@ test "SafeList CompactWriter interleaved pattern with alignment tracking" { offset = std.mem.alignForward(usize, offset, @alignOf(u16)); offset += 4 * @sizeOf(u16); // 4 u16 elements - try testing.expectEqual(@as(usize, 4), d3.len()); - try testing.expectEqual(@as(u16, 100), d3.get(@enumFromInt(0)).*); - try testing.expectEqual(@as(u16, 200), d3.get(@enumFromInt(1)).*); - try testing.expectEqual(@as(u16, 300), d3.get(@enumFromInt(2)).*); - try testing.expectEqual(@as(u16, 400), d3.get(@enumFromInt(3)).*); + try testing.expectEqual(@as(u32, 4), d3.len()); + try testing.expectEqual(@as(u16, 100), d3.get(@enumFromInt(1)).*); + try testing.expectEqual(@as(u16, 200), d3.get(@enumFromInt(2)).*); + try testing.expectEqual(@as(u16, 300), d3.get(@enumFromInt(3)).*); + try testing.expectEqual(@as(u16, 400), d3.get(@enumFromInt(4)).*); // 4. Fourth list - u32 offset = std.mem.alignForward(usize, offset, @alignOf(SafeList(u32).Serialized)); const s4 = @as(*SafeList(u32).Serialized, @ptrCast(@alignCast(buffer.ptr + offset))); const d4 = s4.deserialize(@as(i64, @intCast(base))); - try testing.expectEqual(@as(usize, 1), d4.len()); - try testing.expectEqual(@as(u32, 42), d4.get(@enumFromInt(0)).*); + try testing.expectEqual(@as(u32, 1), d4.len()); + try testing.expectEqual(@as(u32, 42), d4.get(@enumFromInt(1)).*); } test "SafeList CompactWriter brute-force alignment verification" { @@ -1459,11 +1605,11 @@ test "SafeList CompactWriter brute-force alignment verification" { offset = std.mem.alignForward(usize, offset, @alignOf(T)); offset += length * @sizeOf(T); - try testing.expectEqual(length, d1.len()); + try testing.expectEqual(@as(u32, @intCast(length)), d1.len()); i = 0; while (i < length) : (i += 1) { const expected = @as(T, @intCast(i + 1)); - const actual = d1.get(@enumFromInt(i)).*; + const actual = d1.get(@enumFromInt(i + 1)).*; try testing.expectEqual(expected, actual); } @@ -1475,15 +1621,15 @@ test "SafeList CompactWriter brute-force alignment verification" { offset = std.mem.alignForward(usize, offset, @alignOf(u8)); offset += 1; // 1 u8 element - try testing.expectEqual(@as(usize, 1), d_u8.len()); - try testing.expectEqual(@as(u8, 42), d_u8.get(@enumFromInt(0)).*); + try testing.expectEqual(@as(u32, 1), d_u8.len()); + try testing.expectEqual(@as(u8, 42), d_u8.get(@enumFromInt(1)).*); // Second list offset = std.mem.alignForward(usize, offset, @alignOf(SafeList(T).Serialized)); const s2 = @as(*SafeList(T).Serialized, @ptrCast(@alignCast(buffer.ptr + offset))); const d2 = s2.deserialize(@as(i64, @intCast(base))); - try testing.expectEqual(length, d2.len()); + try testing.expectEqual(@as(u32, @intCast(length)), d2.len()); i = 0; while (i < length) : (i += 1) { const multiplier: T = switch (T) { @@ -1492,7 +1638,7 @@ test "SafeList CompactWriter brute-force alignment verification" { else => 100000, }; const expected = @as(T, @intCast(i + 1)) * multiplier; - const actual = d2.get(@enumFromInt(i)).*; + const actual = d2.get(@enumFromInt(i + 1)).*; try testing.expectEqual(expected, actual); } } @@ -1551,7 +1697,7 @@ test "SafeMultiList CompactWriter roundtrip with file" { const deserialized = serialized_ptr.deserialize(@as(i64, @intCast(@intFromPtr(buffer.ptr)))); // Verify the data - try testing.expectEqual(@as(usize, 4), deserialized.len()); + try testing.expectEqual(@as(u32, 4), deserialized.len()); // Verify all the data try testing.expectEqual(@as(u32, 100), deserialized.get(@enumFromInt(0)).id); @@ -1704,7 +1850,7 @@ test "SafeMultiList CompactWriter multiple lists different alignments" { // Deserialize list1 (at offset1) const d1_serialized = @as(*SafeMultiList(Type1).Serialized, @ptrCast(@alignCast(buffer.ptr + offset1))); const d1 = d1_serialized.deserialize(base); - try testing.expectEqual(@as(usize, 3), d1.len()); + try testing.expectEqual(@as(u32, 3), d1.len()); try testing.expectEqual(@as(u8, 10), d1.get(@enumFromInt(0)).a); try testing.expectEqual(@as(u16, 100), d1.get(@enumFromInt(0)).b); try testing.expectEqual(@as(u8, 20), d1.get(@enumFromInt(1)).a); @@ -1715,14 +1861,14 @@ test "SafeMultiList CompactWriter multiple lists different alignments" { // Deserialize list2 (at offset2) const d2_serialized = @as(*SafeMultiList(Type2).Serialized, @ptrCast(@alignCast(buffer.ptr + offset2))); const d2 = d2_serialized.deserialize(base); - try testing.expectEqual(@as(usize, 2), d2.len()); + try testing.expectEqual(@as(u32, 2), d2.len()); try testing.expectEqual(@as(u32, 1000), d2.get(@enumFromInt(0)).x); try testing.expectEqual(@as(u64, 10000), d2.get(@enumFromInt(0)).y); // Deserialize list3 (at offset3) const d3_serialized = @as(*SafeMultiList(Type3).Serialized, @ptrCast(@alignCast(buffer.ptr + offset3))); const d3 = d3_serialized.deserialize(base); - try testing.expectEqual(@as(usize, 2), d3.len()); + try testing.expectEqual(@as(u32, 2), d3.len()); try testing.expectEqual(@as(u64, 999), d3.get(@enumFromInt(0)).id); try testing.expectEqual(@as(u8, 42), d3.get(@enumFromInt(0)).data); try testing.expectEqual(true, d3.get(@enumFromInt(0)).flag); @@ -1815,12 +1961,12 @@ test "SafeMultiList CompactWriter brute-force alignment verification" { const d2_serialized = @as(*SafeMultiList(TestType).Serialized, @ptrCast(@alignCast(buffer.ptr + offset2))); const d2 = d2_serialized.deserialize(base); if (length > 0) { - try testing.expectEqual(@as(usize, 1), d2.len()); + try testing.expectEqual(@as(u32, 1), d2.len()); try testing.expectEqual(@as(u8, 255), d2.get(@enumFromInt(0)).a); try testing.expectEqual(@as(u32, 999999), d2.get(@enumFromInt(0)).b); try testing.expectEqual(@as(u64, 888888888), d2.get(@enumFromInt(0)).c); } else { - try testing.expectEqual(@as(usize, 0), d2.len()); + try testing.expectEqual(@as(u32, 0), d2.len()); } } } diff --git a/src/compile/cache_module.zig b/src/compile/cache_module.zig index 3466f8e5a9d..4caac723e81 100644 --- a/src/compile/cache_module.zig +++ b/src/compile/cache_module.zig @@ -96,7 +96,7 @@ pub const CacheModule = struct { var writer = CompactWriter.init(); // Allocate space for ModuleEnv.Serialized - const env_ptr = try writer.appendAlloc(arena_allocator, ModuleEnv); + const env_ptr = try writer.appendAlloc(arena_allocator, ModuleEnv.Serialized); const serialized_ptr = @as(*ModuleEnv.Serialized, @ptrCast(@alignCast(env_ptr))); // Serialize the ModuleEnv diff --git a/src/compile/compile_build.zig b/src/compile/compile_build.zig index d1df2eccb16..e053b5cb764 100644 --- a/src/compile/compile_build.zig +++ b/src/compile/compile_build.zig @@ -585,7 +585,7 @@ pub const BuildEnv = struct { const platform_root_env = platform_sched.getRootEnv() orelse return; // If the platform has no requires_types, nothing to check - if (platform_root_env.requires_types.items.items.len == 0) { + if (platform_root_env.requires_types.items().len == 0) { return; } @@ -644,7 +644,7 @@ pub const BuildEnv = struct { var platform_to_app_idents = std.AutoHashMap(base.Ident.Idx, base.Ident.Idx).init(self.gpa); defer platform_to_app_idents.deinit(); - for (platform_root_env.requires_types.items.items) |required_type| { + for (platform_root_env.requires_types.items()) |required_type| { const platform_ident_text = platform_root_env.getIdent(required_type.ident); if (app_root_env.common.findIdent(platform_ident_text)) |app_ident| { try platform_to_app_idents.put(required_type.ident, app_ident); diff --git a/src/compile/compile_package.zig b/src/compile/compile_package.zig index c83bb198641..26ace5735fa 100644 --- a/src/compile/compile_package.zig +++ b/src/compile/compile_package.zig @@ -632,11 +632,11 @@ pub const PackageEnv = struct { } // Discover imports from env.imports - const import_count = env.imports.imports.items.items.len; + const import_count = env.imports.len(); var any_new: bool = false; // Mark current node as visiting (gray) before exploring imports st.visit_color = 1; - for (env.imports.imports.items.items[0..import_count]) |str_idx| { + for (env.imports.imports.field(.str_idx)[0..import_count]) |str_idx| { const mod_name = env.getString(str_idx); // Skip "Builtin" - it's handled via the precompiled module in module_envs_map @@ -984,7 +984,7 @@ pub const PackageEnv = struct { var env = &st.env.?; // Build the array of all available modules for this module's imports - const import_count = env.imports.imports.items.items.len; + const import_count = env.imports.len(); var imported_envs = try std.ArrayList(*ModuleEnv).initCapacity(self.gpa, import_count); // NOTE: Don't deinit 'imported_envs' yet - comptime_evaluator holds a reference to imported_envs.items @@ -992,7 +992,7 @@ pub const PackageEnv = struct { try imported_envs.append(self.gpa, self.builtin_modules.builtin_module.env); // Add external and local modules - for (env.imports.imports.items.items[0..import_count]) |str_idx| { + for (env.imports.imports.field(.str_idx)[0..import_count]) |str_idx| { const import_name = env.getString(str_idx); // Skip Builtin - already added above diff --git a/src/compile/test/module_env_test.zig b/src/compile/test/module_env_test.zig index 3be090e41d4..ce31109ebde 100644 --- a/src/compile/test/module_env_test.zig +++ b/src/compile/test/module_env_test.zig @@ -125,7 +125,7 @@ test "ModuleEnv.Serialized roundtrip" { // Verify imports before serialization try testing.expectEqual(import1, import3); // Deduplication should work - try testing.expectEqual(@as(usize, 2), original.imports.imports.len()); // Should have 2 unique imports + try testing.expectEqual(@as(usize, 2), original.imports.len()); // Should have 2 unique imports // First verify that the CommonEnv data was preserved after deserialization // Should have same 53 identifiers as original: hello, world, TestModule + 19 well-known identifiers + 18 type identifiers + 3 field/tag identifiers + 7 more identifiers + 2 Try tag identifiers + 1 method identifier from ModuleEnv.init() @@ -135,20 +135,20 @@ test "ModuleEnv.Serialized roundtrip" { try testing.expectEqual(@as(?u16, 42), env.common.exposed_items.getNodeIndexById(gpa, @as(u32, @bitCast(hello_idx)))); try testing.expectEqual(@as(usize, 3), env.common.line_starts.len()); - try testing.expectEqual(@as(u32, 0), env.common.line_starts.items.items[0]); - try testing.expectEqual(@as(u32, 10), env.common.line_starts.items.items[1]); - try testing.expectEqual(@as(u32, 20), env.common.line_starts.items.items[2]); + try testing.expectEqual(@as(u32, 0), env.common.line_starts.items()[0]); + try testing.expectEqual(@as(u32, 10), env.common.line_starts.items()[1]); + try testing.expectEqual(@as(u32, 20), env.common.line_starts.items()[2]); // TODO restore source using CommonEnv // try testing.expectEqualStrings(source, env.source); try testing.expectEqualStrings("TestModule", env.module_name); // Verify imports were preserved after deserialization - try testing.expectEqual(@as(usize, 2), env.imports.imports.len()); + try testing.expectEqual(@as(usize, 2), env.imports.len()); // Verify the import strings are correct (they reference string indices in the string store) - const import_str1 = env.common.strings.get(env.imports.imports.items.items[0]); - const import_str2 = env.common.strings.get(env.imports.imports.items.items[1]); + const import_str1 = env.common.strings.get(env.imports.imports.field(.str_idx)[0]); + const import_str2 = env.common.strings.get(env.imports.imports.field(.str_idx)[1]); try testing.expectEqualStrings("json.Json", import_str1); try testing.expectEqualStrings("core.List", import_str2); @@ -169,7 +169,7 @@ test "ModuleEnv.Serialized roundtrip" { try testing.expectEqual(@as(u32, 0), @intFromEnum(import4)); // Should create new entry for new.Module try testing.expectEqual(@as(u32, 2), @intFromEnum(import5)); - try testing.expectEqual(@as(usize, 3), env.imports.imports.len()); + try testing.expectEqual(@as(usize, 3), env.imports.len()); } // test "ModuleEnv with types CompactWriter roundtrip" { @@ -393,7 +393,7 @@ test "ModuleEnv.Serialized roundtrip" { // try testing.expectEqualStrings("test.Hello", deserialized.module_name); // // Verify line starts were preserved -// try testing.expectEqual(original.line_starts.items.items.len, deserialized.line_starts.items.items.len); +// try testing.expectEqual(original.line_starts.items().len, deserialized.line_starts.items().len); // } test "ModuleEnv pushExprTypesToSExprTree extracts and formats types" { diff --git a/src/compile/test/type_printing_bug_test.zig b/src/compile/test/type_printing_bug_test.zig index 183d6fd0997..17bf3cb28f2 100644 --- a/src/compile/test/type_printing_bug_test.zig +++ b/src/compile/test/type_printing_bug_test.zig @@ -74,8 +74,8 @@ test "canonicalizeAndTypeCheckModule preserves Try types in type printing" { const ident_idx = pattern.assign.ident; const ident_text = env.getIdent(ident_idx); if (std.mem.eql(u8, ident_text, "map_result")) { - // Get the type variable from the first definition - it's the first in the defs list - map_result_var = @enumFromInt(0); // First variable + // Get the type variable for this definition + map_result_var = ModuleEnv.varFrom(def_idx); break; } } diff --git a/src/eval/comptime_evaluator.zig b/src/eval/comptime_evaluator.zig index c79e5ca7e12..9c2acf01349 100644 --- a/src/eval/comptime_evaluator.zig +++ b/src/eval/comptime_evaluator.zig @@ -472,15 +472,12 @@ pub const ComptimeEvaluator = struct { const tag_name_str = if (is_true) "True" else "False"; const tag_name_ident = try self.env.insertIdent(base.Ident.for_text(tag_name_str)); - // Get variant_var and ext_var + // Get variant_var and ext_var from the tag union type + // Bool should always be a tag union const variant_var: types_mod.Var = bool_rt_var; - var ext_var: types_mod.Var = @enumFromInt(0); - - if (resolved.desc.content == .structure) { - if (resolved.desc.content.structure == .tag_union) { - ext_var = resolved.desc.content.structure.tag_union.ext; - } - } + std.debug.assert(resolved.desc.content == .structure and + resolved.desc.content.structure == .tag_union); + const ext_var = resolved.desc.content.structure.tag_union.ext; // Replace the expression with e_zero_argument_tag try self.env.store.replaceExprWithZeroArgumentTag( @@ -518,16 +515,13 @@ pub const ComptimeEvaluator = struct { return error.NotImplemented; } - // Get variant_var and ext_var from type information + // Get variant_var and ext_var from the tag union type + // This function is specifically for tag unions const resolved = self.interpreter.runtime_types.resolveVar(rt_var); const variant_var: types_mod.Var = rt_var; - var ext_var: types_mod.Var = @enumFromInt(0); - - if (resolved.desc.content == .structure) { - if (resolved.desc.content.structure == .tag_union) { - ext_var = resolved.desc.content.structure.tag_union.ext; - } - } + std.debug.assert(resolved.desc.content == .structure and + resolved.desc.content.structure == .tag_union); + const ext_var = resolved.desc.content.structure.tag_union.ext; // Replace the expression with e_zero_argument_tag try self.env.store.replaceExprWithZeroArgumentTag( @@ -575,16 +569,13 @@ pub const ComptimeEvaluator = struct { return error.NotImplemented; // Has payload, can't fold to e_zero_argument_tag } - // Get variant_var and ext_var from type information + // Get variant_var and ext_var from the tag union type + // This function is specifically for tag unions const resolved = self.interpreter.runtime_types.resolveVar(rt_var); const variant_var: types_mod.Var = rt_var; - var ext_var: types_mod.Var = @enumFromInt(0); - - if (resolved.desc.content == .structure) { - if (resolved.desc.content.structure == .tag_union) { - ext_var = resolved.desc.content.structure.tag_union.ext; - } - } + std.debug.assert(resolved.desc.content == .structure and + resolved.desc.content.structure == .tag_union); + const ext_var = resolved.desc.content.structure.tag_union.ext; // Get closure name - use an empty ident for now (we don't need it for folded constants) const closure_name = tag_info.name; // Reuse tag name as closure name @@ -674,7 +665,7 @@ pub const ComptimeEvaluator = struct { /// For now, validation is skipped - literals are allowed without validation. /// This preserves current behavior while the infrastructure is in place. fn validateDeferredNumericLiterals(self: *ComptimeEvaluator) !void { - const literals = self.env.deferred_numeric_literals.items.items; + const literals = self.env.deferred_numeric_literals.items(); for (literals) |literal| { // Step 1: Resolve the type variable to get the concrete type diff --git a/src/eval/interpreter.zig b/src/eval/interpreter.zig index d1498ba1b28..12ef59d867b 100644 --- a/src/eval/interpreter.zig +++ b/src/eval/interpreter.zig @@ -242,11 +242,8 @@ pub const Interpreter = struct { var next_id: u32 = 1; // Start at 1, reserve 0 for current module - // Safely access import count - const import_count = if (env.imports.imports.items.items.len > 0) - env.imports.imports.items.items.len - else - 0; + // Safely access import count using the new Store.len() method + const import_count = env.imports.len(); if (other_envs.len > 0 and import_count > 0) { // Allocate capacity for all imports (even if some are duplicates) @@ -1108,7 +1105,7 @@ pub const Interpreter = struct { // Get the first element's variables, which is representative of all the element vars const elems = self.env.store.sliceExpr(list_expr.elems); std.debug.assert(elems.len > 0); - const first_elem_var: types.Var = @enumFromInt(@intFromEnum(elems[0])); + const first_elem_var = can.ModuleEnv.varFrom(elems[0]); const elem_rt_var = try self.translateTypeVar(self.env, first_elem_var); const elem_layout = try self.getRuntimeLayout(elem_rt_var); @@ -1939,6 +1936,9 @@ pub const Interpreter = struct { closure_idx -= 1; const cls_val = self_interp.active_closures.items[closure_idx]; if (cls_val.layout.tag == .closure and cls_val.ptr != null) { + // SafeList uses 1-based indexing, index 0 means "no captures" for hosted lambdas + const captures_idx = @intFromEnum(cls_val.layout.data.closure.captures_layout_idx); + if (captures_idx == 0) continue; // Skip closures with no captures const captures_layout = self_interp.runtime_layout_store.getLayout(cls_val.layout.data.closure.captures_layout_idx); const header_sz = @sizeOf(layout.Closure); const cap_align = captures_layout.alignment(self_interp.runtime_layout_store.targetUsize()); @@ -2086,10 +2086,9 @@ pub const Interpreter = struct { var subst_map = std.AutoHashMap(types.Var, types.Var).init(self.allocator); defer subst_map.deinit(); - const func_rt_var = if (should_instantiate) - try self.instantiateType(func_rt_var_orig, &subst_map) - else - func_rt_var_orig; + const func_rt_var = if (should_instantiate) blk: { + break :blk try self.instantiateType(func_rt_var_orig, &subst_map); + } else func_rt_var_orig; // Save current rigid substitution context and merge in the new substitutions (only if we instantiated) // This will be used during function body evaluation @@ -2152,6 +2151,7 @@ pub const Interpreter = struct { } break :blk null; }; + // Get call expression's return type for low-level builtins const call_ret_ct_var = can.ModuleEnv.varFrom(expr_idx); const call_ret_rt_var = try self.translateTypeVar(self.env, call_ret_ct_var); @@ -2185,6 +2185,7 @@ pub const Interpreter = struct { while (j < arg_indices.len) : (j += 1) { arg_values[j] = try self.evalExprMinimal(arg_indices[j], roc_ops, if (arg_rt_buf.len == 0) null else arg_rt_buf[j]); } + // Support calling closures produced by evaluating expressions (including nested calls) if (func_val.layout.tag == .closure) { const header: *const layout.Closure = @ptrCast(@alignCast(func_val.ptr.?)); @@ -2586,7 +2587,9 @@ pub const Interpreter = struct { // Only e_closure creates real capture values; others have uninitialized captures area const lambda_expr = header.source_env.store.getExpr(header.lambda_expr_idx); const has_real_captures = (lambda_expr == .e_closure); - if (has_real_captures) { + // SafeList uses 1-based indexing, index 0 means "no captures" for hosted lambdas + const captures_idx = @intFromEnum(cls_val.layout.data.closure.captures_layout_idx); + if (has_real_captures and captures_idx > 0) { const captures_layout = self.runtime_layout_store.getLayout(cls_val.layout.data.closure.captures_layout_idx); const header_sz = @sizeOf(layout.Closure); const cap_align = captures_layout.alignment(self.runtime_layout_store.targetUsize()); @@ -2731,8 +2734,12 @@ pub const Interpreter = struct { const target_usize = self.runtime_layout_store.targetUsize(); var alignment = layout_val.alignment(target_usize); if (layout_val.tag == .closure) { - const captures_layout = self.runtime_layout_store.getLayout(layout_val.data.closure.captures_layout_idx); - alignment = alignment.max(captures_layout.alignment(target_usize)); + // SafeList uses 1-based indexing, index 0 means "no captures" for hosted lambdas + const captures_idx = @intFromEnum(layout_val.data.closure.captures_layout_idx); + if (captures_idx > 0) { + const captures_layout = self.runtime_layout_store.getLayout(layout_val.data.closure.captures_layout_idx); + alignment = alignment.max(captures_layout.alignment(target_usize)); + } } const ptr = try self.stack_memory.alloca(size, alignment); return StackValue{ .layout = layout_val, .ptr = ptr, .is_initialized = true }; @@ -2743,8 +2750,12 @@ pub const Interpreter = struct { const target_usize = self.runtime_layout_store.targetUsize(); var alignment = src.layout.alignment(target_usize); if (src.layout.tag == .closure) { - const captures_layout = self.runtime_layout_store.getLayout(src.layout.data.closure.captures_layout_idx); - alignment = alignment.max(captures_layout.alignment(target_usize)); + // SafeList uses 1-based indexing, index 0 means "no captures" for hosted lambdas + const captures_idx = @intFromEnum(src.layout.data.closure.captures_layout_idx); + if (captures_idx > 0) { + const captures_layout = self.runtime_layout_store.getLayout(src.layout.data.closure.captures_layout_idx); + alignment = alignment.max(captures_layout.alignment(target_usize)); + } } const ptr = if (size > 0) try self.stack_memory.alloca(size, alignment) else null; // Preserve rt_var for constant folding @@ -6871,7 +6882,7 @@ pub const Interpreter = struct { }, else => { // TODO: Don't use unreachable here - unreachable; + @panic("gatherTags: unexpected flat_type"); }, } }, @@ -6882,7 +6893,7 @@ pub const Interpreter = struct { .rigid => break, else => { // TODO: Don't use unreachable here - unreachable; + @panic("gatherTags: unexpected content"); }, } } diff --git a/src/eval/test/eval_test.zig b/src/eval/test/eval_test.zig index a87b9d06db1..344e9516d73 100644 --- a/src/eval/test/eval_test.zig +++ b/src/eval/test/eval_test.zig @@ -791,9 +791,9 @@ test "ModuleEnv serialization and interpreter evaluation" { }; defer writer.deinit(arena_alloc); - // Allocate space for ModuleEnv and serialize - const env_ptr = try writer.appendAlloc(arena_alloc, ModuleEnv); - const env_start_offset = writer.total_bytes - @sizeOf(ModuleEnv); + // Allocate space for Serialized and serialize + const env_ptr = try writer.appendAlloc(arena_alloc, ModuleEnv.Serialized); + const env_start_offset = writer.total_bytes - @sizeOf(ModuleEnv.Serialized); const serialized_ptr = @as(*ModuleEnv.Serialized, @ptrCast(@alignCast(env_ptr))); try serialized_ptr.serialize(&original_env, arena_alloc, &writer); diff --git a/src/eval/test/helpers.zig b/src/eval/test/helpers.zig index 585cf16746d..17cca082102 100644 --- a/src/eval/test/helpers.zig +++ b/src/eval/test/helpers.zig @@ -420,7 +420,7 @@ pub fn runExpectRecord(src: []const u8, expected_fields: []const ExpectedField, /// Rewrite deferred numeric literals to match their inferred types /// This is similar to what ComptimeEvaluator does but for test expressions fn rewriteDeferredNumericLiterals(env: *ModuleEnv, types_store: *types.Store, import_mapping: *const types.import_mapping.ImportMapping) !void { - const literals = env.deferred_numeric_literals.items.items; + const literals = env.deferred_numeric_literals.items(); for (literals) |literal| { // Resolve the type variable to get the concrete type diff --git a/src/fmt/fmt.zig b/src/fmt/fmt.zig index c31a369b0ca..ae78d96ccfd 100644 --- a/src/fmt/fmt.zig +++ b/src/fmt/fmt.zig @@ -253,8 +253,8 @@ fn printParseErrors(gpa: std.mem.Allocator, source: []const u8, parse_ast: AST) try stderr.print("Errors:\n", .{}); for (parse_ast.parse_diagnostics.items) |err| { const region = parse_ast.tokens.resolve(@intCast(err.region.start)); - const line = binarySearch(line_offsets.items.items, region.start.offset) orelse unreachable; - const column = region.start.offset - line_offsets.items.items[line]; + const line = binarySearch(line_offsets.items(), region.start.offset) orelse unreachable; + const column = region.start.offset - line_offsets.items()[line]; const token = parse_ast.tokens.tokens.items(.tag)[err.region.start]; // TODO: pretty print the parse failures. try stderr.print("\t{s}, at token {s} at {d}:{d}\n", .{ @tagName(err.tag), @tagName(token), line + 1, column }); diff --git a/src/interpreter_shim/main.zig b/src/interpreter_shim/main.zig index 4e9699913bf..bc473794bbf 100644 --- a/src/interpreter_shim/main.zig +++ b/src/interpreter_shim/main.zig @@ -126,6 +126,9 @@ fn initializeSharedMemoryOnce(roc_ops: *RocOps) ShimError!void { /// Cross-platform shared memory evaluation fn evaluateFromSharedMemory(entry_idx: u32, roc_ops: *RocOps, ret_ptr: *anyopaque, arg_ptr: ?*anyopaque) ShimError!void { + // DEBUG: Test that crash mechanism works + // roc_ops.crash("SHIM DEBUG: About to initialize shared memory"); + // Initialize shared memory once per process try initializeSharedMemoryOnce(roc_ops); diff --git a/src/layout/layout.zig b/src/layout/layout.zig index 0c397571b2d..ab6a3c15e73 100644 --- a/src/layout/layout.zig +++ b/src/layout/layout.zig @@ -81,26 +81,27 @@ pub const Idx = enum(@Type(.{ // // The layout store's idxFromScalar method relies on these exact numbers being what they are now, // so be careful when changing them! (Changing them will, at a minimum, cause tests to fail.) - bool = 0, - str = 1, - opaque_ptr = 2, + // Note: These start at 1 because SafeList uses 1-based indexing. + bool = 1, + str = 2, + opaque_ptr = 3, // ints - u8 = 3, - i8 = 4, - u16 = 5, - i16 = 6, - u32 = 7, - i32 = 8, - u64 = 9, - i64 = 10, - u128 = 11, - i128 = 12, + u8 = 4, + i8 = 5, + u16 = 6, + i16 = 7, + u32 = 8, + i32 = 9, + u64 = 10, + i64 = 11, + u128 = 12, + i128 = 13, // fracs - f32 = 13, - f64 = 14, - dec = 15, + f32 = 14, + f64 = 15, + dec = 16, // Regular indices start from here. // num_scalars in store.zig must refer to how many variants we had up to this point. diff --git a/src/layout/store.zig b/src/layout/store.zig index b50f3489568..1ef8edbfb1f 100644 --- a/src/layout/store.zig +++ b/src/layout/store.zig @@ -95,10 +95,11 @@ pub const Store = struct { /// This relies on the careful ordering of ScalarTag and Idx enum values. pub fn idxFromScalar(scalar: Scalar) Idx { // Map scalar to idx using pure arithmetic: - // opaque_ptr (tag 0) -> 2 - // str (tag 1) -> 1 - // int (tag 2) with precision p -> 3 + p - // frac (tag 3) with precision p -> 13 + (p - 2) = 11 + p + // opaque_ptr (tag 0) -> 3 + // str (tag 1) -> 2 + // int (tag 2) with precision p -> 4 + p + // frac (tag 3) with precision p -> 14 + (p - 2) = 12 + p + // Note: All indices shifted by 1 for 1-based SafeList indexing. const tag = @intFromEnum(scalar.tag); @@ -113,10 +114,10 @@ pub const Store = struct { // Calculate the base index based on tag mappings const base_idx = switch (scalar.tag) { - .opaque_ptr => @as(u7, 2), - .str => @as(u7, 1), - .int => @as(u7, 3), - .frac => @as(u7, 11), // 13 - 2 = 11, so 11 + p gives correct result + .opaque_ptr => @as(u7, 3), + .str => @as(u7, 2), + .int => @as(u7, 4), + .frac => @as(u7, 12), // 14 - 2 = 12, so 12 + p gives correct result }; // Calculate the final index @@ -307,7 +308,7 @@ pub const Store = struct { const total_size = @as(u32, @intCast(std.mem.alignForward(u32, current_offset, @as(u32, @intCast(max_alignment.toByteUnits()))))); const fields_range = collections.NonEmptyRange{ .start = @intCast(fields_start), .count = @intCast(temp_fields.items.len) }; - const record_idx = RecordIdx{ .int_idx = @intCast(self.record_data.len()) }; + const record_idx = RecordIdx{ .int_idx = @intCast(self.record_data.len() + 1) }; _ = try self.record_data.append(self.env.gpa, .{ .size = total_size, .fields = fields_range, @@ -377,7 +378,7 @@ pub const Store = struct { const total_size = @as(u32, @intCast(std.mem.alignForward(u32, current_offset, @as(u32, @intCast(max_alignment.toByteUnits()))))); const fields_range = collections.NonEmptyRange{ .start = @intCast(fields_start), .count = @intCast(temp_fields.items.len) }; - const tuple_idx = TupleIdx{ .int_idx = @intCast(self.tuple_data.len()) }; + const tuple_idx = TupleIdx{ .int_idx = @intCast(self.tuple_data.len() + 1) }; _ = try self.tuple_data.append(self.env.gpa, TupleData{ .size = total_size, .fields = fields_range }); const tuple_layout = Layout.tuple(max_alignment, tuple_idx); return try self.insertLayout(tuple_layout); @@ -483,7 +484,7 @@ pub const Store = struct { /// Get or create an empty record layout (for closures with no captures) fn getEmptyRecordLayout(self: *Self) !Idx { // Check if we already have an empty record layout - for (self.record_data.items.items, 0..) |record_data, i| { + for (self.record_data.items(), 1..) |record_data, i| { if (record_data.size == 0 and record_data.fields.count == 0) { const record_idx = RecordIdx{ .int_idx = @intCast(i) }; const empty_record_layout = Layout.record(std.mem.Alignment.@"1", record_idx); @@ -492,7 +493,7 @@ pub const Store = struct { } // Create new empty record layout - const record_idx = RecordIdx{ .int_idx = @intCast(self.record_data.len()) }; + const record_idx = RecordIdx{ .int_idx = @intCast(self.record_data.len() + 1) }; _ = try self.record_data.append(self.env.gpa, .{ .size = 0, .fields = collections.NonEmptyRange{ .start = 0, .count = 0 }, @@ -541,6 +542,12 @@ pub const Store = struct { .closure => { // Closure layout: header + aligned capture data const header_size = @sizeOf(layout_mod.Closure); + // SafeList uses 1-based indexing, index 0 means "no captures" for hosted lambdas + const captures_idx = @intFromEnum(layout.data.closure.captures_layout_idx); + if (captures_idx == 0) { + // No captures - just the header + return header_size; + } const captures_layout = self.getLayout(layout.data.closure.captures_layout_idx); const captures_alignment = captures_layout.alignment(self.targetUsize()); const aligned_captures_offset = std.mem.alignForward(u32, header_size, @intCast(captures_alignment.toByteUnits())); @@ -780,7 +787,7 @@ pub const Store = struct { const fields_range = collections.NonEmptyRange{ .start = @intCast(fields_start), .count = @intCast(num_resolved_fields) }; // Store the record data - const record_idx = RecordIdx{ .int_idx = @intCast(self.record_data.len()) }; + const record_idx = RecordIdx{ .int_idx = @intCast(self.record_data.len() + 1) }; _ = try self.record_data.append(self.env.gpa, RecordData{ .size = total_size, .fields = fields_range, @@ -878,7 +885,7 @@ pub const Store = struct { const fields_range = collections.NonEmptyRange{ .start = @intCast(fields_start), .count = @intCast(num_resolved_fields) }; // Store the tuple data - const tuple_idx = TupleIdx{ .int_idx = @intCast(self.tuple_data.len()) }; + const tuple_idx = TupleIdx{ .int_idx = @intCast(self.tuple_data.len() + 1) }; _ = try self.tuple_data.append(self.env.gpa, TupleData{ .size = total_size, .fields = fields_range, diff --git a/src/parse/AST.zig b/src/parse/AST.zig index 63d43609bc0..47a023b784e 100644 --- a/src/parse/AST.zig +++ b/src/parse/AST.zig @@ -113,7 +113,7 @@ pub fn appendRegionInfoToSexprTree(self: *const AST, env: *const CommonEnv, tree const start = self.tokens.resolve(region.start); const region_end_idx = if (region.end > 0) region.end - 1 else region.end; const end = self.tokens.resolve(region_end_idx); - const info: base.RegionInfo = base.RegionInfo.position(self.env.source, env.line_starts.items.items, start.start.offset, end.end.offset) catch .{ + const info: base.RegionInfo = base.RegionInfo.position(self.env.source, env.line_starts.items(), start.start.offset, end.end.offset) catch .{ .start_line_idx = 0, .start_col_idx = 0, .end_line_idx = 0, @@ -172,14 +172,14 @@ pub fn tokenizeDiagnosticToReport(self: *AST, diagnostic: tokenize.Diagnostic, a diagnostic.region.end.offset <= self.env.source.len) { var env = self.env.*; - if (env.line_starts.items.items.len == 0) { + if (env.line_starts.items().len == 0) { try env.calcLineStarts(allocator); } // Convert region to RegionInfo const region_info = base.RegionInfo.position( self.env.source, - env.line_starts.items.items, + env.line_starts.items(), diagnostic.region.start.offset, diagnostic.region.end.offset, ) catch { @@ -193,7 +193,7 @@ pub fn tokenizeDiagnosticToReport(self: *AST, diagnostic: tokenize.Diagnostic, a .error_highlight, filename, self.env.source, - env.line_starts.items.items, + env.line_starts.items(), ); } @@ -604,7 +604,7 @@ pub fn parseDiagnosticToReport(self: *AST, env: *const CommonEnv, diagnostic: Di // Add source context if we have a valid region if (region.start.offset <= region.end.offset and region.end.offset <= self.env.source.len) { // Use proper region info calculation with converted region - const region_info = base.RegionInfo.position(self.env.source, env.line_starts.items.items, region.start.offset, region.end.offset) catch { + const region_info = base.RegionInfo.position(self.env.source, env.line_starts.items(), region.start.offset, region.end.offset) catch { return report; // Return report without source context if region calculation fails }; @@ -613,7 +613,7 @@ pub fn parseDiagnosticToReport(self: *AST, env: *const CommonEnv, diagnostic: Di // Use the proper addSourceContext method with owned filename const owned_filename = try report.addOwnedString(filename); - try report.addSourceContext(region_info, owned_filename, self.env.source, env.line_starts.items.items); + try report.addSourceContext(region_info, owned_filename, self.env.source, env.line_starts.items()); } return report; diff --git a/src/parse/HTML.zig b/src/parse/HTML.zig index 0f1b4e0ab21..26880863191 100644 --- a/src/parse/HTML.zig +++ b/src/parse/HTML.zig @@ -69,7 +69,7 @@ pub fn tokensToHtml(ast: *const AST, env: *const CommonEnv, writer: *std.io.Writ try writer.print("", .{css_class}); try writer.print("{s}", .{@tagName(tag)}); try writer.writeAll(" "); - try writeSourceRangeSpan(writer, region, env.source, env.line_starts.items.items); + try writeSourceRangeSpan(writer, region, env.source, env.line_starts.items()); try writer.writeAll(""); } try writer.writeAll(""); diff --git a/src/playground_wasm/main.zig b/src/playground_wasm/main.zig index b840fd75a5b..88648e0be68 100644 --- a/src/playground_wasm/main.zig +++ b/src/playground_wasm/main.zig @@ -1691,7 +1691,7 @@ fn writeHoverInfoResponse(response_buffer: []u8, data: CompilerStageData, messag } const source = data.module_env.common.source; - const line_starts = data.module_env.common.line_starts.items.items; + const line_starts = data.module_env.common.line_starts.items(); if (line_num >= line_starts.len) { try writeErrorResponse(response_buffer, .ERROR, "Line number out of range"); diff --git a/src/snapshot_tool/main.zig b/src/snapshot_tool/main.zig index 00056121c26..a980eee0347 100644 --- a/src/snapshot_tool/main.zig +++ b/src/snapshot_tool/main.zig @@ -1234,8 +1234,8 @@ fn processSnapshotContent( // Build builtin_modules array in the same order as can_ir.imports // Dict and Set are now nested inside Builtin, so we only have one module to add - const import_count = can_ir.imports.imports.items.items.len; - for (can_ir.imports.imports.items.items[0..import_count]) |str_idx| { + const import_count = can_ir.imports.len(); + for (can_ir.imports.imports.field(.str_idx)[0..import_count]) |str_idx| { const import_name = can_ir.getString(str_idx); // Match the import name to the corresponding loaded builtin module diff --git a/src/types/TypeWriter.zig b/src/types/TypeWriter.zig index 1f36ca72c57..ee65a4947ac 100644 --- a/src/types/TypeWriter.zig +++ b/src/types/TypeWriter.zig @@ -302,7 +302,8 @@ fn hasSeenVar(self: *const TypeWriter, var_: Var) bool { /// Convert a var to a type string fn writeVarWithContext(self: *TypeWriter, var_: Var, context: TypeContext, root_var: Var) std.mem.Allocator.Error!void { - if (@intFromEnum(var_) >= self.types.slots.backing.len()) { + // With 1-based indexing: valid Vars are 1..len, so check > instead of >= + if (@intFromEnum(var_) > self.types.slots.backing.len()) { // Variable is out of bounds - this can happen with corrupted type data _ = try self.buf.writer().write("Error"); return; @@ -310,7 +311,8 @@ fn writeVarWithContext(self: *TypeWriter, var_: Var, context: TypeContext, root_ const resolved = self.types.resolveVar(var_); - if (@intFromEnum(resolved.var_) >= self.types.slots.backing.len()) { + // With 1-based indexing: valid Vars are 1..len, so check > instead of >= + if (@intFromEnum(resolved.var_) > self.types.slots.backing.len()) { // Variable is out of bounds - this can happen with corrupted type data _ = try self.buf.writer().write("Error"); return; @@ -733,7 +735,8 @@ pub fn writeFlexVarName(self: *TypeWriter, var_: Var, context: TypeContext, root const resolved_var = self.types.resolveVar(var_).var_; // If resolved var is out of bounds, it's corrupted - just write a simple name - if (@intFromEnum(resolved_var) >= self.types.slots.backing.len()) { + // With 1-based indexing: valid Vars are 1..len, so check > instead of >= + if (@intFromEnum(resolved_var) > self.types.slots.backing.len()) { _ = try self.buf.writer().write("_"); try self.generateContextualName(context); return; @@ -782,7 +785,8 @@ fn countVarOccurrences(self: *TypeWriter, search_var: Var, root_var: Var) std.me } fn countVar(self: *TypeWriter, search_var: Var, current_var: Var, count: *usize) std.mem.Allocator.Error!void { - if (@intFromEnum(current_var) >= self.types.slots.backing.len()) return; + // With 1-based indexing: valid Vars are 1..len, so check > instead of >= + if (@intFromEnum(current_var) > self.types.slots.backing.len()) return; const resolved = self.types.resolveVar(current_var); diff --git a/src/types/instantiate.zig b/src/types/instantiate.zig index 4bb37407191..880e63da95f 100644 --- a/src/types/instantiate.zig +++ b/src/types/instantiate.zig @@ -100,7 +100,9 @@ pub const Instantiator = struct { if (rigid_subs.get(rigid.name)) |existing_flex| { break :inner_blk existing_flex; } else { - std.debug.assert(false); + // Rigid variable name not found in substitution map + // This can happen with hosted functions that have unconstrained type variables + // Just create a fresh error var to continue gracefully break :inner_blk try self.store.freshFromContentWithRank( .err, self.current_rank, @@ -171,8 +173,8 @@ pub const Instantiator = struct { .flex => |flex| Content{ .flex = try self.instantiateFlex(flex) }, .rigid => { // Rigids should be handled by `instantiateVar` - // If we have run into one here, it is abug - unreachable; + // If we have run into one here, it is a bug + @panic("instantiateContent: unexpected rigid type variable - should be handled by instantiateVar"); }, .alias => |alias| { // Instantiate the structure recursively @@ -246,11 +248,17 @@ pub const Instantiator = struct { } fn instantiateTuple(self: *Self, tuple: Tuple) std.mem.Allocator.Error!Tuple { - const elems_slice = self.store.sliceVars(tuple.elems); + // NOTE: We must not hold a slice across instantiateVar calls, because + // instantiateVar can grow self.store.vars which may reallocate the backing array. + // Instead, we iterate by index and re-slice on each iteration. var fresh_elems = std.ArrayList(Var).empty; defer fresh_elems.deinit(self.store.gpa); - for (elems_slice) |elem_var| { + var i: usize = 0; + while (i < tuple.elems.count) : (i += 1) { + // Re-slice each iteration to get fresh pointer after any reallocation + const elems_slice = self.store.sliceVars(tuple.elems); + const elem_var = elems_slice[i]; const fresh_elem = try self.instantiateVar(elem_var); try fresh_elems.append(self.store.gpa, fresh_elem); } @@ -259,11 +267,17 @@ pub const Instantiator = struct { return Tuple{ .elems = fresh_elems_range }; } fn instantiateFunc(self: *Self, func: Func) std.mem.Allocator.Error!Func { - const args_slice = self.store.sliceVars(func.args); + // NOTE: We must not hold a slice across instantiateVar calls, because + // instantiateVar can grow self.store.vars which may reallocate the backing array. + // Instead, we iterate by index and re-slice on each iteration. var fresh_args = std.ArrayList(Var).empty; defer fresh_args.deinit(self.store.gpa); - for (args_slice) |arg_var| { + var i: usize = 0; + while (i < func.args.count) : (i += 1) { + // Re-slice each iteration to get fresh pointer after any reallocation + const args_slice = self.store.sliceVars(func.args); + const arg_var = args_slice[i]; const fresh_arg = try self.instantiateVar(arg_var); try fresh_args.append(self.store.gpa, fresh_arg); } @@ -278,12 +292,18 @@ pub const Instantiator = struct { } fn instantiateRecordFields(self: *Self, fields: RecordField.SafeMultiList.Range) std.mem.Allocator.Error!RecordField.SafeMultiList.Range { - const fields_slice = self.store.getRecordFieldsSlice(fields); - + // NOTE: We must not hold a slice across instantiateVar calls, because + // instantiateVar can grow self.store which may reallocate backing arrays. + // Instead, we iterate by index and re-slice on each iteration. var fresh_fields = std.ArrayList(RecordField).empty; defer fresh_fields.deinit(self.store.gpa); - for (fields_slice.items(.name), fields_slice.items(.var_)) |name, type_var| { + var i: usize = 0; + while (i < fields.count) : (i += 1) { + // Re-slice each iteration to get fresh pointer after any reallocation + const fields_slice = self.store.getRecordFieldsSlice(fields); + const name = fields_slice.items(.name)[i]; + const type_var = fields_slice.items(.var_)[i]; const fresh_type = try self.instantiateVar(type_var); _ = try fresh_fields.append(self.store.gpa, RecordField{ .name = name, @@ -295,12 +315,18 @@ pub const Instantiator = struct { } fn instantiateRecord(self: *Self, record: Record) std.mem.Allocator.Error!Record { - const fields_slice = self.store.getRecordFieldsSlice(record.fields); - + // NOTE: We must not hold a slice across instantiateVar calls, because + // instantiateVar can grow self.store which may reallocate backing arrays. + // Instead, we iterate by index and re-slice on each iteration. var fresh_fields = std.ArrayList(RecordField).empty; defer fresh_fields.deinit(self.store.gpa); - for (fields_slice.items(.name), fields_slice.items(.var_)) |name, type_var| { + var i: usize = 0; + while (i < record.fields.count) : (i += 1) { + // Re-slice each iteration to get fresh pointer after any reallocation + const fields_slice = self.store.getRecordFieldsSlice(record.fields); + const name = fields_slice.items(.name)[i]; + const type_var = fields_slice.items(.var_)[i]; const fresh_type = try self.instantiateVar(type_var); _ = try fresh_fields.append(self.store.gpa, RecordField{ .name = name, @@ -316,17 +342,27 @@ pub const Instantiator = struct { } fn instantiateTagUnion(self: *Self, tag_union: TagUnion) std.mem.Allocator.Error!TagUnion { - const tags_slice = self.store.getTagsSlice(tag_union.tags); - + // NOTE: We must not hold a slice across instantiateVar calls, because + // instantiateVar can grow self.store which may reallocate backing arrays. + // Instead, we iterate by index and re-slice on each iteration. var fresh_tags = std.ArrayList(Tag).empty; defer fresh_tags.deinit(self.store.gpa); - for (tags_slice.items(.name), tags_slice.items(.args)) |tag_name, tag_args| { + var tag_i: usize = 0; + while (tag_i < tag_union.tags.count) : (tag_i += 1) { + // Re-slice each iteration to get fresh pointer after any reallocation + const tags_slice = self.store.getTagsSlice(tag_union.tags); + const tag_name = tags_slice.items(.name)[tag_i]; + const tag_args = tags_slice.items(.args)[tag_i]; + var fresh_args = std.ArrayList(Var).empty; defer fresh_args.deinit(self.store.gpa); - const args_slice = self.store.sliceVars(tag_args); - for (args_slice) |arg_var| { + var arg_i: usize = 0; + while (arg_i < tag_args.count) : (arg_i += 1) { + // Re-slice each iteration to get fresh pointer after any reallocation + const args_slice = self.store.sliceVars(tag_args); + const arg_var = args_slice[arg_i]; const fresh_arg = try self.instantiateVar(arg_var); try fresh_args.append(self.store.gpa, fresh_arg); } diff --git a/src/types/mod.zig b/src/types/mod.zig index e66acb63f73..38a597557dd 100644 --- a/src/types/mod.zig +++ b/src/types/mod.zig @@ -31,6 +31,7 @@ pub const Tag = types.Tag; pub const TagUnion = types.TagUnion; pub const Tuple = types.Tuple; pub const Var = types.Var; +pub const MaybeVar = types.MaybeVar; pub const TypeIdent = types.TypeIdent; pub const Descriptor = types.Descriptor; pub const TwoRecordFields = types.TwoRecordFields; diff --git a/src/types/store.zig b/src/types/store.zig index 5647cb47c55..9f7364e9053 100644 --- a/src/types/store.zig +++ b/src/types/store.zig @@ -117,7 +117,7 @@ pub const Store = struct { /// Ensure that slots & descriptor arrays have at least the provided capacity pub fn ensureTotalCapacity(self: *Self, capacity: usize) Allocator.Error!void { try self.descs.backing.ensureTotalCapacity(self.gpa, capacity); - try self.slots.backing.items.ensureTotalCapacity(self.gpa, capacity); + try self.slots.backing.ensureTotalCapacity(self.gpa, capacity); } /// Deinit the unification table @@ -192,14 +192,14 @@ pub const Store = struct { /// Set a type variable to the provided content pub fn setVarDesc(self: *Self, target_var: Var, desc: Desc) Allocator.Error!void { - std.debug.assert(@intFromEnum(target_var) < self.len()); + std.debug.assert(@intFromEnum(target_var) <= self.len()); const resolved = self.resolveVar(target_var); self.descs.set(resolved.desc_idx, desc); } /// Set a type variable to the provided content pub fn setVarContent(self: *Self, target_var: Var, content: Content) Allocator.Error!void { - std.debug.assert(@intFromEnum(target_var) < self.len()); + std.debug.assert(@intFromEnum(target_var) <= self.len()); const resolved = self.resolveVar(target_var); var desc = resolved.desc; desc.content = content; @@ -208,8 +208,8 @@ pub const Store = struct { /// Set a type variable to redirect to the provided redirect pub fn setVarRedirect(self: *Self, target_var: Var, redirect_to: Var) Allocator.Error!void { - std.debug.assert(@intFromEnum(target_var) < self.len()); - std.debug.assert(@intFromEnum(redirect_to) < self.len()); + std.debug.assert(@intFromEnum(target_var) <= self.len()); + std.debug.assert(@intFromEnum(redirect_to) <= self.len()); const slot_idx = Self.varToSlotIdx(target_var); self.slots.set(slot_idx, .{ .redirect = redirect_to }); } @@ -278,6 +278,17 @@ pub const Store = struct { }; } + /// Make alias data type from a pre-built range (backing_var already included at start) + /// Does not insert content into the types store + pub fn mkAliasFromRange(_: *Self, ident: TypeIdent, vars_range: VarSafeList.Range) Content { + return Content{ + .alias = Alias{ + .ident = ident, + .vars = .{ .nonempty = vars_range }, + }, + }; + } + /// Make nominal data type /// Does not insert content into the types store pub fn mkNominal( @@ -303,14 +314,57 @@ pub const Store = struct { } }; } + /// Make nominal data type from a pre-built range (backing_var already included at start) + /// Does not insert content into the types store + pub fn mkNominalFromRange( + _: *Self, + ident: TypeIdent, + vars_range: VarSafeList.Range, + origin_module: base.Ident.Idx, + ) Content { + return Content{ .structure = FlatType{ + .nominal_type = NominalType{ + .ident = ident, + .vars = .{ .nonempty = vars_range }, + .origin_module = origin_module, + }, + } }; + } + // Make a function data type with unbound effectfulness // Does not insert content into the types store. pub fn mkFuncUnbound(self: *Self, args: []const Var, ret: Var) std.mem.Allocator.Error!Content { + // Check if any arguments need instantiation BEFORE appending, + // because appendVars may reallocate and invalidate the args slice + var needs_inst = false; + for (args) |arg| { + if (self.needsInstantiation(arg)) { + needs_inst = true; + break; + } + } + + // Also check the return type + if (!needs_inst) { + needs_inst = self.needsInstantiation(ret); + } + + // NOW it's safe to append (which may reallocate) const args_range = try self.appendVars(args); + return Content{ .structure = .{ .fn_unbound = .{ + .args = args_range, + .ret = ret, + .needs_instantiation = needs_inst, + } } }; + } + + // Make a function data type with unbound effectfulness from a pre-existing args range + // Use this when args are already stored in types.vars to avoid reallocation issues + pub fn mkFuncUnboundFromRange(self: *const Self, args_range: VarSafeList.Range, ret: Var) Content { // Check if any arguments need instantiation var needs_inst = false; - for (args) |arg| { + for (self.sliceVars(args_range)) |arg| { if (self.needsInstantiation(arg)) { needs_inst = true; break; @@ -332,11 +386,33 @@ pub const Store = struct { // Make a pure function data type (as opposed to an effectful or unbound function) // Does not insert content into the types store. pub fn mkFuncPure(self: *Self, args: []const Var, ret: Var) std.mem.Allocator.Error!Content { + // Check if any arguments need instantiation BEFORE appending, + // because appendVars may reallocate and invalidate the args slice + var needs_inst = false; + for (args) |arg| { + if (self.needsInstantiation(arg)) { + needs_inst = true; + break; + } + } + + // Also check the return type + if (!needs_inst) { + needs_inst = self.needsInstantiation(ret); + } + + // NOW it's safe to append (which may reallocate) const args_range = try self.appendVars(args); + return Content{ .structure = .{ .fn_pure = .{ .args = args_range, .ret = ret, .needs_instantiation = needs_inst } } }; + } + + // Make a pure function data type from a pre-existing args range + // Use this when args are already stored in types.vars to avoid reallocation issues + pub fn mkFuncPureFromRange(self: *const Self, args_range: VarSafeList.Range, ret: Var) Content { // Check if any arguments need instantiation var needs_inst = false; - for (args) |arg| { + for (self.sliceVars(args_range)) |arg| { if (self.needsInstantiation(arg)) { needs_inst = true; break; @@ -354,11 +430,33 @@ pub const Store = struct { // Make an effectful function data type (as opposed to a pure or unbound function) // Does not insert content into the types store. pub fn mkFuncEffectful(self: *Self, args: []const Var, ret: Var) std.mem.Allocator.Error!Content { + // Check if any arguments need instantiation BEFORE appending, + // because appendVars may reallocate and invalidate the args slice + var needs_inst = false; + for (args) |arg| { + if (self.needsInstantiation(arg)) { + needs_inst = true; + break; + } + } + + // Also check the return type + if (!needs_inst) { + needs_inst = self.needsInstantiation(ret); + } + + // NOW it's safe to append (which may reallocate) const args_range = try self.appendVars(args); + return Content{ .structure = .{ .fn_effectful = .{ .args = args_range, .ret = ret, .needs_instantiation = needs_inst } } }; + } + + // Make an effectful function data type from a pre-existing args range + // Use this when args are already stored in types.vars to avoid reallocation issues + pub fn mkFuncEffectfulFromRange(self: *const Self, args_range: VarSafeList.Range, ret: Var) Content { // Check if any arguments need instantiation var needs_inst = false; - for (args) |arg| { + for (self.sliceVars(args_range)) |arg| { if (self.needsInstantiation(arg)) { needs_inst = true; break; @@ -1386,7 +1484,7 @@ test "SlotStore.Serialized roundtrip" { // Add some slots _ = try slot_store.insert(gpa, .{ .root = @enumFromInt(100) }); - _ = try slot_store.insert(gpa, .{ .redirect = @enumFromInt(0) }); + _ = try slot_store.insert(gpa, .{ .redirect = @enumFromInt(1) }); _ = try slot_store.insert(gpa, .{ .root = @enumFromInt(200) }); // Create temp file @@ -1422,9 +1520,9 @@ test "SlotStore.Serialized roundtrip" { // Verify try std.testing.expectEqual(@as(u64, 3), deserialized.backing.len()); - try std.testing.expectEqual(Slot{ .root = @enumFromInt(100) }, deserialized.get(@enumFromInt(0))); - try std.testing.expectEqual(Slot{ .redirect = @enumFromInt(0) }, deserialized.get(@enumFromInt(1))); - try std.testing.expectEqual(Slot{ .root = @enumFromInt(200) }, deserialized.get(@enumFromInt(2))); + try std.testing.expectEqual(Slot{ .root = @enumFromInt(100) }, deserialized.get(@enumFromInt(1))); + try std.testing.expectEqual(Slot{ .redirect = @enumFromInt(1) }, deserialized.get(@enumFromInt(2))); + try std.testing.expectEqual(Slot{ .root = @enumFromInt(200) }, deserialized.get(@enumFromInt(3))); } test "DescStore.Serialized roundtrip" { diff --git a/src/types/types.zig b/src/types/types.zig index 796ca0c8508..3de34e27c5e 100644 --- a/src/types/types.zig +++ b/src/types/types.zig @@ -31,7 +31,8 @@ test { try std.testing.expectEqual(72, @sizeOf(StaticDispatchConstraint)); // Includes recursion_info + num_literal fields } -/// A type variable +/// A type variable. Values are 1-based; 0 is reserved as a sentinel meaning "none". +/// Use `MaybeVar` when you need to represent an optional/missing variable. pub const Var = enum(u32) { _, @@ -44,6 +45,25 @@ pub const Var = enum(u32) { } }; +/// A type variable that may be absent (none). Value 0 represents "none". +/// Use this when a Var is optional, rather than using Var 0 directly. +pub const MaybeVar = enum(u32) { + none = 0, + _, + + /// Convert to an optional Var. Returns null if this is none (0). + pub fn get(self: MaybeVar) ?Var { + const int_value = @intFromEnum(self); + if (int_value == 0) return null; + return @enumFromInt(int_value); + } + + /// Create a MaybeVar from a Var. + pub fn from(var_: Var) MaybeVar { + return @enumFromInt(@intFromEnum(var_)); + } +}; + /// A mapping from polymorphic type variables to concrete type variables pub const VarMap = std.hash_map.HashMap(Var, Var, std.hash_map.AutoContext(Var), 80); diff --git a/test/fx/simple_stdout.roc b/test/fx/simple_stdout.roc new file mode 100644 index 00000000000..f196e96f49b --- /dev/null +++ b/test/fx/simple_stdout.roc @@ -0,0 +1,7 @@ +app [main!] { pf: platform "./platform/main.roc" } + +import pf.Stdout + +main! = || { + Stdout.line!("Hello!") +} diff --git a/test/snapshots/can_list_mismatch_then_nested_error.md b/test/snapshots/can_list_mismatch_then_nested_error.md index ba3bd3dd730..14c496d1323 100644 --- a/test/snapshots/can_list_mismatch_then_nested_error.md +++ b/test/snapshots/can_list_mismatch_then_nested_error.md @@ -8,17 +8,17 @@ type=expr [1, "hello", [3, "world"]] ~~~ # EXPECTED -INCOMPATIBLE LIST ELEMENTS - can_list_mismatch_then_nested_error.md:1:5:1:5 +INCOMPATIBLE LIST ELEMENTS - can_list_mismatch_then_nested_error.md:1:6:1:6 MISSING METHOD - can_list_mismatch_then_nested_error.md:1:2:1:3 MISSING METHOD - can_list_mismatch_then_nested_error.md:1:15:1:16 # PROBLEMS **INCOMPATIBLE LIST ELEMENTS** The second and third elements in this list have incompatible types: -**can_list_mismatch_then_nested_error.md:1:5:** +**can_list_mismatch_then_nested_error.md:1:6:** ```roc [1, "hello", [3, "world"]] ``` - ^^^^^^^ ^^^^^^^^^^^^ + ^^^^^ ^^^^^^^^^^^^ The second element has this type: _Str_ diff --git a/test/snapshots/if_then_else/if_then_else_simple_file.md b/test/snapshots/if_then_else/if_then_else_simple_file.md index 4fb98659c0d..fe36c858396 100644 --- a/test/snapshots/if_then_else/if_then_else_simple_file.md +++ b/test/snapshots/if_then_else/if_then_else_simple_file.md @@ -12,12 +12,12 @@ foo = if 1 A } ~~~ # EXPECTED -INCOMPATIBLE IF BRANCHES - if_then_else_simple_file.md:1:7:1:7 +INCOMPATIBLE IF BRANCHES - if_then_else_simple_file.md:1:12:1:12 MISSING METHOD - if_then_else_simple_file.md:1:10:1:11 # PROBLEMS **INCOMPATIBLE IF BRANCHES** This `if` has an `else` branch with a different type from it's `then` branch: -**if_then_else_simple_file.md:1:7:** +**if_then_else_simple_file.md:1:12:** ```roc foo = if 1 A @@ -25,7 +25,6 @@ foo = if 1 A "hello" } ``` - ^^^^^^^ The `else` branch has the type: _Str_ diff --git a/test/snapshots/statement/return_stmt_block_example.md b/test/snapshots/statement/return_stmt_block_example.md index 1816dd5e110..c2829b7e590 100644 --- a/test/snapshots/statement/return_stmt_block_example.md +++ b/test/snapshots/statement/return_stmt_block_example.md @@ -16,11 +16,11 @@ foo = |num| { } ~~~ # EXPECTED -INCOMPATIBLE IF BRANCHES - return_stmt_block_example.md:3:11:3:11 +INCOMPATIBLE IF BRANCHES - return_stmt_block_example.md:3:25:3:25 # PROBLEMS **INCOMPATIBLE IF BRANCHES** This `if` has an `else` branch with a different type from it's `then` branch: -**return_stmt_block_example.md:3:11:** +**return_stmt_block_example.md:3:25:** ```roc str = if (num > 10) { return Err(TooBig) @@ -28,7 +28,6 @@ This `if` has an `else` branch with a different type from it's `then` branch: "SMALL" } ``` - ^^^^^^^ The `else` branch has the type: _Str_