diff --git a/build.zig b/build.zig index 720e0af430..0d34d302c0 100644 --- a/build.zig +++ b/build.zig @@ -816,6 +816,9 @@ pub fn build(b: *std.Build) void { } // Ensure host library is copied before running the test run_fx_platform_test.step.dependOn(©_test_fx_host.step); + // Ensure roc binary is installed before running the test (test uses ./zig-out/bin/roc) + const install_roc_for_fx_test = b.addInstallArtifact(roc_exe, .{}); + run_fx_platform_test.step.dependOn(&install_roc_for_fx_test.step); tests_summary.addRun(&run_fx_platform_test.step); } diff --git a/src/base/CommonEnv.zig b/src/base/CommonEnv.zig index bad52f14c7..6986133cf9 100644 --- a/src/base/CommonEnv.zig +++ b/src/base/CommonEnv.zig @@ -117,18 +117,26 @@ pub const Serialized = extern struct { offset: i64, source: []const u8, ) *CommonEnv { - // Note: Serialized may be smaller than the runtime struct because: - // - Uses i64 offsets instead of usize pointers (same size on 64-bit, but conceptually different) - // - May have different alignment/padding requirements - // We deserialize by overwriting the Serialized memory with the runtime struct. + // CRITICAL: We must deserialize ALL fields into local variables BEFORE writing to the + // output struct. This is because CommonEnv is a regular struct (not extern), so Zig may + // reorder its fields differently than Serialized (which is extern). If we read from self + // while writing to env (which aliases self), we may read corrupted data in Release mode + // when field orderings differ. + + // Step 1: Deserialize all fields into local variables first + const deserialized_idents = self.idents.deserialize(offset).*; + const deserialized_strings = self.strings.deserialize(offset).*; + const deserialized_exposed_items = self.exposed_items.deserialize(offset).*; + const deserialized_line_starts = self.line_starts.deserialize(offset).*; + + // Step 2: Overwrite ourself with the deserialized version const env = @as(*CommonEnv, @ptrFromInt(@intFromPtr(self))); env.* = CommonEnv{ - .idents = self.idents.deserialize(offset).*, - // .ident_ids_for_slicing = self.ident_ids_for_slicing.deserialize(offset).*, - .strings = self.strings.deserialize(offset).*, - .exposed_items = self.exposed_items.deserialize(offset).*, - .line_starts = self.line_starts.deserialize(offset).*, + .idents = deserialized_idents, + .strings = deserialized_strings, + .exposed_items = deserialized_exposed_items, + .line_starts = deserialized_line_starts, .source = source, }; diff --git a/src/base/Ident.zig b/src/base/Ident.zig index 5b33731505..3b8eb52a30 100644 --- a/src/base/Ident.zig +++ b/src/base/Ident.zig @@ -16,10 +16,6 @@ const CompactWriter = collections.CompactWriter; const Ident = @This(); -/// Method name for parsing integers from digit lists - used by numeric literal type checking -pub const FROM_INT_DIGITS_METHOD_NAME = "from_int_digits"; -/// Method name for parsing decimals from digit lists - used by numeric literal type checking -pub const FROM_DEC_DIGITS_METHOD_NAME = "from_dec_digits"; /// Method name for addition - used by + operator desugaring pub const PLUS_METHOD_NAME = "plus"; /// Method name for negation - used by unary - operator desugaring @@ -124,15 +120,25 @@ pub const Store = struct { } /// Deserialize this Serialized struct into a Store - pub fn deserialize(self: *Serialized, offset: i64) *Store { - // Note: Serialized may be smaller than the runtime struct. - // We deserialize by overwriting the Serialized memory with the runtime struct. + pub noinline fn deserialize(self: *Serialized, offset: i64) *Store { + // CRITICAL: We must deserialize ALL fields into local variables BEFORE writing to the + // output struct. This is because Store is a regular struct (not extern), so Zig may + // reorder its fields differently than Serialized (which is extern). If we read from self + // while writing to store (which aliases self), we may read corrupted data in Release mode + // when field orderings differ. + + // Step 1: Deserialize all fields into local variables first + const deserialized_interner = self.interner.deserialize(offset).*; + const deserialized_attributes = self.attributes.deserialize(offset).*; + const deserialized_next_unique_name = self.next_unique_name; + + // Step 2: Overwrite ourself with the deserialized version const store = @as(*Store, @ptrFromInt(@intFromPtr(self))); store.* = Store{ - .interner = self.interner.deserialize(offset).*, - .attributes = self.attributes.deserialize(offset).*, - .next_unique_name = self.next_unique_name, + .interner = deserialized_interner, + .attributes = deserialized_attributes, + .next_unique_name = deserialized_next_unique_name, }; return store; diff --git a/src/base/SmallStringInterner.zig b/src/base/SmallStringInterner.zig index 64d8537b17..cdcd24c41f 100644 --- a/src/base/SmallStringInterner.zig +++ b/src/base/SmallStringInterner.zig @@ -226,14 +226,25 @@ pub const Serialized = extern struct { } /// Deserialize this Serialized struct into a SmallStringInterner - pub fn deserialize(self: *Serialized, offset: i64) *SmallStringInterner { - // Overwrite ourself with the deserialized version, and return our pointer after casting it to Self. + pub noinline fn deserialize(self: *Serialized, offset: i64) *SmallStringInterner { + // CRITICAL: We must deserialize ALL fields into local variables BEFORE writing to the + // output struct. This is because SmallStringInterner is a regular struct (not extern), so Zig may + // reorder its fields differently than Serialized (which is extern). If we read from self + // while writing to interner (which aliases self), we may read corrupted data in Release mode + // when field orderings differ. + + // Step 1: Deserialize all fields into local variables first + const deserialized_bytes = self.bytes.deserialize(offset).*; + const deserialized_hash_table = self.hash_table.deserialize(offset).*; + const deserialized_entry_count = self.entry_count; + + // Step 2: Overwrite ourself with the deserialized version const interner = @as(*SmallStringInterner, @ptrCast(self)); interner.* = .{ - .bytes = self.bytes.deserialize(offset).*, - .hash_table = self.hash_table.deserialize(offset).*, - .entry_count = self.entry_count, + .bytes = deserialized_bytes, + .hash_table = deserialized_hash_table, + .entry_count = deserialized_entry_count, }; return interner; diff --git a/src/base/StringLiteral.zig b/src/base/StringLiteral.zig index 9c3da1c637..891bef8175 100644 --- a/src/base/StringLiteral.zig +++ b/src/base/StringLiteral.zig @@ -113,12 +113,18 @@ pub const Store = struct { } /// Deserialize this Serialized struct into a Store - pub fn deserialize(self: *Serialized, offset: i64) *Store { - // Overwrite ourself with the deserialized version, and return our pointer after casting it to Self. + pub noinline fn deserialize(self: *Serialized, offset: i64) *Store { + // CRITICAL: We must deserialize ALL fields into local variables BEFORE writing to the + // output struct to avoid aliasing issues in Release mode. + + // Step 1: Deserialize all fields into local variables first + const deserialized_buffer = self.buffer.deserialize(offset).*; + + // Step 2: Overwrite ourself with the deserialized version const store = @as(*Store, @ptrFromInt(@intFromPtr(self))); store.* = Store{ - .buffer = self.buffer.deserialize(offset).*, + .buffer = deserialized_buffer, }; return store; diff --git a/src/build/builtin_compiler/main.zig b/src/build/builtin_compiler/main.zig index 4c1a81d02b..078f046513 100644 --- a/src/build/builtin_compiler/main.zig +++ b/src/build/builtin_compiler/main.zig @@ -236,29 +236,6 @@ fn replaceStrIsEmptyWithLowLevel(env: *ModuleEnv) !std.ArrayList(CIR.Def.Idx) { } } - // Numeric parsing operations (all numeric types have from_int_digits) - for (numeric_types) |num_type| { - var buf: [256]u8 = undefined; - - // from_int_digits - const from_int_digits = try std.fmt.bufPrint(&buf, "Builtin.Num.{s}.from_int_digits", .{num_type}); - if (env.common.findIdent(from_int_digits)) |ident| { - try low_level_map.put(ident, .num_from_int_digits); - } - } - - // from_dec_digits (Dec, F32, F64 only) - const dec_types = [_][]const u8{ "Dec", "F32", "F64" }; - for (dec_types) |num_type| { - var buf: [256]u8 = undefined; - - // from_dec_digits - const from_dec_digits = try std.fmt.bufPrint(&buf, "Builtin.Num.{s}.from_dec_digits", .{num_type}); - if (env.common.findIdent(from_dec_digits)) |ident| { - try low_level_map.put(ident, .num_from_dec_digits); - } - } - // from_numeral (all numeric types) for (numeric_types) |num_type| { var buf: [256]u8 = undefined; @@ -341,7 +318,7 @@ fn replaceStrIsEmptyWithLowLevel(env: *ModuleEnv) !std.ArrayList(CIR.Def.Idx) { // Create parameter patterns for the lambda // Binary operations need 2 parameters, unary operations need 1 const num_params: u32 = switch (low_level_op) { - .num_negate, .num_is_zero, .num_is_negative, .num_is_positive, .num_from_numeral, .num_from_int_digits, .u8_to_str, .i8_to_str, .u16_to_str, .i16_to_str, .u32_to_str, .i32_to_str, .u64_to_str, .i64_to_str, .u128_to_str, .i128_to_str, .dec_to_str, .f32_to_str, .f64_to_str => 1, + .num_negate, .num_is_zero, .num_is_negative, .num_is_positive, .num_from_numeral, .u8_to_str, .i8_to_str, .u16_to_str, .i16_to_str, .u32_to_str, .i32_to_str, .u64_to_str, .i64_to_str, .u128_to_str, .i128_to_str, .dec_to_str, .f32_to_str, .f64_to_str => 1, else => 2, // Most numeric operations are binary }; diff --git a/src/build/roc/Builtin.roc b/src/build/roc/Builtin.roc index 104fa1035d..0967106b8c 100644 --- a/src/build/roc/Builtin.roc +++ b/src/build/roc/Builtin.roc @@ -58,7 +58,7 @@ Builtin :: [].{ Box(item) :: [ProvidedByCompiler].{} - Try(ok, err) := [Ok(ok), Err(err)].{ + Try(ok, err) := [Err(err), Ok(ok)].{ is_ok : Try(_ok, _err) -> Bool is_ok = |try| match try { Ok(_) => True @@ -154,7 +154,6 @@ Builtin :: [].{ div_trunc_by : U8, U8 -> U8 rem_by : U8, U8 -> U8 - from_int_digits : List(U8) -> Try(U8, [OutOfRange]) from_numeral : Numeral -> Try(U8, [InvalidNumeral(Str)]) } @@ -177,7 +176,6 @@ Builtin :: [].{ div_trunc_by : I8, I8 -> I8 rem_by : I8, I8 -> I8 - from_int_digits : List(U8) -> Try(I8, [OutOfRange]) from_numeral : Numeral -> Try(I8, [InvalidNumeral(Str)]) } @@ -197,7 +195,6 @@ Builtin :: [].{ div_trunc_by : U16, U16 -> U16 rem_by : U16, U16 -> U16 - from_int_digits : List(U8) -> Try(U16, [OutOfRange]) from_numeral : Numeral -> Try(U16, [InvalidNumeral(Str)]) } @@ -220,7 +217,6 @@ Builtin :: [].{ div_trunc_by : I16, I16 -> I16 rem_by : I16, I16 -> I16 - from_int_digits : List(U8) -> Try(I16, [OutOfRange]) from_numeral : Numeral -> Try(I16, [InvalidNumeral(Str)]) } @@ -240,7 +236,6 @@ Builtin :: [].{ div_trunc_by : U32, U32 -> U32 rem_by : U32, U32 -> U32 - from_int_digits : List(U8) -> Try(U32, [OutOfRange]) from_numeral : Numeral -> Try(U32, [InvalidNumeral(Str)]) } @@ -263,7 +258,6 @@ Builtin :: [].{ div_trunc_by : I32, I32 -> I32 rem_by : I32, I32 -> I32 - from_int_digits : List(U8) -> Try(I32, [OutOfRange]) from_numeral : Numeral -> Try(I32, [InvalidNumeral(Str)]) } @@ -283,7 +277,6 @@ Builtin :: [].{ div_trunc_by : U64, U64 -> U64 rem_by : U64, U64 -> U64 - from_int_digits : List(U8) -> Try(U64, [OutOfRange]) from_numeral : Numeral -> Try(U64, [InvalidNumeral(Str)]) } @@ -306,7 +299,6 @@ Builtin :: [].{ div_trunc_by : I64, I64 -> I64 rem_by : I64, I64 -> I64 - from_int_digits : List(U8) -> Try(I64, [OutOfRange]) from_numeral : Numeral -> Try(I64, [InvalidNumeral(Str)]) } @@ -326,7 +318,6 @@ Builtin :: [].{ div_trunc_by : U128, U128 -> U128 rem_by : U128, U128 -> U128 - from_int_digits : List(U8) -> Try(U128, [OutOfRange]) from_numeral : Numeral -> Try(U128, [InvalidNumeral(Str)]) } @@ -349,7 +340,6 @@ Builtin :: [].{ div_trunc_by : I128, I128 -> I128 rem_by : I128, I128 -> I128 - from_int_digits : List(U8) -> Try(I128, [OutOfRange]) from_numeral : Numeral -> Try(I128, [InvalidNumeral(Str)]) } @@ -373,8 +363,6 @@ Builtin :: [].{ div_trunc_by : Dec, Dec -> Dec rem_by : Dec, Dec -> Dec - from_int_digits : List(U8) -> Try(Dec, [OutOfRange]) - from_dec_digits : (List(U8), List(U8)) -> Try(Dec, [OutOfRange]) from_numeral : Numeral -> Try(Dec, [InvalidNumeral(Str)]) } @@ -396,8 +384,6 @@ Builtin :: [].{ div_trunc_by : F32, F32 -> F32 rem_by : F32, F32 -> F32 - from_int_digits : List(U8) -> Try(F32, [OutOfRange]) - from_dec_digits : (List(U8), List(U8)) -> Try(F32, [OutOfRange]) from_numeral : Numeral -> Try(F32, [InvalidNumeral(Str)]) } @@ -419,8 +405,6 @@ Builtin :: [].{ div_trunc_by : F64, F64 -> F64 rem_by : F64, F64 -> F64 - from_int_digits : List(U8) -> Try(F64, [OutOfRange]) - from_dec_digits : (List(U8), List(U8)) -> Try(F64, [OutOfRange]) from_numeral : Numeral -> Try(F64, [InvalidNumeral(Str)]) } } diff --git a/src/canonicalize/CIR.zig b/src/canonicalize/CIR.zig index 330d904bee..bbc6144506 100644 --- a/src/canonicalize/CIR.zig +++ b/src/canonicalize/CIR.zig @@ -704,13 +704,17 @@ pub const Import = struct { } /// Deserialize this Serialized struct into a Store - pub fn deserialize(self: *Serialized, offset: i64, allocator: std.mem.Allocator) std.mem.Allocator.Error!*Store { - // Overwrite ourself with the deserialized version, and return our pointer after casting it to Store. + pub noinline fn deserialize(self: *Serialized, offset: i64, allocator: std.mem.Allocator) std.mem.Allocator.Error!*Store { + // CRITICAL: Deserialize nested struct BEFORE casting and writing to store + // to avoid aliasing issues in Release mode. + const deserialized_imports = self.imports.deserialize(offset).*; + + // Now we can cast and write to the destination const store = @as(*Store, @ptrFromInt(@intFromPtr(self))); store.* = .{ .map = .{}, // Will be repopulated below - .imports = self.imports.deserialize(offset).*, + .imports = deserialized_imports, }; // Pre-allocate the exact capacity needed for the map diff --git a/src/canonicalize/Expression.zig b/src/canonicalize/Expression.zig index 08e4cabd28..d03f4a0dee 100644 --- a/src/canonicalize/Expression.zig +++ b/src/canonicalize/Expression.zig @@ -453,8 +453,6 @@ pub const Expr = union(enum) { num_rem_by, // All numeric types // Numeric parsing operations - num_from_int_digits, // Parse List(U8) -> Try(num, [OutOfRange]) - num_from_dec_digits, // Parse (List(U8), List(U8)) -> Try(num, [OutOfRange]) num_from_numeral, // Parse Numeral -> Try(num, [InvalidNumeral(Str)]) }; diff --git a/src/canonicalize/ModuleEnv.zig b/src/canonicalize/ModuleEnv.zig index 5004e7de4c..c62de74fbc 100644 --- a/src/canonicalize/ModuleEnv.zig +++ b/src/canonicalize/ModuleEnv.zig @@ -132,10 +132,9 @@ evaluation_order: ?*DependencyGraph.EvaluationOrder, // Cached well-known identifiers for type checking // (These are interned once during init to avoid repeated string comparisons during type checking) -/// Interned identifier for "from_int_digits" - used for numeric literal type checking -from_int_digits_ident: Ident.Idx, -/// Interned identifier for "from_dec_digits" - used for decimal literal type checking -from_dec_digits_ident: Ident.Idx, +/// Padding for removed from_dec_digits_ident field (maintains struct layout for serialization) +_padding_removed_from_dec_digits: u32 = 0, + /// Interned identifier for "Try" - used for numeric literal type checking try_ident: Ident.Idx, /// Interned identifier for "OutOfRange" - used for numeric literal type checking @@ -233,8 +232,6 @@ pub fn init(gpa: std.mem.Allocator, source: []const u8) std.mem.Allocator.Error! var common = try CommonEnv.init(gpa, source); // Intern well-known identifiers once during initialization for fast type checking - const from_int_digits_ident = try common.insertIdent(gpa, Ident.for_text(Ident.FROM_INT_DIGITS_METHOD_NAME)); - const from_dec_digits_ident = try common.insertIdent(gpa, Ident.for_text(Ident.FROM_DEC_DIGITS_METHOD_NAME)); const try_ident = try common.insertIdent(gpa, Ident.for_text("Try")); const out_of_range_ident = try common.insertIdent(gpa, Ident.for_text("OutOfRange")); const builtin_module_ident = try common.insertIdent(gpa, Ident.for_text("Builtin")); @@ -284,8 +281,6 @@ pub fn init(gpa: std.mem.Allocator, source: []const u8) std.mem.Allocator.Error! .diagnostics = CIR.Diagnostic.Span{ .span = base.DataSpan{ .start = 0, .len = 0 } }, .store = try NodeStore.initCapacity(gpa, 10_000), // Default node store capacity .evaluation_order = null, // Will be set after canonicalization completes - .from_int_digits_ident = from_int_digits_ident, - .from_dec_digits_ident = from_dec_digits_ident, .try_ident = try_ident, .out_of_range_ident = out_of_range_ident, .builtin_module_ident = builtin_module_ident, @@ -1722,8 +1717,7 @@ pub const Serialized = extern struct { store: NodeStore.Serialized, module_kind: ModuleKind.Serialized, evaluation_order_reserved: u64, // Reserved space for evaluation_order field (required for in-place deserialization cast) - from_int_digits_ident_reserved: u32, // Reserved space for from_int_digits_ident field (interned during deserialization) - from_dec_digits_ident_reserved: u32, // Reserved space for from_dec_digits_ident field (interned during deserialization) + _padding_removed_from_dec_digits: u32 = 0, // Padding for removed from_dec_digits_ident field (maintains struct layout) try_ident_reserved: u32, // Reserved space for try_ident field (interned during deserialization) out_of_range_ident_reserved: u32, // Reserved space for out_of_range_ident field (interned during deserialization) builtin_module_ident_reserved: u32, // Reserved space for builtin_module_ident field (interned during deserialization) @@ -1777,8 +1771,6 @@ pub const Serialized = extern struct { self.module_name = .{ 0, 0 }; self.module_name_idx_reserved = 0; self.evaluation_order_reserved = 0; - self.from_int_digits_ident_reserved = 0; - self.from_dec_digits_ident_reserved = 0; self.try_ident_reserved = 0; self.out_of_range_ident_reserved = 0; self.builtin_module_ident_reserved = 0; @@ -1799,7 +1791,7 @@ pub const Serialized = extern struct { } /// Deserialize a ModuleEnv from the buffer, updating the ModuleEnv in place - pub fn deserialize( + pub noinline fn deserialize( self: *Serialized, offset: i64, gpa: std.mem.Allocator, @@ -1811,49 +1803,66 @@ pub const Serialized = extern struct { // On 32-bit platforms, Serialized may be larger due to using fixed-size types for platform-independent serialization. comptime std.debug.assert(@sizeOf(@This()) >= @sizeOf(Self)); - // Overwrite ourself with the deserialized version, and return our pointer after casting it to Self. + // CRITICAL: We must deserialize ALL fields into local variables BEFORE writing to the + // output struct. This is because ModuleEnv is a regular struct (not extern), so Zig may + // reorder its fields differently than Serialized (which is extern). If we read from self + // while writing to env (which aliases self), we may read corrupted data in Release mode + // when field orderings differ. + // + // Following the same pattern as NodeStore.deserialize to avoid aliasing issues. + + // Step 1: Deserialize all complex fields into local variables first + const deserialized_common = self.common.deserialize(offset, source).*; + const deserialized_types = self.types.deserialize(offset, gpa).*; + const deserialized_module_kind = self.module_kind.decode(); + const deserialized_all_defs = self.all_defs; + const deserialized_all_statements = self.all_statements; + const deserialized_exports = self.exports; + const deserialized_builtin_statements = self.builtin_statements; + const deserialized_external_decls = self.external_decls.deserialize(offset).*; + const deserialized_imports = (try self.imports.deserialize(offset, gpa)).*; + const deserialized_diagnostics = self.diagnostics; + const deserialized_store = self.store.deserialize(offset, gpa).*; + const deserialized_deferred_numeric_literals = self.deferred_numeric_literals.deserialize(offset).*; + + // Step 2: Overwrite ourself with the deserialized version const env = @as(*Self, @ptrFromInt(@intFromPtr(self))); - // Deserialize common env first so we can look up identifiers - const common = self.common.deserialize(offset, source).*; - env.* = Self{ .gpa = gpa, - .common = common, - .types = self.types.deserialize(offset, gpa).*, - .module_kind = self.module_kind.decode(), - .all_defs = self.all_defs, - .all_statements = self.all_statements, - .exports = self.exports, - .builtin_statements = self.builtin_statements, - .external_decls = self.external_decls.deserialize(offset).*, - .imports = (try self.imports.deserialize(offset, gpa)).*, + .common = deserialized_common, + .types = deserialized_types, + .module_kind = deserialized_module_kind, + .all_defs = deserialized_all_defs, + .all_statements = deserialized_all_statements, + .exports = deserialized_exports, + .builtin_statements = deserialized_builtin_statements, + .external_decls = deserialized_external_decls, + .imports = deserialized_imports, .module_name = module_name, .module_name_idx = undefined, // Not used for deserialized modules (only needed during fresh canonicalization) - .diagnostics = self.diagnostics, - .store = self.store.deserialize(offset, gpa).*, + .diagnostics = deserialized_diagnostics, + .store = deserialized_store, .evaluation_order = null, // Not serialized, will be recomputed if needed // Well-known identifiers for type checking - look them up in the deserialized common env - .from_int_digits_ident = common.findIdent(Ident.FROM_INT_DIGITS_METHOD_NAME) orelse unreachable, - .from_dec_digits_ident = common.findIdent(Ident.FROM_DEC_DIGITS_METHOD_NAME) orelse unreachable, - .try_ident = common.findIdent("Try") orelse unreachable, - .out_of_range_ident = common.findIdent("OutOfRange") orelse unreachable, - .builtin_module_ident = common.findIdent("Builtin") orelse unreachable, - .plus_ident = common.findIdent(Ident.PLUS_METHOD_NAME) orelse unreachable, - .minus_ident = common.findIdent("minus") orelse unreachable, - .times_ident = common.findIdent("times") orelse unreachable, - .div_by_ident = common.findIdent("div_by") orelse unreachable, - .div_trunc_by_ident = common.findIdent("div_trunc_by") orelse unreachable, - .rem_by_ident = common.findIdent("rem_by") orelse unreachable, - .negate_ident = common.findIdent(Ident.NEGATE_METHOD_NAME) orelse unreachable, - .not_ident = common.findIdent("not") orelse unreachable, - .is_lt_ident = common.findIdent("is_lt") orelse unreachable, - .is_lte_ident = common.findIdent("is_lte") orelse unreachable, - .is_gt_ident = common.findIdent("is_gt") orelse unreachable, - .is_gte_ident = common.findIdent("is_gte") orelse unreachable, - .is_eq_ident = common.findIdent("is_eq") orelse unreachable, - .is_ne_ident = common.findIdent("is_ne") orelse unreachable, - .deferred_numeric_literals = self.deferred_numeric_literals.deserialize(offset).*, + .try_ident = deserialized_common.findIdent("Try") orelse unreachable, + .out_of_range_ident = deserialized_common.findIdent("OutOfRange") orelse unreachable, + .builtin_module_ident = deserialized_common.findIdent("Builtin") orelse unreachable, + .plus_ident = deserialized_common.findIdent(Ident.PLUS_METHOD_NAME) orelse unreachable, + .minus_ident = deserialized_common.findIdent("minus") orelse unreachable, + .times_ident = deserialized_common.findIdent("times") orelse unreachable, + .div_by_ident = deserialized_common.findIdent("div_by") orelse unreachable, + .div_trunc_by_ident = deserialized_common.findIdent("div_trunc_by") orelse unreachable, + .rem_by_ident = deserialized_common.findIdent("rem_by") orelse unreachable, + .negate_ident = deserialized_common.findIdent(Ident.NEGATE_METHOD_NAME) orelse unreachable, + .not_ident = deserialized_common.findIdent("not") orelse unreachable, + .is_lt_ident = deserialized_common.findIdent("is_lt") orelse unreachable, + .is_lte_ident = deserialized_common.findIdent("is_lte") orelse unreachable, + .is_gt_ident = deserialized_common.findIdent("is_gt") orelse unreachable, + .is_gte_ident = deserialized_common.findIdent("is_gte") orelse unreachable, + .is_eq_ident = deserialized_common.findIdent("is_eq") orelse unreachable, + .is_ne_ident = deserialized_common.findIdent("is_ne") orelse unreachable, + .deferred_numeric_literals = deserialized_deferred_numeric_literals, }; return env; diff --git a/src/canonicalize/NodeStore.zig b/src/canonicalize/NodeStore.zig index bd47e1fa14..d9b2c83a2b 100644 --- a/src/canonicalize/NodeStore.zig +++ b/src/canonicalize/NodeStore.zig @@ -3526,7 +3526,7 @@ pub const Serialized = extern struct { } /// Deserialize this Serialized struct into a NodeStore - pub fn deserialize(self: *Serialized, offset: i64, gpa: Allocator) *NodeStore { + pub noinline fn deserialize(self: *Serialized, offset: i64, gpa: Allocator) *NodeStore { // Note: Serialized may be smaller than the runtime struct. // CRITICAL: On 32-bit platforms, deserializing nodes in-place corrupts the adjacent // regions and extra_data fields. We must deserialize in REVERSE order (last to first) diff --git a/src/check/test/TestEnv.zig b/src/check/test/TestEnv.zig index b6757d10bf..a2dacd50d2 100644 --- a/src/check/test/TestEnv.zig +++ b/src/check/test/TestEnv.zig @@ -85,8 +85,6 @@ fn loadCompiledModule(gpa: std.mem.Allocator, bin_data: []const u8, module_name: .diagnostics = serialized_ptr.diagnostics, .store = serialized_ptr.store.deserialize(@as(i64, @intCast(base_ptr)), gpa).*, .evaluation_order = null, - .from_int_digits_ident = common.findIdent(base.Ident.FROM_INT_DIGITS_METHOD_NAME) orelse unreachable, - .from_dec_digits_ident = common.findIdent(base.Ident.FROM_DEC_DIGITS_METHOD_NAME) orelse unreachable, .try_ident = common.findIdent("Try") orelse unreachable, .out_of_range_ident = common.findIdent("OutOfRange") orelse unreachable, .builtin_module_ident = common.findIdent("Builtin") orelse unreachable, diff --git a/src/check/test/type_checking_integration.zig b/src/check/test/type_checking_integration.zig index 5bb1901df0..6ed2e1cd9b 100644 --- a/src/check/test/type_checking_integration.zig +++ b/src/check/test/type_checking_integration.zig @@ -2109,9 +2109,7 @@ test "check type - equirecursive static dispatch with type annotation" { // with the same constraint structure as the motivating example. const source = \\fn : a, b -> ret where [ - \\ a.plus : a, b -> ret, - \\ a.from_int_digits : List(U8) -> Try(a, [OutOfRange]), - \\ b.from_int_digits : List(U8) -> Try(b, [OutOfRange]) + \\ a.plus : a, b -> ret \\] \\fn = |a, b| (|x| x.plus(b))(a) ; @@ -2120,7 +2118,7 @@ test "check type - equirecursive static dispatch with type annotation" { try checkTypesModule( source, .{ .pass = .{ .def = "fn" } }, - "a, b -> ret where [a.plus : a, b -> ret, a.from_int_digits : List(U8) -> Try(a, [OutOfRange]), b.from_int_digits : List(U8) -> Try(b, [OutOfRange])]", + "a, b -> ret where [a.plus : a, b -> ret]", ); } diff --git a/src/check/unify.zig b/src/check/unify.zig index 86afe48987..6e1c261cec 100644 --- a/src/check/unify.zig +++ b/src/check/unify.zig @@ -1116,132 +1116,6 @@ const Unifier = struct { self.merge(vars, vars.b.desc.content); } - /// Check if a nominal type has a from_int_digits method by unifying its signature - /// with the expected type: List(U8) -> Try(Self, [OutOfRange]) - /// Returns true if unification succeeds, false otherwise. - fn nominalTypeHasFromIntDigits( - self: *Self, - nominal_type: NominalType, - ) Error!bool { - const method_var = try self.getNominalMethodVar(nominal_type, self.module_env.from_int_digits_ident) orelse return false; - const resolved = self.types_store.resolveVar(method_var); - - const func = switch (resolved.desc.content) { - .structure => |structure| switch (structure) { - .fn_pure => structure.fn_pure, - .fn_effectful => structure.fn_effectful, - .fn_unbound => structure.fn_unbound, - else => return false, - }, - else => return false, - }; - - const ret_desc = self.types_store.resolveVar(func.ret); - const ret_nominal = switch (ret_desc.desc.content) { - .structure => |structure| switch (structure) { - .nominal_type => structure.nominal_type, - else => return false, - }, - else => return false, - }; - if (!self.isBuiltinTryNominal(ret_nominal)) return false; - const ret_args = self.types_store.sliceVars(ret_nominal.vars.nonempty); - if (ret_args.len < 3) return false; - - const args_slice = self.types_store.sliceVars(func.args); - if (args_slice.len != 1) return false; - - const list_u8_var = try self.createListU8Var(); - self.unifyGuarded(args_slice[0], list_u8_var) catch return false; - - const self_ret_var = try self.createNominalInstanceVar(nominal_type); - self.unifyGuarded(ret_args[1], self_ret_var) catch return false; - - const try_error_var = try self.createOutOfRangeTagUnion(); - self.unifyGuarded(ret_args[2], try_error_var) catch return false; - - return true; - } - - /// Check if a nominal type has a from_dec_digits method by unifying its signature - /// with the expected type: (List(U8), List(U8)) -> Try(Self, [OutOfRange]) - /// Returns true if unification succeeds, false otherwise. - fn nominalTypeHasFromDecDigits( - self: *Self, - nominal_type: NominalType, - ) Error!bool { - const method_var = try self.getNominalMethodVar(nominal_type, self.module_env.from_dec_digits_ident) orelse return false; - const resolved = self.types_store.resolveVar(method_var); - - const func = switch (resolved.desc.content) { - .structure => |structure| switch (structure) { - .fn_pure => structure.fn_pure, - .fn_effectful => structure.fn_effectful, - .fn_unbound => structure.fn_unbound, - else => return false, - }, - else => return false, - }; - - const ret_desc = self.types_store.resolveVar(func.ret); - const ret_nominal = switch (ret_desc.desc.content) { - .structure => |structure| switch (structure) { - .nominal_type => structure.nominal_type, - else => return false, - }, - else => return false, - }; - if (!self.isBuiltinTryNominal(ret_nominal)) return false; - const ret_args = self.types_store.sliceVars(ret_nominal.vars.nonempty); - if (ret_args.len < 3) return false; - - const args_slice = self.types_store.sliceVars(func.args); - if (args_slice.len != 1) return false; - - const before_ident = self.module_env.common.findIdent("before_dot") orelse return false; - const after_ident = self.module_env.common.findIdent("after_dot") orelse return false; - - const record_desc = self.types_store.resolveVar(args_slice[0]); - const record = switch (record_desc.desc.content) { - .structure => |structure| switch (structure) { - .record => structure.record, - else => return false, - }, - else => return false, - }; - - if (record.fields.len() != 2) return false; - const fields_slice = self.types_store.getRecordFieldsSlice(record.fields); - const names = fields_slice.items(.name); - const vars = fields_slice.items(.var_); - - var before_idx: ?usize = null; - var after_idx: ?usize = null; - for (names, 0..) |name, idx| { - if (name == before_ident) { - before_idx = idx; - } else if (name == after_ident) { - after_idx = idx; - } - } - - if (before_idx == null or after_idx == null) return false; - - const list_u8_first = try self.createListU8Var(); - const list_u8_second = try self.createListU8Var(); - - self.unifyGuarded(vars[before_idx.?], list_u8_first) catch return false; - self.unifyGuarded(vars[after_idx.?], list_u8_second) catch return false; - - const self_ret_var = try self.createNominalInstanceVar(nominal_type); - self.unifyGuarded(ret_args[1], self_ret_var) catch return false; - - const try_error_var = try self.createOutOfRangeTagUnion(); - self.unifyGuarded(ret_args[2], try_error_var) catch return false; - - return true; - } - fn getNominalMethodVar( self: *Self, nominal_type: NominalType, diff --git a/src/cli/main.zig b/src/cli/main.zig index 5996a941ff..3dd5b7b65d 100644 --- a/src/cli/main.zig +++ b/src/cli/main.zig @@ -110,6 +110,31 @@ pub const c = struct { pub const link = std.c.link; pub const ftruncate = std.c.ftruncate; pub const _errno = std.c._errno; + + // POSIX wait status macros + pub fn WIFEXITED(status: c_int) bool { + return WTERMSIG(status) == 0; + } + + pub fn WEXITSTATUS(status: c_int) u8 { + return @intCast((status >> 8) & 0xff); + } + + pub fn WIFSIGNALED(status: c_int) bool { + return ((status & 0x7f) + 1) >> 1 > 0; + } + + pub fn WTERMSIG(status: c_int) u8 { + return @intCast(status & 0x7f); + } + + pub fn WIFSTOPPED(status: c_int) bool { + return (status & 0xff) == 0x7f; + } + + pub fn WSTOPSIG(status: c_int) u8 { + return WEXITSTATUS(status); + } }; // Platform-specific shared memory implementation @@ -170,10 +195,13 @@ fn stderrWriter() *std.Io.Writer { const posix = if (!is_windows) struct { extern "c" fn shm_open(name: [*:0]const u8, oflag: c_int, mode: std.c.mode_t) c_int; extern "c" fn shm_unlink(name: [*:0]const u8) c_int; - extern "c" fn mmap(addr: ?*anyopaque, len: usize, prot: c_int, flags: c_int, fd: c_int, offset: std.c.off_t) ?*anyopaque; + // NOTE: mmap returns MAP_FAILED ((void*)-1) on error, NOT NULL! + extern "c" fn mmap(addr: ?*anyopaque, len: usize, prot: c_int, flags: c_int, fd: c_int, offset: std.c.off_t) usize; extern "c" fn munmap(addr: *anyopaque, len: usize) c_int; extern "c" fn fcntl(fd: c_int, cmd: c_int, arg: c_int) c_int; + /// MAP_FAILED is (void*)-1, which is maxInt(usize) + const MAP_FAILED: usize = std.math.maxInt(usize); // fcntl constants const F_GETFD = 1; const F_SETFD = 2; @@ -1086,7 +1114,15 @@ fn rocRun(allocs: *Allocators, args: cli_args.RunArgs) !void { std.log.debug("Interpreter execution completed", .{}); } -/// Run child process using Windows handle inheritance (idiomatic Windows approach) +/// Run child process using Windows handle inheritance. +/// +/// We use direct Win32 APIs (CreateProcessW) rather than library abstractions +/// to ensure no interference with our handle inheritance. The handle value is +/// passed to the child via command line arguments, so we don't depend on any +/// specific handle number - the child receives the value and uses it directly. +/// +/// This mirrors the POSIX implementation which uses direct fork/exec for the +/// same reason. fn runWithWindowsHandleInheritance(allocs: *Allocators, exe_path: []const u8, shm_handle: SharedMemoryHandle) !void { // Make the shared memory handle inheritable if (windows.SetHandleInformation(@ptrCast(shm_handle.fd), windows.HANDLE_FLAG_INHERIT, windows.HANDLE_FLAG_INHERIT) == 0) { @@ -1173,7 +1209,19 @@ fn runWithWindowsHandleInheritance(allocs: *Allocators, exe_path: []const u8, sh std.log.debug("Child process completed successfully", .{}); } -/// Run child process using POSIX file descriptor inheritance (existing approach for Unix) +/// Run child process using POSIX file descriptor inheritance. +/// +/// We use direct fork/exec syscalls rather than std.process.Child to ensure +/// no interference with our fd inheritance. Library abstractions may manipulate +/// fds between fork and exec (e.g. for progress reporting), which can clobber +/// our shared memory fd if it happens to use the same fd number. +/// +/// The fd number is communicated to the child via a coordination file, so we +/// don't depend on any specific fd number - whatever fd the kernel assigned +/// for the shared memory will be inherited and used correctly. +/// +/// This mirrors the Windows implementation which uses direct Win32 APIs for +/// the same reason. fn runWithPosixFdInheritance(allocs: *Allocators, exe_path: []const u8, shm_handle: SharedMemoryHandle, cache_manager: *CacheManager) !void { // Get cache directory for temporary files const temp_cache_dir = cache_manager.config.getTempDir(allocs.arena) catch |err| { @@ -1198,7 +1246,7 @@ fn runWithPosixFdInheritance(allocs: *Allocators, exe_path: []const u8, shm_hand }; std.log.debug("Temporary executable created at: {s}", .{temp_exe_path}); - // Configure fd inheritance + // Configure fd inheritance - clear FD_CLOEXEC so child inherits the fd var flags = posix.fcntl(shm_handle.fd, posix.F_GETFD, 0); if (flags < 0) { std.log.err("Failed to get fd flags: {}", .{c._errno().*}); @@ -1212,61 +1260,79 @@ fn runWithPosixFdInheritance(allocs: *Allocators, exe_path: []const u8, shm_hand return error.FdConfigFailed; } - // Run the interpreter as a child process from the temp directory - var child = std.process.Child.init(&.{temp_exe_path}, allocs.gpa); - child.cwd = std.fs.cwd().realpathAlloc(allocs.arena, ".") catch |err| { - std.log.err("Failed to get current directory: {}", .{err}); - return err; - }; + // We use direct fork/exec instead of std.process.Child here. + // + // std.process.Child can perform internal fd manipulations between fork and exec + // (e.g. setting up pipes for progress reporting on specific fd numbers). + // If our shared memory happens to be on one of those fd numbers, it gets + // clobbered before the child can use it. + // + // By using raw fork/exec, we guarantee that NO code runs between fork and + // exec that could interfere with our fd table. The child inherits exactly + // the fds we configured, regardless of what fd numbers they happen to have. + // + // This mirrors the Windows implementation which also uses direct OS APIs + // (CreateProcessW) rather than library abstractions. + std.log.debug("Spawning child process via fork/exec: {s}", .{temp_exe_path}); + + const pid = std.c.fork(); + if (pid < 0) { + std.log.err("fork() failed: {}", .{c._errno().*}); + return error.ForkFailed; + } - // Forward stdout and stderr - child.stdout_behavior = .Inherit; - child.stderr_behavior = .Inherit; + if (pid == 0) { + // Child process - exec the interpreter + // Create null-terminated path for execve + const path_z = allocs.arena.dupeZ(u8, temp_exe_path) catch { + std.c._exit(127); + }; + const argv = [_:null]?[*:0]const u8{path_z}; + const envp = [_:null]?[*:0]const u8{}; - // Spawn the child process - std.log.debug("Spawning child process: {s}", .{temp_exe_path}); - std.log.debug("Child process working directory: {s}", .{child.cwd.?}); - child.spawn() catch |err| { - std.log.err("Failed to spawn {s}: {}", .{ temp_exe_path, err }); - return err; - }; - std.log.debug("Child process spawned successfully (PID: {})", .{child.id}); + _ = std.c.execve(path_z, &argv, &envp); - // Wait for child to complete - const term = child.wait() catch |err| { - std.log.err("Failed waiting for child process: {}", .{err}); - return err; - }; + // If execve returns, it failed + std.c._exit(127); + } - // Check the termination status - switch (term) { - .Exited => |exit_code| { - if (exit_code == 0) { - std.log.debug("Child process completed successfully", .{}); - } else { - std.log.err("Child process {s} exited with code: {}", .{ temp_exe_path, exit_code }); - return error.ProcessExitedWithError; - } - }, - .Signal => |signal| { - std.log.err("Child process {s} killed by signal: {}", .{ temp_exe_path, signal }); - if (signal == 11) { // SIGSEGV - std.log.err("Child process crashed with segmentation fault (SIGSEGV)", .{}); - } else if (signal == 6) { // SIGABRT - std.log.err("Child process aborted (SIGABRT)", .{}); - } else if (signal == 9) { // SIGKILL - std.log.err("Child process was killed (SIGKILL)", .{}); - } - return error.ProcessKilledBySignal; - }, - .Stopped => |signal| { - std.log.err("Child process {s} stopped by signal: {}", .{ temp_exe_path, signal }); - return error.ProcessStopped; - }, - .Unknown => |status| { - std.log.err("Child process {s} terminated with unknown status: {}", .{ temp_exe_path, status }); - return error.ProcessUnknownTermination; - }, + // Parent process - wait for child + std.log.debug("Child process spawned (PID: {})", .{pid}); + + var status: c_int = 0; + const wait_result = std.c.waitpid(pid, &status, 0); + if (wait_result < 0) { + std.log.err("waitpid() failed: {}", .{c._errno().*}); + return error.WaitFailed; + } + + // Check termination status using POSIX macros + if (c.WIFEXITED(status)) { + const exit_code = c.WEXITSTATUS(status); + if (exit_code == 0) { + std.log.debug("Child process completed successfully", .{}); + } else { + std.log.err("Child process {s} exited with code: {}", .{ temp_exe_path, exit_code }); + return error.ProcessExitedWithError; + } + } else if (c.WIFSIGNALED(status)) { + const signal = c.WTERMSIG(status); + std.log.err("Child process {s} killed by signal: {}", .{ temp_exe_path, signal }); + if (signal == 11) { // SIGSEGV + std.log.err("Child process crashed with segmentation fault (SIGSEGV)", .{}); + } else if (signal == 6) { // SIGABRT + std.log.err("Child process aborted (SIGABRT)", .{}); + } else if (signal == 9) { // SIGKILL + std.log.err("Child process was killed (SIGKILL)", .{}); + } + return error.ProcessKilledBySignal; + } else if (c.WIFSTOPPED(status)) { + const signal = c.WSTOPSIG(status); + std.log.err("Child process {s} stopped by signal: {}", .{ temp_exe_path, signal }); + return error.ProcessStopped; + } else { + std.log.err("Child process {s} terminated with unknown status: {}", .{ temp_exe_path, status }); + return error.ProcessUnknownTermination; } } @@ -1797,17 +1863,20 @@ fn writeToPosixSharedMemory(data: []const u8, total_size: usize) !SharedMemoryHa } // Map the shared memory - const mapped_ptr = posix.mmap( + const mmap_result = posix.mmap( null, total_size, 0x01 | 0x02, // PROT_READ | PROT_WRITE 0x0001, // MAP_SHARED shm_fd, 0, - ) orelse { + ); + // Check for MAP_FAILED (-1), NOT null! + if (mmap_result == posix.MAP_FAILED) { _ = c.close(shm_fd); return error.SharedMemoryMapFailed; - }; + } + const mapped_ptr: *anyopaque = @ptrFromInt(mmap_result); const mapped_memory = @as([*]u8, @ptrCast(mapped_ptr))[0..total_size]; // Write length at the beginning diff --git a/src/collections/ExposedItems.zig b/src/collections/ExposedItems.zig index baf02875fb..02eab3e2e8 100644 --- a/src/collections/ExposedItems.zig +++ b/src/collections/ExposedItems.zig @@ -121,13 +121,17 @@ pub const ExposedItems = struct { } /// Deserialize this Serialized struct into an ExposedItems - pub fn deserialize(self: *Serialized, offset: i64) *ExposedItems { - // Note: Serialized may be smaller than the runtime struct. - // We deserialize by overwriting the Serialized memory with the runtime struct. + pub noinline fn deserialize(self: *Serialized, offset: i64) *ExposedItems { + // CRITICAL: Deserialize nested struct BEFORE casting and writing to exposed_items. + // Since exposed_items aliases self (they point to the same memory), we must complete + // all reads before any writes to avoid corruption in Release mode. + const deserialized_items = self.items.deserialize(offset).*; + + // Now we can cast and write to the destination const exposed_items = @as(*ExposedItems, @ptrFromInt(@intFromPtr(self))); exposed_items.* = ExposedItems{ - .items = self.items.deserialize(offset).*, + .items = deserialized_items, }; return exposed_items; diff --git a/src/collections/SortedArrayBuilder.zig b/src/collections/SortedArrayBuilder.zig index f0f8f89c40..d510d9765c 100644 --- a/src/collections/SortedArrayBuilder.zig +++ b/src/collections/SortedArrayBuilder.zig @@ -299,33 +299,35 @@ pub fn SortedArrayBuilder(comptime K: type, comptime V: type) type { } /// Deserialize this Serialized struct into a SortedArrayBuilder - pub fn deserialize(self: *Serialized, offset: i64) *SortedArrayBuilder(K, V) { - // Note: Serialized may be smaller than the runtime struct because: - // - Uses i64 offsets instead of usize pointers - // - Omits runtime-only fields like allocators - // - May have different alignment/padding requirements - // We deserialize by overwriting the Serialized memory with the runtime struct. - - // Overwrite ourself with the deserialized version, and return our pointer after casting it to Self. + pub noinline fn deserialize(self: *Serialized, offset: i64) *SortedArrayBuilder(K, V) { + // CRITICAL: Read ALL fields from self BEFORE casting and writing to builder. + // Since builder aliases self (they point to the same memory), we must complete + // all reads before any writes to avoid corruption in Release mode. + const serialized_entries_offset = self.entries_offset; + const serialized_entries_len = self.entries_len; + const serialized_entries_capacity = self.entries_capacity; + const serialized_sorted = self.sorted; + + // Now we can cast and write to the destination const builder = @as(*SortedArrayBuilder(K, V), @ptrFromInt(@intFromPtr(self))); // Handle empty array case - if (self.entries_len == 0) { + if (serialized_entries_len == 0) { builder.* = SortedArrayBuilder(K, V){ .entries = .{}, - .sorted = self.sorted, + .sorted = serialized_sorted, }; } else { // Apply the offset to convert from serialized offset to actual pointer - const entries_ptr_usize: usize = @intCast(self.entries_offset + offset); + const entries_ptr_usize: usize = @intCast(serialized_entries_offset + offset); const entries_ptr: [*]Entry = @ptrFromInt(entries_ptr_usize); builder.* = SortedArrayBuilder(K, V){ .entries = .{ - .items = entries_ptr[0..@intCast(self.entries_len)], - .capacity = @intCast(self.entries_capacity), + .items = entries_ptr[0..@intCast(serialized_entries_len)], + .capacity = @intCast(serialized_entries_capacity), }, - .sorted = self.sorted, + .sorted = serialized_sorted, }; } diff --git a/src/collections/safe_list.zig b/src/collections/safe_list.zig index 2ed41b5544..23c15dfa7c 100644 --- a/src/collections/safe_list.zig +++ b/src/collections/safe_list.zig @@ -148,24 +148,30 @@ pub fn SafeList(comptime T: type) type { } /// Deserialize this Serialized struct into a SafeList - pub fn deserialize(self: *Serialized, offset: i64) *SafeList(T) { - // Note: Serialized may be smaller than the runtime struct. - // We deserialize by overwriting the Serialized memory with the runtime struct. + pub noinline fn deserialize(self: *Serialized, offset: i64) *SafeList(T) { + // CRITICAL: Read ALL fields from self BEFORE casting and writing to safe_list. + // Since safe_list aliases self (they point to the same memory), we must complete + // all reads before any writes to avoid corruption in Release mode. + const serialized_offset = self.offset; + const serialized_len = self.len; + const serialized_capacity = self.capacity; + + // Now we can cast and write to the destination const safe_list = @as(*SafeList(T), @ptrFromInt(@intFromPtr(self))); // Handle empty list case - if (self.len == 0) { + if (serialized_len == 0) { safe_list.* = SafeList(T){ .items = .{}, }; } else { // Apply the offset to convert from serialized offset to actual pointer - const items_ptr: [*]T = @ptrFromInt(@as(usize, @intCast(self.offset + offset))); + const items_ptr: [*]T = @ptrFromInt(@as(usize, @intCast(serialized_offset + offset))); safe_list.* = SafeList(T){ .items = .{ - .items = items_ptr[0..@intCast(self.len)], - .capacity = @intCast(self.capacity), + .items = items_ptr[0..@intCast(serialized_len)], + .capacity = @intCast(serialized_capacity), }, }; } @@ -665,20 +671,26 @@ pub fn SafeMultiList(comptime T: type) type { } /// Deserialize this Serialized struct into a SafeMultiList - pub fn deserialize(self: *Serialized, offset: i64) *SafeMultiList(T) { - // Note: Serialized may be smaller than the runtime struct. - // We deserialize by overwriting the Serialized memory with the runtime struct. + pub noinline fn deserialize(self: *Serialized, offset: i64) *SafeMultiList(T) { + // CRITICAL: Read ALL fields from self BEFORE casting and writing to multi_list. + // Since multi_list aliases self (they point to the same memory), we must complete + // all reads before any writes to avoid corruption in Release mode. + const serialized_offset = self.offset; + const serialized_len = self.len; + const serialized_capacity = self.capacity; + + // Now we can cast and write to the destination const multi_list = @as(*SafeMultiList(T), @ptrFromInt(@intFromPtr(self))); // Handle empty list case - if (self.len == 0) { + if (serialized_len == 0) { multi_list.* = SafeMultiList(T){ .items = .{}, }; } else { // We need to reconstruct the MultiArrayList from the serialized field arrays // MultiArrayList stores fields separately by type, and we serialized them in field order - const current_ptr = @as([*]u8, @ptrFromInt(@as(usize, @intCast(self.offset + offset)))); + const current_ptr = @as([*]u8, @ptrFromInt(@as(usize, @intCast(serialized_offset + offset)))); // Allocate aligned memory for the MultiArrayList bytes const bytes_ptr = @as([*]align(@alignOf(T)) u8, @ptrCast(@alignCast(current_ptr))); @@ -686,8 +698,8 @@ pub fn SafeMultiList(comptime T: type) type { multi_list.* = SafeMultiList(T){ .items = .{ .bytes = bytes_ptr, - .len = @as(usize, @intCast(self.len)), - .capacity = @as(usize, @intCast(self.capacity)), + .len = @as(usize, @intCast(serialized_len)), + .capacity = @as(usize, @intCast(serialized_capacity)), }, }; } @@ -916,7 +928,7 @@ test "SafeList edge cases serialization" { try self.list_u8.serialize(&container.list_u8, allocator, writer); } - pub fn deserialize(self: *Serialized, offset: i64) *Self { + pub noinline fn deserialize(self: *Serialized, offset: i64) *Self { const container = @as(*Self, @ptrFromInt(@intFromPtr(self))); container.* = Self{ .list_u32 = self.list_u32.deserialize(offset).*, diff --git a/src/compile/cache_module.zig b/src/compile/cache_module.zig index 3466f8e5a9..073f1cba20 100644 --- a/src/compile/cache_module.zig +++ b/src/compile/cache_module.zig @@ -95,9 +95,9 @@ pub const CacheModule = struct { // Create CompactWriter var writer = CompactWriter.init(); - // Allocate space for ModuleEnv.Serialized - const env_ptr = try writer.appendAlloc(arena_allocator, ModuleEnv); - const serialized_ptr = @as(*ModuleEnv.Serialized, @ptrCast(@alignCast(env_ptr))); + // Allocate space for ModuleEnv.Serialized and serialize into it + comptime std.debug.assert(@sizeOf(ModuleEnv.Serialized) >= @sizeOf(ModuleEnv)); + const serialized_ptr = try writer.appendAlloc(arena_allocator, ModuleEnv.Serialized); // Serialize the ModuleEnv try serialized_ptr.serialize(module_env, arena_allocator, &writer); diff --git a/src/compile/test/module_env_test.zig b/src/compile/test/module_env_test.zig index cd9dfee6f7..a0ea0aabb7 100644 --- a/src/compile/test/module_env_test.zig +++ b/src/compile/test/module_env_test.zig @@ -102,8 +102,6 @@ test "ModuleEnv.Serialized roundtrip" { .diagnostics = deserialized_ptr.diagnostics, .store = deserialized_ptr.store.deserialize(@as(i64, @intCast(@intFromPtr(buffer.ptr))), deser_alloc).*, .evaluation_order = null, - .from_int_digits_ident = common.findIdent(Ident.FROM_INT_DIGITS_METHOD_NAME) orelse unreachable, - .from_dec_digits_ident = common.findIdent(Ident.FROM_DEC_DIGITS_METHOD_NAME) orelse unreachable, .try_ident = common.findIdent("Try") orelse unreachable, .out_of_range_ident = common.findIdent("OutOfRange") orelse unreachable, .builtin_module_ident = common.findIdent("Builtin") orelse unreachable, @@ -129,9 +127,9 @@ test "ModuleEnv.Serialized roundtrip" { // Verify original data before serialization was correct // initCIRFields inserts the module name ("TestModule") into the interner, so we have 3 total: hello, world, TestModule - // ModuleEnv.init() also interns 19 well-known identifiers: from_int_digits, from_dec_digits, Try, OutOfRange, Builtin, plus, minus, times, div_by, div_trunc_by, rem_by, negate, not, is_lt, is_lte, is_gt, is_gte, is_eq, is_ne + // ModuleEnv.init() also interns 17 well-known identifiers: Try, OutOfRange, Builtin, plus, minus, times, div_by, div_trunc_by, rem_by, negate, not, is_lt, is_lte, is_gt, is_gte, is_eq, is_ne // Plus 13 numeric type identifiers: Num.U8, Num.I8, Num.U16, Num.I16, Num.U32, Num.I32, Num.U64, Num.I64, Num.U128, Num.I128, Num.F32, Num.F64, Num.Dec - try testing.expectEqual(@as(u32, 35), original.common.idents.interner.entry_count); + try testing.expectEqual(@as(u32, 33), original.common.idents.interner.entry_count); try testing.expectEqualStrings("hello", original.getIdent(hello_idx)); try testing.expectEqualStrings("world", original.getIdent(world_idx)); @@ -140,8 +138,8 @@ test "ModuleEnv.Serialized roundtrip" { try testing.expectEqual(@as(usize, 2), original.imports.imports.len()); // Should have 2 unique imports // First verify that the CommonEnv data was preserved after deserialization - // Should have same 35 identifiers as original: hello, world, TestModule + 19 well-known identifiers + 13 numeric type identifiers from ModuleEnv.init() - try testing.expectEqual(@as(u32, 35), env.common.idents.interner.entry_count); + // Should have same 33 identifiers as original: hello, world, TestModule + 17 well-known identifiers + 13 numeric type identifiers from ModuleEnv.init() + try testing.expectEqual(@as(u32, 33), env.common.idents.interner.entry_count); try testing.expectEqual(@as(usize, 1), env.common.exposed_items.count()); try testing.expectEqual(@as(?u16, 42), env.common.exposed_items.getNodeIndexById(gpa, @as(u32, @bitCast(hello_idx)))); diff --git a/src/eval/builtin_loading.zig b/src/eval/builtin_loading.zig index 36079ba82c..8d368502da 100644 --- a/src/eval/builtin_loading.zig +++ b/src/eval/builtin_loading.zig @@ -79,8 +79,6 @@ pub fn loadCompiledModule(gpa: std.mem.Allocator, bin_data: []const u8, module_n .evaluation_order = null, // Well-known identifiers for type checking - look them up in the deserialized common env // These must exist in the Builtin module which defines them - .from_int_digits_ident = common.findIdent(Ident.FROM_INT_DIGITS_METHOD_NAME) orelse unreachable, - .from_dec_digits_ident = common.findIdent(Ident.FROM_DEC_DIGITS_METHOD_NAME) orelse unreachable, .try_ident = common.findIdent("Try") orelse unreachable, .out_of_range_ident = common.findIdent("OutOfRange") orelse unreachable, .builtin_module_ident = common.findIdent("Builtin") orelse unreachable, diff --git a/src/eval/comptime_evaluator.zig b/src/eval/comptime_evaluator.zig index c862e23099..83c7d9aa51 100644 --- a/src/eval/comptime_evaluator.zig +++ b/src/eval/comptime_evaluator.zig @@ -343,35 +343,14 @@ pub const ComptimeEvaluator = struct { // Convert StackValue to CIR expression based on layout const layout = stack_value.layout; - // Get the runtime type variable from the StackValue first, or fall back to expression type - const rt_var: types_mod.Var = if (stack_value.rt_var) |sv_rt_var| - sv_rt_var - else blk: { - // Fall back to expression type variable - const ct_var = ModuleEnv.varFrom(def.expr); - break :blk self.interpreter.translateTypeVar(self.env, ct_var) catch { - return error.NotImplemented; - }; - }; + // Get the runtime type variable from the StackValue - should always be set + const rt_var = stack_value.rt_var orelse return error.NotImplemented; const resolved = self.interpreter.runtime_types.resolveVar(rt_var); // Check if it's a tag union type const is_tag_union = resolved.desc.content == .structure and resolved.desc.content.structure == .tag_union; - // Special case for Bool type: u8 scalar with value 0 or 1 - // This handles nominal Bool types that aren't properly tracked through rt_var - if (layout.tag == .scalar and layout.data.scalar.tag == .int and - layout.data.scalar.data.int == .u8) - { - const val = stack_value.asI128(); - if (val == 0 or val == 1) { - // This is a Bool value - fold it directly - try self.foldBoolScalar(expr_idx, val == 1); - return; - } - } - if (is_tag_union) { // Tag unions can be scalars (no payload) or tuples (with payload) switch (layout.tag) { diff --git a/src/eval/interpreter.zig b/src/eval/interpreter.zig index a78861709d..7ab4c63f56 100644 --- a/src/eval/interpreter.zig +++ b/src/eval/interpreter.zig @@ -827,6 +827,7 @@ pub const Interpreter = struct { else => return error.TypeMismatch, } value.is_initialized = true; + value.rt_var = rt_var; return value; }, .e_unary_minus => |unary_minus| { @@ -1363,6 +1364,7 @@ pub const Interpreter = struct { out.is_initialized = false; try out.setInt(@intCast(tag_index)); out.is_initialized = true; + out.rt_var = rt_var; return out; } return error.NotImplemented; @@ -1378,6 +1380,7 @@ pub const Interpreter = struct { tmp.is_initialized = false; try tmp.setInt(@intCast(tag_index)); } else return error.NotImplemented; + dest.rt_var = rt_var; return dest; } else if (layout_val.tag == .tuple) { // Tuple (payload, tag) - tag unions are now represented as tuples @@ -1391,6 +1394,7 @@ pub const Interpreter = struct { tmp.is_initialized = false; try tmp.setInt(@intCast(tag_index)); } else return error.NotImplemented; + dest.rt_var = rt_var; return dest; } return error.NotImplemented; @@ -3207,166 +3211,6 @@ pub const Interpreter = struct { out.is_initialized = true; return out; }, - - // Numeric parsing operations - .num_from_int_digits => { - // num.from_int_digits : List(U8) -> Try(num, [OutOfRange]) - std.debug.assert(args.len == 1); // expects 1 argument: List(U8) - - const result_rt_var = return_rt_var orelse { - self.triggerCrash("num_from_int_digits requires return type info", false, roc_ops); - return error.Crash; - }; - - // Get the result layout (Try tag union) - const result_layout = try self.getRuntimeLayout(result_rt_var); - - // Extract base-256 digits from List(U8) - const list_arg = args[0]; - std.debug.assert(list_arg.ptr != null); - const roc_list: *const builtins.list.RocList = @ptrCast(@alignCast(list_arg.ptr.?)); - const list_len = roc_list.len(); - const digits_ptr = roc_list.elements(u8); - const digits: []const u8 = if (digits_ptr) |ptr| ptr[0..list_len] else &[_]u8{}; - - // Convert base-256 digits to u128 (max intermediate precision) - var value: u128 = 0; - var overflow = false; - for (digits) |digit| { - const new_value = @mulWithOverflow(value, 256); - if (new_value[1] != 0) { - overflow = true; - break; - } - const add_result = @addWithOverflow(new_value[0], digit); - if (add_result[1] != 0) { - overflow = true; - break; - } - value = add_result[0]; - } - - // Resolve the Try type to get Ok's payload type (the numeric type) - const resolved = self.resolveBaseVar(result_rt_var); - if (resolved.desc.content != .structure or resolved.desc.content.structure != .tag_union) { - self.triggerCrash("num_from_int_digits: expected Try tag union result type", false, roc_ops); - return error.Crash; - } - - // Find tag indices for Ok and Err - var tag_list = std.array_list.AlignedManaged(types.Tag, null).init(self.allocator); - defer tag_list.deinit(); - try self.appendUnionTags(result_rt_var, &tag_list); - - var ok_index: ?usize = null; - var err_index: ?usize = null; - var ok_payload_var: ?types.Var = null; - - for (tag_list.items, 0..) |tag_info, i| { - const tag_name = self.env.getIdent(tag_info.name); - if (std.mem.eql(u8, tag_name, "Ok")) { - ok_index = i; - const arg_vars = self.runtime_types.sliceVars(tag_info.args); - if (arg_vars.len >= 1) { - ok_payload_var = arg_vars[0]; - } - } else if (std.mem.eql(u8, tag_name, "Err")) { - err_index = i; - } - } - - // Determine target numeric type and check range - var in_range = !overflow; - if (in_range and ok_payload_var != null) { - const num_layout = try self.getRuntimeLayout(ok_payload_var.?); - if (num_layout.tag == .scalar and num_layout.data.scalar.tag == .int) { - // Check if value fits in target integer type - const int_type = num_layout.data.scalar.data.int; - in_range = switch (int_type) { - .u8 => value <= std.math.maxInt(u8), - .i8 => value <= std.math.maxInt(i8), - .u16 => value <= std.math.maxInt(u16), - .i16 => value <= std.math.maxInt(i16), - .u32 => value <= std.math.maxInt(u32), - .i32 => value <= std.math.maxInt(i32), - .u64 => value <= std.math.maxInt(u64), - .i64 => value <= std.math.maxInt(i64), - .u128, .i128 => true, // u128 fits, i128 needs sign check - }; - } - } - - // Construct the result tag union - if (result_layout.tag == .scalar) { - // Simple tag with no payload (shouldn't happen for Try) - var out = try self.pushRaw(result_layout, 0); - out.is_initialized = false; - const tag_idx: usize = if (in_range) ok_index orelse 0 else err_index orelse 1; - try out.setInt(@intCast(tag_idx)); - out.is_initialized = true; - return out; - } else if (result_layout.tag == .record) { - // Record { tag, payload } - var dest = try self.pushRaw(result_layout, 0); - var acc = try dest.asRecord(&self.runtime_layout_store); - const tag_field_idx = acc.findFieldIndex(self.env, "tag") orelse return error.NotImplemented; - const payload_field_idx = acc.findFieldIndex(self.env, "payload") orelse return error.NotImplemented; - - // Write tag discriminant - const tag_field = try acc.getFieldByIndex(tag_field_idx); - if (tag_field.layout.tag == .scalar and tag_field.layout.data.scalar.tag == .int) { - var tmp = tag_field; - tmp.is_initialized = false; - const tag_idx: usize = if (in_range) ok_index orelse 0 else err_index orelse 1; - try tmp.setInt(@intCast(tag_idx)); - } else return error.NotImplemented; - - // Clear payload area - const payload_field = try acc.getFieldByIndex(payload_field_idx); - if (payload_field.ptr) |payload_ptr| { - const payload_bytes_len = self.runtime_layout_store.layoutSize(payload_field.layout); - if (payload_bytes_len > 0) { - const bytes = @as([*]u8, @ptrCast(payload_ptr))[0..payload_bytes_len]; - @memset(bytes, 0); - } - } - - // Write payload - if (in_range and ok_payload_var != null) { - // Write the numeric value as Ok payload - const num_layout = try self.getRuntimeLayout(ok_payload_var.?); - if (payload_field.ptr) |payload_ptr| { - if (num_layout.tag == .scalar and num_layout.data.scalar.tag == .int) { - // Write integer value directly to payload - const int_type = num_layout.data.scalar.data.int; - switch (int_type) { - .u8 => @as(*u8, @ptrCast(@alignCast(payload_ptr))).* = @intCast(value), - .i8 => @as(*i8, @ptrCast(@alignCast(payload_ptr))).* = @intCast(value), - .u16 => @as(*u16, @ptrCast(@alignCast(payload_ptr))).* = @intCast(value), - .i16 => @as(*i16, @ptrCast(@alignCast(payload_ptr))).* = @intCast(value), - .u32 => @as(*u32, @ptrCast(@alignCast(payload_ptr))).* = @intCast(value), - .i32 => @as(*i32, @ptrCast(@alignCast(payload_ptr))).* = @intCast(value), - .u64 => @as(*u64, @ptrCast(@alignCast(payload_ptr))).* = @intCast(value), - .i64 => @as(*i64, @ptrCast(@alignCast(payload_ptr))).* = @intCast(value), - .u128 => @as(*u128, @ptrCast(@alignCast(payload_ptr))).* = value, - .i128 => @as(*i128, @ptrCast(@alignCast(payload_ptr))).* = @intCast(value), - } - } - } - } - // For Err case, payload is OutOfRange which is a zero-arg tag (already zeroed) - - return dest; - } - - self.triggerCrash("num_from_int_digits: unsupported result layout", false, roc_ops); - return error.Crash; - }, - .num_from_dec_digits => { - // num.from_dec_digits : (List(U8), List(U8)) -> Try(num, [OutOfRange]) - self.triggerCrash("num_from_dec_digits not yet implemented", false, roc_ops); - return error.Crash; - }, .num_from_numeral => { // num.from_numeral : Numeral -> Try(num, [InvalidNumeral(Str)]) // Numeral is { is_negative: Bool, digits_before_pt: List(U8), digits_after_pt: List(U8) } @@ -3461,31 +3305,25 @@ pub const Interpreter = struct { return error.Crash; } - // Find tag indices for Ok and Err + // Get tag info - tags are sorted alphabetically, so for a 2-tag success/failure union: + // Index 0 = failure (alphabetically first), Index 1 = success (alphabetically second) var tag_list = std.array_list.AlignedManaged(types.Tag, null).init(self.allocator); defer tag_list.deinit(); try self.appendUnionTags(result_rt_var, &tag_list); - var ok_index: ?usize = null; - var err_index: ?usize = null; - var ok_payload_var: ?types.Var = null; - var err_payload_var: ?types.Var = null; - - for (tag_list.items, 0..) |tag_info, i| { - // Use runtime_layout_store.env for tag names since appendUnionTags uses runtime types - const tag_name = self.runtime_layout_store.env.getIdent(tag_info.name); - if (std.mem.eql(u8, tag_name, "Ok")) { - ok_index = i; - const arg_vars = self.runtime_types.sliceVars(tag_info.args); - if (arg_vars.len >= 1) { - ok_payload_var = arg_vars[0]; - } - } else if (std.mem.eql(u8, tag_name, "Err")) { - err_index = i; - const arg_vars = self.runtime_types.sliceVars(tag_info.args); - if (arg_vars.len >= 1) { - err_payload_var = arg_vars[0]; - } + // Get payload types for success (index 1) and failure (index 0) tags + var success_payload_var: ?types.Var = null; + var failure_payload_var: ?types.Var = null; + if (tag_list.items.len > 1) { + const success_tag = tag_list.items[1]; + const success_args = self.runtime_types.sliceVars(success_tag.args); + if (success_args.len >= 1) { + success_payload_var = success_args[0]; + } + const failure_tag = tag_list.items[0]; + const failure_args = self.runtime_types.sliceVars(failure_tag.args); + if (failure_args.len >= 1) { + failure_payload_var = failure_args[0]; } } @@ -3499,8 +3337,8 @@ pub const Interpreter = struct { var min_value_str: []const u8 = ""; var max_value_str: []const u8 = ""; - // Use the explicit target type if provided, otherwise fall back to ok_payload_var - const target_type_var = self.num_literal_target_type orelse ok_payload_var; + // Use the explicit target type if provided, otherwise fall back to success_payload_var + const target_type_var = self.num_literal_target_type orelse success_payload_var; if (in_range and target_type_var != null) { // Use the target type var directly - getRuntimeLayout handles nominal types properly @@ -3626,7 +3464,7 @@ pub const Interpreter = struct { // Simple tag with no payload var out = try self.pushRaw(result_layout, 0); out.is_initialized = false; - const tag_idx: usize = if (in_range) ok_index orelse 0 else err_index orelse 1; + const tag_idx: usize = if (in_range) 1 else 0; try out.setInt(@intCast(tag_idx)); out.is_initialized = true; return out; @@ -3643,7 +3481,7 @@ pub const Interpreter = struct { if (tag_field.layout.tag == .scalar and tag_field.layout.data.scalar.tag == .int) { var tmp = tag_field; tmp.is_initialized = false; - const tag_idx: usize = if (in_range) ok_index orelse 0 else err_index orelse 1; + const tag_idx: usize = if (in_range) 1 else 0; try tmp.setInt(@intCast(tag_idx)); } else return error.NotImplemented; @@ -3658,8 +3496,8 @@ pub const Interpreter = struct { } // Write payload for Ok case - if (in_range and ok_payload_var != null) { - const num_layout = try self.getRuntimeLayout(ok_payload_var.?); + if (in_range and success_payload_var != null) { + const num_layout = try self.getRuntimeLayout(success_payload_var.?); if (payload_field.ptr) |payload_ptr| { if (num_layout.tag == .scalar and num_layout.data.scalar.tag == .int) { const int_type = num_layout.data.scalar.data.int; @@ -3748,7 +3586,7 @@ pub const Interpreter = struct { } } } - } else if (!in_range and err_payload_var != null) { + } else if (!in_range and failure_payload_var != null) { // For Err case, construct InvalidNumeral(Str) with descriptive message // Format the number that was rejected var num_str_buf: [128]u8 = undefined; @@ -3808,7 +3646,7 @@ pub const Interpreter = struct { if (error_msg) |msg| { // Get the Err payload layout (which is [InvalidNumeral(Str)]) - const err_payload_layout = try self.getRuntimeLayout(err_payload_var.?); + const err_payload_layout = try self.getRuntimeLayout(failure_payload_var.?); const payload_field_size = self.runtime_layout_store.layoutSize(payload_field.layout); // Check if payload area has enough space for RocStr (24 bytes on 64-bit) @@ -3878,7 +3716,7 @@ pub const Interpreter = struct { if (tag_field.layout.tag == .scalar and tag_field.layout.data.scalar.tag == .int) { var tmp = tag_field; tmp.is_initialized = false; - const tag_idx: usize = if (in_range) ok_index orelse 0 else err_index orelse 1; + const tag_idx: usize = if (in_range) 1 else 0; try tmp.setInt(@intCast(tag_idx)); } else return error.NotImplemented; @@ -3893,8 +3731,8 @@ pub const Interpreter = struct { } // Write payload for Ok case - if (in_range and ok_payload_var != null) { - const num_layout = try self.getRuntimeLayout(ok_payload_var.?); + if (in_range and success_payload_var != null) { + const num_layout = try self.getRuntimeLayout(success_payload_var.?); if (payload_field.ptr) |payload_ptr| { if (num_layout.tag == .scalar and num_layout.data.scalar.tag == .int) { const int_type = num_layout.data.scalar.data.int; @@ -3971,7 +3809,7 @@ pub const Interpreter = struct { } } } - } else if (!in_range and err_payload_var != null) { + } else if (!in_range and failure_payload_var != null) { // For Err case, construct InvalidNumeral(Str) with descriptive message var num_str_buf: [128]u8 = undefined; const num_str = blk: { @@ -5636,8 +5474,9 @@ pub const Interpreter = struct { if (lambda_expr == .e_low_level_lambda) { const low_level = lambda_expr.e_low_level_lambda; // Dispatch to actual low-level builtin implementation - // Binary ops don't need return type info (not num_from_int_digits etc) - return try self.callLowLevelBuiltin(low_level.op, &args, roc_ops, null); + var ll_result = try self.callLowLevelBuiltin(low_level.op, &args, roc_ops, null); + ll_result.rt_var = lhs_rt_var; + return ll_result; } // Bind parameters @@ -5651,7 +5490,7 @@ pub const Interpreter = struct { } // Evaluate the method body - const result = try self.evalExprMinimal(closure_header.body_idx, roc_ops, null); + var result = try self.evalExprMinimal(closure_header.body_idx, roc_ops, null); // Clean up bindings var k = params.len; @@ -5660,6 +5499,8 @@ pub const Interpreter = struct { _ = self.bindings.pop(); } + // Set rt_var on result for constant folding (binary ops return same type as lhs) + result.rt_var = lhs_rt_var; return result; } @@ -5744,8 +5585,9 @@ pub const Interpreter = struct { if (lambda_expr == .e_low_level_lambda) { const low_level = lambda_expr.e_low_level_lambda; // Dispatch to actual low-level builtin implementation - // Binary ops don't need return type info (not num_from_int_digits etc) - return try self.callLowLevelBuiltin(low_level.op, &args, roc_ops, null); + var ll_result = try self.callLowLevelBuiltin(low_level.op, &args, roc_ops, null); + ll_result.rt_var = operand_rt_var; + return ll_result; } // Bind parameters @@ -5759,7 +5601,7 @@ pub const Interpreter = struct { } // Evaluate the method body - const result = try self.evalExprMinimal(closure_header.body_idx, roc_ops, null); + var result = try self.evalExprMinimal(closure_header.body_idx, roc_ops, null); // Clean up bindings var k = params.len; @@ -5768,6 +5610,8 @@ pub const Interpreter = struct { _ = self.bindings.pop(); } + // Set rt_var on result for constant folding (unary ops return same type as operand) + result.rt_var = operand_rt_var; return result; } diff --git a/src/ipc/platform.zig b/src/ipc/platform.zig index 85ee49987d..ee067b8340 100644 --- a/src/ipc/platform.zig +++ b/src/ipc/platform.zig @@ -84,6 +84,8 @@ pub const windows = if (is_windows) struct { /// POSIX shared memory functions pub const posix = if (!is_windows) struct { + // NOTE: mmap returns MAP_FAILED ((void*)-1) on error, NOT NULL! + // We use usize to properly detect this sentinel value. pub extern "c" fn mmap( addr: ?*anyopaque, len: usize, @@ -91,7 +93,10 @@ pub const posix = if (!is_windows) struct { flags: c_int, fd: c_int, offset: std.c.off_t, - ) ?*anyopaque; + ) usize; + + /// MAP_FAILED is (void*)-1, which is maxInt(usize) + pub const MAP_FAILED: usize = std.math.maxInt(usize); pub extern "c" fn munmap(addr: *anyopaque, len: usize) c_int; pub extern "c" fn close(fd: c_int) c_int; @@ -303,18 +308,20 @@ pub fn mapMemory(handle: Handle, size: usize, base_addr: ?*anyopaque) SharedMemo return ptr.?; }, .linux, .macos, .freebsd, .openbsd, .netbsd => { - const ptr = posix.mmap( + const result = posix.mmap( base_addr, size, posix.PROT_READ | posix.PROT_WRITE, posix.MAP_SHARED, handle, 0, - ) orelse { - std.log.err("POSIX: Failed to map shared memory (size: {})", .{size}); + ); + // Check for MAP_FAILED (-1), NOT null! + if (result == posix.MAP_FAILED) { + std.log.err("POSIX: Failed to map shared memory (size: {}, fd: {})", .{ size, handle }); return error.MmapFailed; - }; - return ptr; + } + return @ptrFromInt(result); }, else => return error.UnsupportedPlatform, } diff --git a/src/playground_wasm/main.zig b/src/playground_wasm/main.zig index df0934b047..fb29249627 100644 --- a/src/playground_wasm/main.zig +++ b/src/playground_wasm/main.zig @@ -961,8 +961,6 @@ fn compileSource(source: []const u8) !CompilerStageData { .diagnostics = serialized_ptr.diagnostics, .store = serialized_ptr.store.deserialize(@as(i64, @intCast(base_ptr)), gpa).*, .evaluation_order = null, - .from_int_digits_ident = common.findIdent(base.Ident.FROM_INT_DIGITS_METHOD_NAME) orelse unreachable, - .from_dec_digits_ident = common.findIdent(base.Ident.FROM_DEC_DIGITS_METHOD_NAME) orelse unreachable, .try_ident = common.findIdent("Try") orelse unreachable, .out_of_range_ident = common.findIdent("OutOfRange") orelse unreachable, .builtin_module_ident = common.findIdent("Builtin") orelse unreachable, diff --git a/src/repl/repl_test.zig b/src/repl/repl_test.zig index 50df0daf83..28409d6a9a 100644 --- a/src/repl/repl_test.zig +++ b/src/repl/repl_test.zig @@ -88,8 +88,6 @@ fn loadCompiledModule(gpa: std.mem.Allocator, bin_data: []const u8, module_name: .diagnostics = serialized_ptr.diagnostics, .store = serialized_ptr.store.deserialize(@as(i64, @intCast(base_ptr)), gpa).*, .evaluation_order = null, - .from_int_digits_ident = common.findIdent(base.Ident.FROM_INT_DIGITS_METHOD_NAME) orelse unreachable, - .from_dec_digits_ident = common.findIdent(base.Ident.FROM_DEC_DIGITS_METHOD_NAME) orelse unreachable, .try_ident = common.findIdent("Try") orelse unreachable, .out_of_range_ident = common.findIdent("OutOfRange") orelse unreachable, .builtin_module_ident = common.findIdent("Builtin") orelse unreachable, diff --git a/src/types/store.zig b/src/types/store.zig index 5647cb47c5..3b84f5a981 100644 --- a/src/types/store.zig +++ b/src/types/store.zig @@ -751,21 +751,28 @@ pub const Store = struct { } /// Deserialize this Serialized struct into a Store - pub fn deserialize(self: *Serialized, offset: i64, gpa: Allocator) *Store { - // Note: Serialized may be smaller than the runtime struct because: - // - Uses i64 offsets instead of usize pointers - // - Omits runtime-only fields like the allocator - // We deserialize by overwriting the Serialized memory with the runtime struct. + pub noinline fn deserialize(self: *Serialized, offset: i64, gpa: Allocator) *Store { + // CRITICAL: Deserialize ALL nested structs BEFORE casting and writing to store. + // Since store aliases self (they point to the same memory), we must complete + // all reads before any writes to avoid corruption in Release mode. + const deserialized_slots = self.slots.deserialize(offset).*; + const deserialized_descs = self.descs.deserialize(offset).*; + const deserialized_vars = self.vars.deserialize(offset).*; + const deserialized_record_fields = self.record_fields.deserialize(offset).*; + const deserialized_tags = self.tags.deserialize(offset).*; + const deserialized_static_dispatch_constraints = self.static_dispatch_constraints.deserialize(offset).*; + + // Now we can cast and write to the destination const store = @as(*Store, @ptrFromInt(@intFromPtr(self))); store.* = Store{ .gpa = gpa, - .slots = self.slots.deserialize(offset).*, - .descs = self.descs.deserialize(offset).*, - .vars = self.vars.deserialize(offset).*, - .record_fields = self.record_fields.deserialize(offset).*, - .tags = self.tags.deserialize(offset).*, - .static_dispatch_constraints = self.static_dispatch_constraints.deserialize(offset).*, + .slots = deserialized_slots, + .descs = deserialized_descs, + .vars = deserialized_vars, + .record_fields = deserialized_record_fields, + .tags = deserialized_tags, + .static_dispatch_constraints = deserialized_static_dispatch_constraints, }; return store; @@ -938,13 +945,15 @@ const SlotStore = struct { } /// Deserialize this Serialized struct into a SlotStore - pub fn deserialize(self: *Serialized, offset: i64) *SlotStore { - // Note: Serialized may be smaller than the runtime struct. - // We deserialize by overwriting the Serialized memory with the runtime struct. + pub noinline fn deserialize(self: *Serialized, offset: i64) *SlotStore { + // CRITICAL: Deserialize nested struct BEFORE casting and writing to slot_store + // to avoid aliasing issues in Release mode. + const deserialized_backing = self.backing.deserialize(offset).*; + const slot_store = @as(*SlotStore, @ptrFromInt(@intFromPtr(self))); slot_store.* = SlotStore{ - .backing = self.backing.deserialize(offset).*, + .backing = deserialized_backing, }; return slot_store; @@ -1041,13 +1050,15 @@ const DescStore = struct { } /// Deserialize this Serialized struct into a DescStore - pub fn deserialize(self: *Serialized, offset: i64) *DescStore { - // Note: Serialized may be smaller than the runtime struct. - // We deserialize by overwriting the Serialized memory with the runtime struct. + pub noinline fn deserialize(self: *Serialized, offset: i64) *DescStore { + // CRITICAL: Deserialize nested struct BEFORE casting and writing to desc_store + // to avoid aliasing issues in Release mode. + const deserialized_backing = self.backing.deserialize(offset).*; + const desc_store = @as(*DescStore, @ptrFromInt(@intFromPtr(self))); desc_store.* = DescStore{ - .backing = self.backing.deserialize(offset).*, + .backing = deserialized_backing, }; return desc_store; diff --git a/test/serialization_size_check.zig b/test/serialization_size_check.zig index 4c12caa099..487e4b34d0 100644 --- a/test/serialization_size_check.zig +++ b/test/serialization_size_check.zig @@ -14,6 +14,7 @@ const std = @import("std"); const builtin = @import("builtin"); const collections = @import("collections"); const can = @import("can"); +const base = @import("base"); const ModuleEnv = can.ModuleEnv; const NodeStore = can.CIR.NodeStore; @@ -31,7 +32,7 @@ const expected_safelist_u8_size = 24; const expected_safelist_u32_size = 24; const expected_safemultilist_teststruct_size = 24; const expected_safemultilist_node_size = 24; -const expected_moduleenv_size = 712; // Platform-independent size +const expected_moduleenv_size = 704; // Platform-independent size const expected_nodestore_size = 96; // Platform-independent size // Compile-time assertions - build will fail if sizes don't match expected values @@ -97,3 +98,17 @@ pub fn main() void { std.debug.print("✓ Serialization size check passed - all types have correct platform-independent sizes\n", .{}); } } + +// Verify Serialized types are at least as large as their runtime counterparts, +// which is required for safe in-place deserialization. +test "Serialized struct sizes are sufficient for in-place deserialization" { + const CommonEnv = base.CommonEnv; + const Ident = base.Ident; + const SmallStringInterner = base.SmallStringInterner; + + try std.testing.expect(@sizeOf(CommonEnv.Serialized) >= @sizeOf(CommonEnv)); + try std.testing.expect(@sizeOf(Ident.Store.Serialized) >= @sizeOf(Ident.Store)); + try std.testing.expect(@sizeOf(SmallStringInterner.Serialized) >= @sizeOf(SmallStringInterner)); + try std.testing.expect(@sizeOf(SafeList(u8).Serialized) >= @sizeOf(SafeList(u8))); + try std.testing.expect(@sizeOf(SafeList(SmallStringInterner.Idx).Serialized) >= @sizeOf(SafeList(SmallStringInterner.Idx))); +}