diff --git a/src/build/builtin_compiler/main.zig b/src/build/builtin_compiler/main.zig index 64041ba0701..f436da93cb7 100644 --- a/src/build/builtin_compiler/main.zig +++ b/src/build/builtin_compiler/main.zig @@ -1824,16 +1824,16 @@ fn findTypeDeclaration(env: *const ModuleEnv, type_name: []const u8) !CIR.Statem const all_stmts = env.store.sliceStatements(env.all_statements); for (all_stmts) |stmt_idx| { const stmt = env.store.getStatement(stmt_idx); - switch (stmt) { - .s_nominal_decl => |decl| { - const header = env.store.getTypeHeader(decl.header); - const ident_idx = header.name; - const ident_text = env.getIdentText(ident_idx); - if (std.mem.eql(u8, ident_text, qualified_name)) { - return stmt_idx; - } - }, + const header_idx = switch (stmt) { + .s_nominal_decl => |decl| decl.header, + .s_alias_decl => |alias| alias.header, else => continue, + }; + const header = env.store.getTypeHeader(header_idx); + const ident_idx = header.name; + const ident_text = env.getIdentText(ident_idx); + if (std.mem.eql(u8, ident_text, qualified_name)) { + return stmt_idx; } } diff --git a/src/build/roc/Builtin.roc b/src/build/roc/Builtin.roc index f7a2d7bbd80..b6119ae376e 100644 --- a/src/build/roc/Builtin.roc +++ b/src/build/roc/Builtin.roc @@ -46,6 +46,13 @@ Builtin :: [].{ encode = |self, format| { format.encode_str(self) } + + decode : src, fmt -> (Try(Str, err), src) + where [fmt.decode_str : fmt, src -> (Try(Str, err), src)] + decode = |source, format| { + Fmt : fmt + Fmt.decode_str(format, source) + } } List(_item) :: [ProvidedByCompiler].{ @@ -269,6 +276,18 @@ Builtin :: [].{ format.encode_list(self, |elem, f| elem.encode(f)) } + # Decode a list using a format that provides decode_list + decode : src, fmt -> (Try(List(item), err), src) + where [ + fmt.decode_list : fmt, src, (src, fmt -> (Try(item, err), src)) -> (Try(List(item), err), src), + item.decode : src, fmt -> (Try(item, err), src), + ] + decode = |source, format| { + Fmt : fmt + Item : item + Fmt.decode_list(format, source, |s, f| Item.decode(s, f)) + } + } Bool := [False, True].{ @@ -286,6 +305,13 @@ Builtin :: [].{ encode = |self, format| { format.encode_bool(self) } + + decode : src, fmt -> (Try(Bool, err), src) + where [fmt.decode_bool : fmt, src -> (Try(Bool, err), src)] + decode = |source, format| { + Fmt : fmt + Fmt.decode_bool(format, source) + } } Box(item) :: [ProvidedByCompiler].{ @@ -465,6 +491,13 @@ Builtin :: [].{ encode = |self, format| { format.encode_u8(self) } + + decode : src, fmt -> (Try(U8, err), src) + where [fmt.decode_u8 : fmt, src -> (Try(U8, err), src)] + decode = |source, format| { + Fmt : fmt + Fmt.decode_u8(format, source) + } } I8 :: [].{ @@ -541,6 +574,13 @@ Builtin :: [].{ encode = |self, format| { format.encode_i8(self) } + + decode : src, fmt -> (Try(I8, err), src) + where [fmt.decode_i8 : fmt, src -> (Try(I8, err), src)] + decode = |source, format| { + Fmt : fmt + Fmt.decode_i8(format, source) + } } U16 :: [].{ @@ -611,6 +651,13 @@ Builtin :: [].{ encode = |self, format| { format.encode_u16(self) } + + decode : src, fmt -> (Try(U16, err), src) + where [fmt.decode_u16 : fmt, src -> (Try(U16, err), src)] + decode = |source, format| { + Fmt : fmt + Fmt.decode_u16(format, source) + } } I16 :: [].{ @@ -688,6 +735,13 @@ Builtin :: [].{ encode = |self, format| { format.encode_i16(self) } + + decode : src, fmt -> (Try(I16, err), src) + where [fmt.decode_i16 : fmt, src -> (Try(I16, err), src)] + decode = |source, format| { + Fmt : fmt + Fmt.decode_i16(format, source) + } } U32 :: [].{ @@ -760,6 +814,13 @@ Builtin :: [].{ encode = |self, format| { format.encode_u32(self) } + + decode : src, fmt -> (Try(U32, err), src) + where [fmt.decode_u32 : fmt, src -> (Try(U32, err), src)] + decode = |source, format| { + Fmt : fmt + Fmt.decode_u32(format, source) + } } I32 :: [].{ @@ -838,6 +899,14 @@ Builtin :: [].{ encode = |self, format| { format.encode_i32(self) } + + # Decode an I32 using a format that provides decode_i32 + decode : src, fmt -> (Try(I32, err), src) + where [fmt.decode_i32 : fmt, src -> (Try(I32, err), src)] + decode = |source, format| { + Fmt : fmt + Fmt.decode_i32(format, source) + } } U64 :: [].{ @@ -912,6 +981,13 @@ Builtin :: [].{ encode = |self, format| { format.encode_u64(self) } + + decode : src, fmt -> (Try(U64, err), src) + where [fmt.decode_u64 : fmt, src -> (Try(U64, err), src)] + decode = |source, format| { + Fmt : fmt + Fmt.decode_u64(format, source) + } } I64 :: [].{ @@ -991,6 +1067,13 @@ Builtin :: [].{ encode = |self, format| { format.encode_i64(self) } + + decode : src, fmt -> (Try(I64, err), src) + where [fmt.decode_i64 : fmt, src -> (Try(I64, err), src)] + decode = |source, format| { + Fmt : fmt + Fmt.decode_i64(format, source) + } } U128 :: [].{ @@ -1069,6 +1152,13 @@ Builtin :: [].{ encode = |self, format| { format.encode_u128(self) } + + decode : src, fmt -> (Try(U128, err), src) + where [fmt.decode_u128 : fmt, src -> (Try(U128, err), src)] + decode = |source, format| { + Fmt : fmt + Fmt.decode_u128(format, source) + } } I128 :: [].{ @@ -1151,6 +1241,13 @@ Builtin :: [].{ encode = |self, format| { format.encode_i128(self) } + + decode : src, fmt -> (Try(I128, err), src) + where [fmt.decode_i128 : fmt, src -> (Try(I128, err), src)] + decode = |source, format| { + Fmt : fmt + Fmt.decode_i128(format, source) + } } Dec :: [].{ @@ -1229,6 +1326,13 @@ Builtin :: [].{ encode = |self, format| { format.encode_dec(self) } + + decode : src, fmt -> (Try(Dec, err), src) + where [fmt.decode_dec : fmt, src -> (Try(Dec, err), src)] + decode = |source, format| { + Fmt : fmt + Fmt.decode_dec(format, source) + } } F32 :: [].{ @@ -1292,6 +1396,13 @@ Builtin :: [].{ encode = |self, format| { format.encode_f32(self) } + + decode : src, fmt -> (Try(F32, err), src) + where [fmt.decode_f32 : fmt, src -> (Try(F32, err), src)] + decode = |source, format| { + Fmt : fmt + Fmt.decode_f32(format, source) + } } F64 :: [].{ @@ -1365,8 +1476,16 @@ Builtin :: [].{ encode = |self, format| { format.encode_f64(self) } + + decode : src, fmt -> (Try(F64, err), src) + where [fmt.decode_f64 : fmt, src -> (Try(F64, err), src)] + decode = |source, format| { + Fmt : fmt + Fmt.decode_f64(format, source) + } } } + } range_to = |var $current, end| { diff --git a/src/canonicalize/Can.zig b/src/canonicalize/Can.zig index c5a27bfa3b9..56f0d80ec7c 100644 --- a/src/canonicalize/Can.zig +++ b/src/canonicalize/Can.zig @@ -4327,8 +4327,12 @@ pub fn canonicalizeExpr( if (self.module_envs.?.get(module_alias)) |auto_imported_type_env| { const module_env = auto_imported_type_env.env; - // Get the qualified name of the method (e.g., "Str.is_empty") - const qualified_text = self.env.getIdent(type_qualified_idx); + // Build the FULLY qualified method name using qualified_type_ident + // e.g., for I32.decode: "Builtin.Num.I32" + "decode" -> "Builtin.Num.I32.decode" + // e.g., for Str.concat: "Builtin.Str" + "concat" -> "Builtin.Str.concat" + const qualified_type_text = self.env.getIdent(auto_imported_type_env.qualified_type_ident); + const fully_qualified_idx = try self.env.insertQualifiedIdent(qualified_type_text, field_text); + const qualified_text = self.env.getIdent(fully_qualified_idx); // Try to find the method in the Builtin module's exposed items if (module_env.common.findIdent(qualified_text)) |qname_ident| { @@ -11705,9 +11709,22 @@ fn tryModuleQualifiedLookup(self: *Self, field_access: AST.BinOp) std.mem.Alloca const left_ident = left_expr.ident; const module_alias = self.parse_ir.tokens.resolveIdentifier(left_ident.token) orelse return null; - // Check if this is a module alias - const module_info = self.scopeLookupModule(module_alias) orelse return null; - const module_name = module_info.module_name; + // Check if this is a module alias OR an auto-imported type + // Auto-imported types (like I32, Bool, Str) can have static methods called on them + const module_info = self.scopeLookupModule(module_alias); + const module_name = if (module_info) |info| + info.module_name + else blk: { + // Not a module alias - check if it's an auto-imported type in module_envs + if (self.module_envs) |envs_map| { + if (envs_map.contains(module_alias)) { + // This IS an auto-imported type - use the alias as the module_name + break :blk module_alias; + } + } + // Not a module alias and not an auto-imported type + return null; + }; const module_text = self.env.getIdent(module_name); // Check if this module is imported in the current scope @@ -11761,16 +11778,18 @@ fn tryModuleQualifiedLookup(self: *Self, field_access: AST.BinOp) std.mem.Alloca if (self.module_envs) |envs_map| { if (envs_map.get(module_name)) |auto_imported_type| { if (auto_imported_type.statement_idx != null) { - // This is an imported type module (like Stdout) - // Look up the qualified method name (e.g., "Stdout.line!") in the module's exposed items + // This is an imported type module (like Stdout, I32, etc.) + // Look up the qualified method name (e.g., "Builtin.Num.I32.decode") in the module's exposed items const module_env = auto_imported_type.env; const module_name_text = module_env.module_name; const auto_import_idx = try self.getOrCreateAutoImport(module_name_text); - // Build the qualified method name: "TypeName.method_name" - const type_name_text = self.env.getIdent(module_name); + // Build the FULLY qualified method name using qualified_type_ident + // e.g., for I32.decode: "Builtin.Num.I32" + "decode" -> "Builtin.Num.I32.decode" + // e.g., for Str.concat: "Builtin.Str" + "concat" -> "Builtin.Str.concat" + const qualified_type_text = self.env.getIdent(auto_imported_type.qualified_type_ident); const method_name_text = self.env.getIdent(method_name); - const qualified_method_name = try self.env.insertQualifiedIdent(type_name_text, method_name_text); + const qualified_method_name = try self.env.insertQualifiedIdent(qualified_type_text, method_name_text); const qualified_text = self.env.getIdent(qualified_method_name); // Look up the qualified method in the module's exposed items diff --git a/src/check/Check.zig b/src/check/Check.zig index 2d2fcf27261..a09abf3164d 100644 --- a/src/check/Check.zig +++ b/src/check/Check.zig @@ -5651,7 +5651,8 @@ fn checkDeferredStaticDispatchConstraints(self: *Self, env: *Env) std.mem.Alloca ); } } else { - // Other methods are not supported on anonymous types + // Structural types (other than is_eq) cannot have methods called on them. + // The user must explicitly wrap the value in a nominal type. try self.reportConstraintError( deferred_constraint.var_, constraint, @@ -6011,50 +6012,51 @@ pub fn createImportMapping( // Skip invalid statement indices (index 0 is typically invalid/sentinel) if (@intFromEnum(stmt_idx) != 0) { const stmt = builtin_env.store.getStatement(stmt_idx); - switch (stmt) { - .s_nominal_decl => |decl| { - const header = builtin_env.store.getTypeHeader(decl.header); - const qualified_name = builtin_env.getIdentText(header.name); - const relative_name = builtin_env.getIdentText(header.relative_name); - - // Extract display name (last component after dots) - const display_name = blk: { - var last_dot: usize = 0; - for (qualified_name, 0..) |c, i| { - if (c == '.') last_dot = i + 1; - } - break :blk qualified_name[last_dot..]; - }; - - const qualified_ident = try idents.insert(gpa, Ident.for_text(qualified_name)); - const relative_ident = try idents.insert(gpa, Ident.for_text(relative_name)); - const display_ident = try idents.insert(gpa, Ident.for_text(display_name)); - - // Add mapping for qualified_name -> display_name - if (mapping.get(qualified_ident)) |existing_ident| { - const existing_name = idents.getText(existing_ident); - if (displayNameIsBetter(display_name, existing_name)) { - try mapping.put(qualified_ident, display_ident); - } - } else { + const header_idx = switch (stmt) { + .s_nominal_decl => |decl| decl.header, + .s_alias_decl => |alias| alias.header, + else => null, + }; + if (header_idx) |hdr_idx| { + const header = builtin_env.store.getTypeHeader(hdr_idx); + const qualified_name = builtin_env.getIdentText(header.name); + const relative_name = builtin_env.getIdentText(header.relative_name); + + // Extract display name (last component after dots) + const display_name = blk: { + var last_dot: usize = 0; + for (qualified_name, 0..) |c, i| { + if (c == '.') last_dot = i + 1; + } + break :blk qualified_name[last_dot..]; + }; + + const qualified_ident = try idents.insert(gpa, Ident.for_text(qualified_name)); + const relative_ident = try idents.insert(gpa, Ident.for_text(relative_name)); + const display_ident = try idents.insert(gpa, Ident.for_text(display_name)); + + // Add mapping for qualified_name -> display_name + if (mapping.get(qualified_ident)) |existing_ident| { + const existing_name = idents.getText(existing_ident); + if (displayNameIsBetter(display_name, existing_name)) { try mapping.put(qualified_ident, display_ident); } + } else { + try mapping.put(qualified_ident, display_ident); + } - // Also add mapping for relative_name -> display_name - // This ensures types stored with relative_name (like "Num.Numeral") also map to display_name - if (mapping.get(relative_ident)) |existing_ident| { - const existing_name = idents.getText(existing_ident); - if (displayNameIsBetter(display_name, existing_name)) { - try mapping.put(relative_ident, display_ident); - } - } else { + // Also add mapping for relative_name -> display_name + // This ensures types stored with relative_name (like "Num.Numeral") also map to display_name + if (mapping.get(relative_ident)) |existing_ident| { + const existing_name = idents.getText(existing_ident); + if (displayNameIsBetter(display_name, existing_name)) { try mapping.put(relative_ident, display_ident); } - }, - else => { - // Skip non-nominal statements (e.g., nested types that aren't directly importable) - }, + } else { + try mapping.put(relative_ident, display_ident); + } } + // else: Skip non-nominal/alias statements (e.g., nested types that aren't directly importable) } } } diff --git a/src/check/unify.zig b/src/check/unify.zig index eee3d19f368..64bb66601a4 100644 --- a/src/check/unify.zig +++ b/src/check/unify.zig @@ -942,10 +942,18 @@ const Unifier = struct { return; } - // Check if the nominal's backing is also an empty record - if (b_backing_resolved.desc.content == .structure and - b_backing_resolved.desc.content.structure == .empty_record) - { + // Check if the nominal's backing is also an empty record (or record with 0 fields) + const backing_is_empty = blk: { + if (b_backing_resolved.desc.content != .structure) break :blk false; + const backing_flat = b_backing_resolved.desc.content.structure; + if (backing_flat == .empty_record) break :blk true; + if (backing_flat == .record) { + const fields = self.types_store.getRecordFieldsSlice(backing_flat.record.fields); + if (fields.len == 0) break :blk true; + } + break :blk false; + }; + if (backing_is_empty) { // Both are empty, unify with the nominal self.merge(vars, vars.b.desc.content); } else { diff --git a/src/eval/BuiltinModules.zig b/src/eval/BuiltinModules.zig index c189aeb41e9..85b7f15d8b2 100644 --- a/src/eval/BuiltinModules.zig +++ b/src/eval/BuiltinModules.zig @@ -29,7 +29,7 @@ pub const BuiltinModules = struct { builtin_indices: BuiltinIndices, /// Get an array of all builtin modules for iteration - /// For compatibility, we expose the Builtin module three times (once for each nested type) + /// For compatibility, we expose the Builtin module for each auto-imported type pub fn modules(self: *const BuiltinModules) [3]ModuleInfo { return .{ .{ .name = "Bool", .module = &self.builtin_module }, diff --git a/src/eval/StackValue.zig b/src/eval/StackValue.zig index d3fc41fc34e..8d4b965edbe 100644 --- a/src/eval/StackValue.zig +++ b/src/eval/StackValue.zig @@ -22,6 +22,7 @@ const RocOps = builtins.host_abi.RocOps; const RocList = builtins.list.RocList; const RocStr = builtins.str.RocStr; const RocDec = builtins.dec.RocDec; + const Closure = layout_mod.Closure; const StackValue = @This(); @@ -247,6 +248,10 @@ fn decrefLayoutPtr(layout: Layout, ptr: ?*anyopaque, layout_cache: *LayoutStore, }); } + // Debug assertion: closure layout index must be within bounds. + // If this trips, it indicates a compiler bug in layout index assignment. + std.debug.assert(idx_as_usize < layout_cache.layouts.len()); + const captures_layout = layout_cache.getLayout(captures_layout_idx); if (comptime trace_refcount) { @@ -432,7 +437,7 @@ pub fn copyToPtr(self: StackValue, layout_cache: *LayoutStore, dest_ptr: *anyopa std.debug.assert(self.ptr != null); const src = @as([*]u8, @ptrCast(self.ptr.?))[0..result_size]; const dst = @as([*]u8, @ptrCast(dest_ptr))[0..result_size]; - @memcpy(dst, src); + @memmove(dst, src); const record_data = layout_cache.getRecordData(self.layout.data.record.idx); if (record_data.fields.count == 0) return; @@ -463,7 +468,7 @@ pub fn copyToPtr(self: StackValue, layout_cache: *LayoutStore, dest_ptr: *anyopa std.debug.assert(self.ptr != null); const src = @as([*]u8, @ptrCast(self.ptr.?))[0..result_size]; const dst = @as([*]u8, @ptrCast(dest_ptr))[0..result_size]; - @memcpy(dst, src); + @memmove(dst, src); const tuple_data = layout_cache.getTupleData(self.layout.data.tuple.idx); if (tuple_data.fields.count == 0) return; @@ -490,10 +495,16 @@ pub fn copyToPtr(self: StackValue, layout_cache: *LayoutStore, dest_ptr: *anyopa std.debug.assert(self.ptr != null); const src = @as([*]u8, @ptrCast(self.ptr.?))[0..result_size]; const dst = @as([*]u8, @ptrCast(dest_ptr))[0..result_size]; - @memcpy(dst, src); + @memmove(dst, src); // Get the closure header to find the captures layout const closure = self.asClosure().?; + + // Debug assertion: closure layout index must be within bounds. + // If this trips, it indicates a compiler bug in layout index assignment. + const idx_as_usize = @intFromEnum(closure.captures_layout_idx); + std.debug.assert(idx_as_usize < layout_cache.layouts.len()); + const captures_layout = layout_cache.getLayout(closure.captures_layout_idx); // Only incref if there are actual captures (record with fields) @@ -526,7 +537,7 @@ pub fn copyToPtr(self: StackValue, layout_cache: *LayoutStore, dest_ptr: *anyopa std.debug.assert(self.ptr != null); const src = @as([*]u8, @ptrCast(self.ptr.?))[0..result_size]; const dst = @as([*]u8, @ptrCast(dest_ptr))[0..result_size]; - @memcpy(dst, src); + @memmove(dst, src); const base_ptr = @as([*]const u8, @ptrCast(self.ptr.?)); const discriminant = readTagUnionDiscriminant(self.layout, base_ptr, layout_cache); @@ -548,38 +559,7 @@ pub fn copyToPtr(self: StackValue, layout_cache: *LayoutStore, dest_ptr: *anyopa std.debug.assert(self.ptr != null); const src = @as([*]u8, @ptrCast(self.ptr.?))[0..result_size]; const dst = @as([*]u8, @ptrCast(dest_ptr))[0..result_size]; - - // Skip memcpy if source and destination overlap to avoid aliasing error - const src_start = @intFromPtr(src.ptr); - const src_end = src_start + result_size; - const dst_start = @intFromPtr(dst.ptr); - const dst_end = dst_start + result_size; - - // Check if ranges overlap - if ((src_start < dst_end) and (dst_start < src_end)) { - // Overlapping regions - skip if they're identical, otherwise use memmove - if (src.ptr == dst.ptr) { - return; - } - // Use manual copy for overlapping but non-identical regions - if (dst_start < src_start) { - // Copy forward - var i: usize = 0; - while (i < result_size) : (i += 1) { - dst[i] = src[i]; - } - } else { - // Copy backward - var i: usize = result_size; - while (i > 0) { - i -= 1; - dst[i] = src[i]; - } - } - return; - } - - @memcpy(dst, src); + @memmove(dst, src); } /// Read this StackValue's integer value, ensuring it's initialized @@ -1511,6 +1491,12 @@ pub fn incref(self: StackValue, layout_cache: *LayoutStore, roc_ops: *RocOps) vo if (self.layout.tag == .closure) { if (self.ptr == null) return; const closure_header: *const layout_mod.Closure = @ptrCast(@alignCast(self.ptr.?)); + + // Debug assertion: closure layout index must be within bounds. + // If this trips, it indicates a compiler bug in layout index assignment. + const idx_as_usize = @intFromEnum(closure_header.captures_layout_idx); + std.debug.assert(idx_as_usize < layout_cache.layouts.len()); + const captures_layout = layout_cache.getLayout(closure_header.captures_layout_idx); // Only incref if there are actual captures (record with fields) @@ -1752,6 +1738,12 @@ pub fn decref(self: StackValue, layout_cache: *LayoutStore, ops: *RocOps) void { pub fn getTotalSize(self: StackValue, layout_cache: *LayoutStore, _: *RocOps) u32 { if (self.layout.tag == .closure and self.ptr != null) { const closure = self.asClosure().?; + + // Debug assertion: closure layout index must be within bounds. + // If this trips, it indicates a compiler bug in layout index assignment. + const idx_as_usize = @intFromEnum(closure.captures_layout_idx); + std.debug.assert(idx_as_usize < layout_cache.layouts.len()); + const captures_layout = layout_cache.getLayout(closure.captures_layout_idx); const captures_alignment = captures_layout.alignment(layout_cache.targetUsize()); const header_size = @sizeOf(Closure); diff --git a/src/eval/test/eval_test.zig b/src/eval/test/eval_test.zig index 7d24d2bea7d..ad35e4d28b5 100644 --- a/src/eval/test/eval_test.zig +++ b/src/eval/test/eval_test.zig @@ -1761,6 +1761,296 @@ test "early return: ? in first argument of multi-arg call" { , 0, .no_trace); } +test "Decoder: create ok result - check result is Ok" { + // Test that we can create a decode result and it is an Ok + try runExpectBool( + \\{ + \\ result = { result: Ok(42i64), rest: [] } + \\ match result.result { + \\ Ok(_) => Bool.True + \\ Err(_) => Bool.False + \\ } + \\} + , true, .no_trace); +} + +test "Decoder: create ok result - extract value" { + // Test that we can extract the value from a decode result + try runExpectI64( + \\{ + \\ result = { result: Ok(42i64), rest: [] } + \\ match result.result { + \\ Ok(n) => n + \\ Err(_) => 0i64 + \\ } + \\} + , 42, .no_trace); +} + +test "Decoder: create err result" { + // Test that we can create an error decode result + try runExpectBool( + \\{ + \\ result = { result: Err(TooShort), rest: [1u8, 2u8, 3u8] } + \\ match result.result { + \\ Ok(_) => Bool.True + \\ Err(_) => Bool.False + \\ } + \\} + , false, .no_trace); +} + +test "decode: I32.decode with simple format" { + // Test I32.decode with a format that provides decode_i32 + try runExpectI64( + \\{ + \\ # Define a format type with decode_i32 method + \\ MyFormat := {}.{ + \\ decode_i32 : MyFormat, List(U8) -> (Try(I32, [Err]), List(U8)) + \\ decode_i32 = |_fmt, src| (Ok(42i32), src) + \\ } + \\ fmt : MyFormat + \\ fmt = {} + \\ (result, _rest) = I32.decode([], fmt) + \\ match result { + \\ Ok(n) => n.to_i64() + \\ Err(_) => 0i64 + \\ } + \\} + , 42, .no_trace); +} + +test "decode: I64.decode with simple format" { + // Test I64.decode with a simple format that returns a constant + try runExpectI64( + \\{ + \\ MyFormat := {}.{ + \\ decode_i64 : MyFormat, List(U8) -> (Try(I64, [Err]), List(U8)) + \\ decode_i64 = |_fmt, src| (Ok(99i64), src) + \\ } + \\ fmt : MyFormat + \\ fmt = {} + \\ (result, _rest) = I64.decode([], fmt) + \\ match result { + \\ Ok(n) => n + \\ Err(_) => 0i64 + \\ } + \\} + , 99, .no_trace); +} + +test "decode: U8.decode success" { + // Test U8.decode with simple constant format + try runExpectI64( + \\{ + \\ MyFormat := {}.{ + \\ decode_u8 : MyFormat, List(U8) -> (Try(U8, [Empty]), List(U8)) + \\ decode_u8 = |_fmt, src| (Ok(255u8), src) + \\ } + \\ fmt : MyFormat + \\ fmt = {} + \\ (result, _rest) = U8.decode([], fmt) + \\ match result { + \\ Ok(n) => n.to_i64() + \\ Err(_) => -1i64 + \\ } + \\} + , 255, .no_trace); +} + +test "decode: U8.decode error" { + // Test U8.decode returns error - use I64 result to avoid complex match + try runExpectI64( + \\{ + \\ MyFormat := {}.{ + \\ decode_u8 : MyFormat, List(U8) -> (Try(U8, [Empty]), List(U8)) + \\ decode_u8 = |_fmt, src| (Err(Empty), src) + \\ } + \\ fmt : MyFormat + \\ fmt = {} + \\ (result, _rest) = U8.decode([], fmt) + \\ match result { + \\ Ok(_) => 0i64 + \\ Err(_) => 1i64 + \\ } + \\} + , 1, .no_trace); +} + +test "decode: Bool.decode true" { + // Test Bool.decode returns true + try runExpectBool( + \\{ + \\ MyFormat := {}.{ + \\ decode_bool : MyFormat, List(U8) -> (Try(Bool, [Empty]), List(U8)) + \\ decode_bool = |_fmt, src| (Ok(Bool.True), src) + \\ } + \\ fmt : MyFormat + \\ fmt = {} + \\ (result, _rest) = Bool.decode([], fmt) + \\ match result { + \\ Ok(b) => b + \\ Err(_) => Bool.False + \\ } + \\} + , true, .no_trace); +} + +test "decode: Bool.decode false" { + // Test Bool.decode returns false + try runExpectBool( + \\{ + \\ MyFormat := {}.{ + \\ decode_bool : MyFormat, List(U8) -> (Try(Bool, [Empty]), List(U8)) + \\ decode_bool = |_fmt, src| (Ok(Bool.False), src) + \\ } + \\ fmt : MyFormat + \\ fmt = {} + \\ (result, _rest) = Bool.decode([], fmt) + \\ match result { + \\ Ok(b) => b + \\ Err(_) => Bool.True # Return True on error to distinguish + \\ } + \\} + , false, .no_trace); +} + +test "decode: Str.decode success" { + // Test Str.decode with constant + try runExpectStr( + \\{ + \\ MyFormat := {}.{ + \\ decode_str : MyFormat, List(U8) -> (Try(Str, [BadUtf8]), List(U8)) + \\ decode_str = |_fmt, src| (Ok("hi"), src) + \\ } + \\ fmt : MyFormat + \\ fmt = {} + \\ (result, _rest) = Str.decode([], fmt) + \\ match result { + \\ Ok(s) => s + \\ Err(_) => "error" + \\ } + \\} + , "hi", .no_trace); +} + +test "decode: rest returned from decode" { + // Verify that decode returns the rest bytes + try runExpectI64( + \\{ + \\ MyFormat := {}.{ + \\ decode_u8 : MyFormat, List(U8) -> (Try(U8, [Empty]), List(U8)) + \\ decode_u8 = |_fmt, src| (Ok(1u8), src) + \\ } + \\ fmt : MyFormat + \\ fmt = {} + \\ (result, _rest) = U8.decode([5u8], fmt) + \\ match result { + \\ Ok(n) => n.to_i64() + \\ Err(_) => 0i64 + \\ } + \\} + , 1, .no_trace); +} + +test "decode: U16.decode" { + // Test U16.decode + try runExpectI64( + \\{ + \\ MyFormat := {}.{ + \\ decode_u16 : MyFormat, List(U8) -> (Try(U16, [Err]), List(U8)) + \\ decode_u16 = |_fmt, src| (Ok(1000u16), src) + \\ } + \\ fmt : MyFormat + \\ fmt = {} + \\ (result, _rest) = U16.decode([], fmt) + \\ match result { + \\ Ok(n) => n.to_i64() + \\ Err(_) => 0i64 + \\ } + \\} + , 1000, .no_trace); +} + +test "decode: U32.decode" { + // Test U32.decode + try runExpectI64( + \\{ + \\ MyFormat := {}.{ + \\ decode_u32 : MyFormat, List(U8) -> (Try(U32, [Err]), List(U8)) + \\ decode_u32 = |_fmt, src| (Ok(100000u32), src) + \\ } + \\ fmt : MyFormat + \\ fmt = {} + \\ (result, _rest) = U32.decode([], fmt) + \\ match result { + \\ Ok(n) => n.to_i64() + \\ Err(_) => 0i64 + \\ } + \\} + , 100000, .no_trace); +} + +test "decode: U64.decode" { + // Test U64.decode + try runExpectI64( + \\{ + \\ MyFormat := {}.{ + \\ decode_u64 : MyFormat, List(U8) -> (Try(U64, [Err]), List(U8)) + \\ decode_u64 = |_fmt, src| (Ok(9223372036854775807u64), src) + \\ } + \\ fmt : MyFormat + \\ fmt = {} + \\ (result, _rest) = U64.decode([], fmt) + \\ match result { + \\ Ok(n) => n.to_i64_wrap() + \\ Err(_) => 0i64 + \\ } + \\} + , 9223372036854775807, .no_trace); +} + +test "decode: I8.decode negative" { + // Test I8.decode with negative value + try runExpectI64( + \\{ + \\ MyFormat := {}.{ + \\ decode_i8 : MyFormat, List(U8) -> (Try(I8, [Err]), List(U8)) + \\ decode_i8 = |_fmt, src| (Ok(-42i8), src) + \\ } + \\ fmt : MyFormat + \\ fmt = {} + \\ (result, _rest) = I8.decode([], fmt) + \\ match result { + \\ Ok(n) => n.to_i64() + \\ Err(_) => 0i64 + \\ } + \\} + , -42, .no_trace); +} + +test "decode: I16.decode negative" { + // Test I16.decode with negative value + try runExpectI64( + \\{ + \\ MyFormat := {}.{ + \\ decode_i16 : MyFormat, List(U8) -> (Try(I16, [Err]), List(U8)) + \\ decode_i16 = |_fmt, src| (Ok(-1000i16), src) + \\ } + \\ fmt : MyFormat + \\ fmt = {} + \\ (result, _rest) = I16.decode([], fmt) + \\ match result { + \\ Ok(n) => n.to_i64() + \\ Err(_) => 0i64 + \\ } + \\} + , -1000, .no_trace); +} + +// TODO: Test with multiple decode methods in same format has issues +// test "decode: chained format with different types" { ... } + test "issue 8783: List.fold with match on tag union elements from pattern match" { // Regression test: List.fold with a callback that matches on elements extracted from pattern matching // would fail with TypeMismatch in match_branches continuation. diff --git a/test/snapshots/static_dispatch/StructuralMethodError.md b/test/snapshots/static_dispatch/StructuralMethodError.md new file mode 100644 index 00000000000..90cadebeda7 --- /dev/null +++ b/test/snapshots/static_dispatch/StructuralMethodError.md @@ -0,0 +1,163 @@ +# META +~~~ini +description=Calling a method on a structural type should error +type=file +~~~ +# SOURCE +~~~roc +# Define a nominal type with empty record backing and a method +Person := {}.{ + greet : Person -> Str + greet = |_| "Hello" +} + +# This should error: calling a method on an anonymous record, +# even though Person has compatible backing type +main = { + x = {} + x.greet() +} +~~~ +# EXPECTED +TYPE MODULE MISSING MATCHING TYPE - StructuralMethodError.md:2:1:12:2 +MISSING METHOD - StructuralMethodError.md:11:7:11:12 +# PROBLEMS +**TYPE MODULE MISSING MATCHING TYPE** +Type modules must have a type declaration matching the module name. + +This file is named `StructuralMethodError`.roc, but no top-level type declaration named `StructuralMethodError` was found. + +Add either: +`StructuralMethodError := ...` (nominal type) +or: +`StructuralMethodError : ...` (type alias) +**StructuralMethodError.md:2:1:12:2:** +```roc +Person := {}.{ + greet : Person -> Str + greet = |_| "Hello" +} + +# This should error: calling a method on an anonymous record, +# even though Person has compatible backing type +main = { + x = {} + x.greet() +} +``` + + +**MISSING METHOD** +This **greet** method is being called on a value whose type doesn't have that method: +**StructuralMethodError.md:11:7:11:12:** +```roc + x.greet() +``` + ^^^^^ + +The value's type, which does not have a method named **greet**, is: + + {} + +# TOKENS +~~~zig +UpperIdent,OpColonEqual,OpenCurly,CloseCurly,Dot,OpenCurly, +LowerIdent,OpColon,UpperIdent,OpArrow,UpperIdent, +LowerIdent,OpAssign,OpBar,Underscore,OpBar,StringStart,StringPart,StringEnd, +CloseCurly, +LowerIdent,OpAssign,OpenCurly, +LowerIdent,OpAssign,OpenCurly,CloseCurly, +LowerIdent,NoSpaceDotLowerIdent,NoSpaceOpenRound,CloseRound, +CloseCurly, +EndOfFile, +~~~ +# PARSE +~~~clojure +(file + (type-module) + (statements + (s-type-decl + (header (name "Person") + (args)) + (ty-record) + (associated + (s-type-anno (name "greet") + (ty-fn + (ty (name "Person")) + (ty (name "Str")))) + (s-decl + (p-ident (raw "greet")) + (e-lambda + (args + (p-underscore)) + (e-string + (e-string-part (raw "Hello"))))))) + (s-decl + (p-ident (raw "main")) + (e-block + (statements + (s-decl + (p-ident (raw "x")) + (e-record)) + (e-field-access + (e-ident (raw "x")) + (e-apply + (e-ident (raw "greet"))))))))) +~~~ +# FORMATTED +~~~roc +# Define a nominal type with empty record backing and a method +Person := {}.{ + greet : Person -> Str + greet = |_| "Hello" +} + +# This should error: calling a method on an anonymous record, +# even though Person has compatible backing type +main = { + x = {} + x.greet() +} +~~~ +# CANONICALIZE +~~~clojure +(can-ir + (d-let + (p-assign (ident "StructuralMethodError.Person.greet")) + (e-lambda + (args + (p-underscore)) + (e-string + (e-literal (string "Hello")))) + (annotation + (ty-fn (effectful false) + (ty-lookup (name "Person") (local)) + (ty-lookup (name "Str") (builtin))))) + (d-let + (p-assign (ident "main")) + (e-block + (s-let + (p-assign (ident "x")) + (e-empty_record)) + (e-dot-access (field "greet") + (receiver + (e-lookup-local + (p-assign (ident "x")))) + (args)))) + (s-nominal-decl + (ty-header (name "Person")) + (ty-record))) +~~~ +# TYPES +~~~clojure +(inferred-types + (defs + (patt (type "Person -> Str")) + (patt (type "Error"))) + (type_decls + (nominal (type "Person") + (ty-header (name "Person")))) + (expressions + (expr (type "Person -> Str")) + (expr (type "Error")))) +~~~