// Tests for lower.zig const std = @import("std"); const ast = @import("../ast.zig"); const Node = ast.Node; const ir_mod = @import("ir.zig"); const TypeId = ir_mod.TypeId; const Ref = ir_mod.Ref; const FuncId = ir_mod.FuncId; const Lowering = ir_mod.Lowering; const parser = @import("../parser.zig"); const imports = @import("../imports.zig"); test "lower: simple function with arithmetic" { const alloc = std.testing.allocator; var module = ir_mod.Module.init(alloc); defer module.deinit(); // Build a minimal AST: add :: (a: s64, b: s64) -> s64 { return a + b; } const a_type = alloc.create(Node) catch unreachable; a_type.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .type_expr = .{ .name = "s64", .is_generic = false } } }; const b_type = alloc.create(Node) catch unreachable; b_type.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .type_expr = .{ .name = "s64", .is_generic = false } } }; const ret_type = alloc.create(Node) catch unreachable; ret_type.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .type_expr = .{ .name = "s64", .is_generic = false } } }; const a_ident = alloc.create(Node) catch unreachable; a_ident.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .identifier = .{ .name = "a" } } }; const b_ident = alloc.create(Node) catch unreachable; b_ident.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .identifier = .{ .name = "b" } } }; const add_expr = alloc.create(Node) catch unreachable; add_expr.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .binary_op = .{ .op = .add, .lhs = a_ident, .rhs = b_ident, } } }; const ret_stmt = alloc.create(Node) catch unreachable; ret_stmt.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .return_stmt = .{ .value = add_expr } } }; const body = alloc.create(Node) catch unreachable; const stmts: []const *Node = &.{ret_stmt}; body.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .block = .{ .stmts = stmts } } }; defer alloc.destroy(a_type); defer alloc.destroy(b_type); defer alloc.destroy(ret_type); defer alloc.destroy(a_ident); defer alloc.destroy(b_ident); defer alloc.destroy(add_expr); defer alloc.destroy(ret_stmt); defer alloc.destroy(body); const params: []const ast.Param = &.{ .{ .name = "a", .name_span = .{ .start = 0, .end = 0 }, .type_expr = a_type }, .{ .name = "b", .name_span = .{ .start = 0, .end = 0 }, .type_expr = b_type }, }; const fn_decl = ast.FnDecl{ .name = "add", .params = params, .return_type = ret_type, .body = body, }; var lowering = Lowering.init(&module); lowering.lowerFunction(&fn_decl, "add", false); // Verify try std.testing.expectEqual(@as(usize, 1), module.functions.items.len); const func = module.getFunction(FuncId.fromIndex(0)); try std.testing.expectEqual(@as(usize, 2), func.params.len); try std.testing.expectEqual(TypeId.s64, func.ret); try std.testing.expect(func.blocks.items.len > 0); // Print the IR to verify it looks reasonable const print_mod = @import("print.zig"); var aw = std.Io.Writer.Allocating.init(alloc); try print_mod.printModule(&module, &aw.writer); var result = aw.writer.toArrayList(); defer result.deinit(alloc); const output = result.items; try std.testing.expect(std.mem.indexOf(u8, output, "func @add") != null); try std.testing.expect(std.mem.indexOf(u8, output, "entry:") != null); try std.testing.expect(std.mem.indexOf(u8, output, "add %") != null or std.mem.indexOf(u8, output, "ret %") != null); } test "lower: instructions carry their AST node's source span (ERR E3.0)" { const alloc = std.testing.allocator; var module = ir_mod.Module.init(alloc); defer module.deinit(); // probe :: (a: s64, b: s64) -> s64 { return a + b; } — the `a + b` node // gets a distinctive span so we can find the emitted `add` instruction and // assert it was stamped (not left at the empty {0,0} default). const a_type = alloc.create(Node) catch unreachable; a_type.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .type_expr = .{ .name = "s64", .is_generic = false } } }; const b_type = alloc.create(Node) catch unreachable; b_type.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .type_expr = .{ .name = "s64", .is_generic = false } } }; const ret_type = alloc.create(Node) catch unreachable; ret_type.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .type_expr = .{ .name = "s64", .is_generic = false } } }; const a_ident = alloc.create(Node) catch unreachable; a_ident.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .identifier = .{ .name = "a" } } }; const b_ident = alloc.create(Node) catch unreachable; b_ident.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .identifier = .{ .name = "b" } } }; const add_expr = alloc.create(Node) catch unreachable; add_expr.* = .{ .span = .{ .start = 42, .end = 47 }, .data = .{ .binary_op = .{ .op = .add, .lhs = a_ident, .rhs = b_ident } } }; const ret_stmt = alloc.create(Node) catch unreachable; ret_stmt.* = .{ .span = .{ .start = 30, .end = 50 }, .data = .{ .return_stmt = .{ .value = add_expr } } }; const body = alloc.create(Node) catch unreachable; const stmts: []const *Node = &.{ret_stmt}; body.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .block = .{ .stmts = stmts } } }; defer { alloc.destroy(a_type); alloc.destroy(b_type); alloc.destroy(ret_type); alloc.destroy(a_ident); alloc.destroy(b_ident); alloc.destroy(add_expr); alloc.destroy(ret_stmt); alloc.destroy(body); } const params: []const ast.Param = &.{ .{ .name = "a", .name_span = .{ .start = 0, .end = 0 }, .type_expr = a_type }, .{ .name = "b", .name_span = .{ .start = 0, .end = 0 }, .type_expr = b_type }, }; const fn_decl = ast.FnDecl{ .name = "probe", .params = params, .return_type = ret_type, .body = body }; var lowering = Lowering.init(&module); lowering.lowerFunction(&fn_decl, "probe", false); // Find the `add` instruction and assert it carries the `a + b` span. const func = module.getFunction(FuncId.fromIndex(0)); var found = false; for (func.blocks.items) |blk| { for (blk.insts.items) |inst| { if (inst.op == .add) { try std.testing.expectEqual(@as(u32, 42), inst.span.start); try std.testing.expectEqual(@as(u32, 47), inst.span.end); found = true; } } } try std.testing.expect(found); } test "lower: if/else generates basic blocks" { const alloc = std.testing.allocator; var module = ir_mod.Module.init(alloc); defer module.deinit(); // Build AST: test :: (c: bool) -> s64 { if c { return 1; } else { return 2; } } // The condition must be a runtime value (a param) — a constant `if true` // is folded by lowering to a single block, defeating the branch test. const cond_node = alloc.create(Node) catch unreachable; defer alloc.destroy(cond_node); cond_node.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .identifier = .{ .name = "c" } } }; const cond_ty = alloc.create(Node) catch unreachable; defer alloc.destroy(cond_ty); cond_ty.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .type_expr = .{ .name = "bool", .is_generic = false } } }; const ret1_val = alloc.create(Node) catch unreachable; defer alloc.destroy(ret1_val); ret1_val.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .int_literal = .{ .value = 1 } } }; const ret2_val = alloc.create(Node) catch unreachable; defer alloc.destroy(ret2_val); ret2_val.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .int_literal = .{ .value = 2 } } }; const then_ret = alloc.create(Node) catch unreachable; defer alloc.destroy(then_ret); then_ret.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .return_stmt = .{ .value = ret1_val } } }; const else_ret = alloc.create(Node) catch unreachable; defer alloc.destroy(else_ret); else_ret.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .return_stmt = .{ .value = ret2_val } } }; const then_body = alloc.create(Node) catch unreachable; defer alloc.destroy(then_body); then_body.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .block = .{ .stmts = &.{then_ret} } } }; const else_body = alloc.create(Node) catch unreachable; defer alloc.destroy(else_body); else_body.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .block = .{ .stmts = &.{else_ret} } } }; const if_node = alloc.create(Node) catch unreachable; defer alloc.destroy(if_node); if_node.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .if_expr = .{ .condition = cond_node, .then_branch = then_body, .else_branch = else_body, .is_inline = false, } } }; const fn_body = alloc.create(Node) catch unreachable; defer alloc.destroy(fn_body); fn_body.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .block = .{ .stmts = &.{if_node} } } }; const ret_type = alloc.create(Node) catch unreachable; defer alloc.destroy(ret_type); ret_type.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .type_expr = .{ .name = "s64", .is_generic = false } } }; const fn_decl = ast.FnDecl{ .name = "test_if", .params = &.{.{ .name = "c", .name_span = .{ .start = 0, .end = 0 }, .type_expr = cond_ty }}, .return_type = ret_type, .body = fn_body, }; var lowering = Lowering.init(&module); lowering.lowerFunction(&fn_decl, "test_if", false); // Verify: should have 4 blocks (entry, if.then, if.else, if.merge) const func = module.getFunction(FuncId.fromIndex(0)); try std.testing.expectEqual(@as(usize, 4), func.blocks.items.len); // Print and verify structure const print_mod = @import("print.zig"); var aw = std.Io.Writer.Allocating.init(alloc); try print_mod.printModule(&module, &aw.writer); var result = aw.writer.toArrayList(); defer result.deinit(alloc); const output = result.items; try std.testing.expect(std.mem.indexOf(u8, output, "cond_br") != null); try std.testing.expect(std.mem.indexOf(u8, output, "if.then") != null); try std.testing.expect(std.mem.indexOf(u8, output, "if.else") != null); try std.testing.expect(std.mem.indexOf(u8, output, "if.merge") != null); } test "lower: while loop generates header/body/exit blocks" { const alloc = std.testing.allocator; var module = ir_mod.Module.init(alloc); defer module.deinit(); // Build AST: loop :: () { while true { break; } } const cond_node = alloc.create(Node) catch unreachable; defer alloc.destroy(cond_node); cond_node.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .bool_literal = .{ .value = true } } }; const break_node = alloc.create(Node) catch unreachable; defer alloc.destroy(break_node); break_node.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .break_expr }; const while_body = alloc.create(Node) catch unreachable; defer alloc.destroy(while_body); while_body.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .block = .{ .stmts = &.{break_node} } } }; const while_node = alloc.create(Node) catch unreachable; defer alloc.destroy(while_node); while_node.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .while_expr = .{ .condition = cond_node, .body = while_body, } } }; const fn_body = alloc.create(Node) catch unreachable; defer alloc.destroy(fn_body); fn_body.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .block = .{ .stmts = &.{while_node} } } }; const fn_decl = ast.FnDecl{ .name = "loop_test", .params = &.{}, .return_type = null, .body = fn_body, }; var lowering = Lowering.init(&module); lowering.lowerFunction(&fn_decl, "loop_test", false); // Verify: should have 4 blocks (entry, while.hdr, while.body, while.exit) const func = module.getFunction(FuncId.fromIndex(0)); try std.testing.expectEqual(@as(usize, 4), func.blocks.items.len); // Print and verify structure const print_mod = @import("print.zig"); var aw = std.Io.Writer.Allocating.init(alloc); try print_mod.printModule(&module, &aw.writer); var result = aw.writer.toArrayList(); defer result.deinit(alloc); const output = result.items; try std.testing.expect(std.mem.indexOf(u8, output, "while.hdr") != null); try std.testing.expect(std.mem.indexOf(u8, output, "while.body") != null); try std.testing.expect(std.mem.indexOf(u8, output, "while.exit") != null); try std.testing.expect(std.mem.indexOf(u8, output, "cond_br") != null); } // M1.2 A.1 — Obj-C type-encoding helper. test "lower: objcTypeEncodingFromSignature emits primitive shapes" { const alloc = std.testing.allocator; var module = ir_mod.Module.init(alloc); defer module.deinit(); var lowering = Lowering.init(&module); // Niladic void method: -(void)greet → "v@:" const e1 = try lowering.objc().objcTypeEncodingFromSignature(.void, &.{}, null); defer alloc.free(e1); try std.testing.expectEqualStrings("v@:", e1); // Returns s32, takes s32: -(int)add:(int)x → "i@:i" const e2 = try lowering.objc().objcTypeEncodingFromSignature(.s32, &.{.s32}, null); defer alloc.free(e2); try std.testing.expectEqualStrings("i@:i", e2); // s64 return, two s64 args: "q@:qq" const e3 = try lowering.objc().objcTypeEncodingFromSignature(.s64, &.{ .s64, .s64 }, null); defer alloc.free(e3); try std.testing.expectEqualStrings("q@:qq", e3); // BOOL return (s8): "c@:" const e4 = try lowering.objc().objcTypeEncodingFromSignature(.s8, &.{}, null); defer alloc.free(e4); try std.testing.expectEqualStrings("c@:", e4); // Float/double: "f@:d" const e5 = try lowering.objc().objcTypeEncodingFromSignature(.f32, &.{.f64}, null); defer alloc.free(e5); try std.testing.expectEqualStrings("f@:d", e5); // bool (i1) is `B` — distinct from BOOL (`c`). const e6 = try lowering.objc().objcTypeEncodingFromSignature(.bool, &.{.bool}, null); defer alloc.free(e6); try std.testing.expectEqualStrings("B@:B", e6); // usize / isize on the 64-bit target. const e7 = try lowering.objc().objcTypeEncodingFromSignature(.usize, &.{.isize}, null); defer alloc.free(e7); try std.testing.expectEqualStrings("Q@:q", e7); // Unsigned variants u8/u16/u32/u64. const e8 = try lowering.objc().objcTypeEncodingFromSignature(.u32, &.{ .u8, .u16, .u64 }, null); defer alloc.free(e8); try std.testing.expectEqualStrings("I@:CSQ", e8); } test "lower: objcTypeEncodingFromSignature emits pointer shapes" { const alloc = std.testing.allocator; var module = ir_mod.Module.init(alloc); defer module.deinit(); var lowering = Lowering.init(&module); // Generic `*void` → `^v`. const void_ptr = module.types.ptrTo(.void); const e1 = try lowering.objc().objcTypeEncodingFromSignature(void_ptr, &.{void_ptr}, null); defer alloc.free(e1); try std.testing.expectEqualStrings("^v@:^v", e1); // `[*]u8` C-string carrier → `*`. const u8_many = module.types.intern(.{ .many_pointer = .{ .element = .u8 } }); const e2 = try lowering.objc().objcTypeEncodingFromSignature(.void, &.{u8_many}, null); defer alloc.free(e2); try std.testing.expectEqualStrings("v@:*", e2); // `[*]s32` (non-u8 many-pointer) → `^v`. const s32_many = module.types.intern(.{ .many_pointer = .{ .element = .s32 } }); const e3 = try lowering.objc().objcTypeEncodingFromSignature(.void, &.{s32_many}, null); defer alloc.free(e3); try std.testing.expectEqualStrings("v@:^v", e3); } // M1.2 A.2 — sx-defined #objc_class state struct construction. test "lower: objcDefinedStateStructType collects user-declared fields" { const alloc = std.testing.allocator; var module = ir_mod.Module.init(alloc); defer module.deinit(); var lowering = Lowering.init(&module); // Synthesize a #objc_class("SxFoo") { counter: s32; ticks: s64; } AST. const span = ast.Span{ .start = 0, .end = 0 }; const counter_type = try alloc.create(Node); defer alloc.destroy(counter_type); counter_type.* = .{ .span = span, .data = .{ .type_expr = .{ .name = "s32", .is_generic = false } } }; const ticks_type = try alloc.create(Node); defer alloc.destroy(ticks_type); ticks_type.* = .{ .span = span, .data = .{ .type_expr = .{ .name = "s64", .is_generic = false } } }; const members = [_]ast.ForeignClassMember{ .{ .field = .{ .name = "counter", .field_type = counter_type } }, .{ .field = .{ .name = "ticks", .field_type = ticks_type } }, }; const fcd = ast.ForeignClassDecl{ .name = "SxFoo", .foreign_path = "SxFoo", .runtime = .objc_class, .members = &members, .is_foreign = false, .is_main = false, }; const state_ty = lowering.objc().objcDefinedStateStructType(&fcd); const info = module.types.get(state_ty); try std.testing.expectEqual(@as(std.meta.Tag(@TypeOf(info)), .@"struct"), std.meta.activeTag(info)); const s = info.@"struct"; try std.testing.expectEqualStrings("__SxFooState", module.types.getString(s.name)); try std.testing.expectEqual(@as(usize, 2), s.fields.len); try std.testing.expectEqualStrings("counter", module.types.getString(s.fields[0].name)); try std.testing.expectEqual(TypeId.s32, s.fields[0].ty); try std.testing.expectEqualStrings("ticks", module.types.getString(s.fields[1].name)); try std.testing.expectEqual(TypeId.s64, s.fields[1].ty); // Idempotency: a second call returns the same TypeId (cache hit on name). const state_ty2 = lowering.objc().objcDefinedStateStructType(&fcd); try std.testing.expectEqual(state_ty, state_ty2); } test "lower: objcDefinedStateStructType handles empty field set" { const alloc = std.testing.allocator; var module = ir_mod.Module.init(alloc); defer module.deinit(); var lowering = Lowering.init(&module); const fcd = ast.ForeignClassDecl{ .name = "SxEmpty", .foreign_path = "SxEmpty", .runtime = .objc_class, .members = &.{}, .is_foreign = false, .is_main = false, }; const state_ty = lowering.objc().objcDefinedStateStructType(&fcd); const info = module.types.get(state_ty); try std.testing.expectEqualStrings("__SxEmptyState", module.types.getString(info.@"struct".name)); try std.testing.expectEqual(@as(usize, 0), info.@"struct".fields.len); } test "lower: objcDefinedStateStructType skips non-field members" { const alloc = std.testing.allocator; var module = ir_mod.Module.init(alloc); defer module.deinit(); var lowering = Lowering.init(&module); // Mix in #extends and method members — only `.field` contributes. const span = ast.Span{ .start = 0, .end = 0 }; const counter_type = try alloc.create(Node); defer alloc.destroy(counter_type); counter_type.* = .{ .span = span, .data = .{ .type_expr = .{ .name = "s32", .is_generic = false } } }; const members = [_]ast.ForeignClassMember{ .{ .extends = "NSObject" }, .{ .field = .{ .name = "counter", .field_type = counter_type } }, .{ .implements = "UIApplicationDelegate" }, }; const fcd = ast.ForeignClassDecl{ .name = "SxMixed", .foreign_path = "SxMixed", .runtime = .objc_class, .members = &members, .is_foreign = false, .is_main = false, }; const state_ty = lowering.objc().objcDefinedStateStructType(&fcd); const info = module.types.get(state_ty); try std.testing.expectEqual(@as(usize, 1), info.@"struct".fields.len); try std.testing.expectEqualStrings("counter", module.types.getString(info.@"struct".fields[0].name)); } test "lower: objcTypeEncodingFromSignature emits @ for Obj-C class pointers" { const alloc = std.testing.allocator; var module = ir_mod.Module.init(alloc); defer module.deinit(); var lowering = Lowering.init(&module); // Synthesize a foreign Obj-C class entry so the encoder recognises // `*NSString` as an object pointer. const ns_name = module.types.internString("NSString"); const ns_struct = module.types.intern(.{ .@"struct" = .{ .name = ns_name, .fields = &.{} } }); const ns_ptr = module.types.ptrTo(ns_struct); var ns_fcd = ast.ForeignClassDecl{ .name = "NSString", .foreign_path = "NSString", .runtime = .objc_class, .members = &.{}, .is_foreign = true, .is_main = false, }; try lowering.program_index.foreign_class_map.put("NSString", &ns_fcd); // Return *NSString, no args: "@@:" const e1 = try lowering.objc().objcTypeEncodingFromSignature(ns_ptr, &.{}, null); defer alloc.free(e1); try std.testing.expectEqualStrings("@@:", e1); // Return *NSString, take *NSString: "@@:@" const e2 = try lowering.objc().objcTypeEncodingFromSignature(ns_ptr, &.{ns_ptr}, null); defer alloc.free(e2); try std.testing.expectEqualStrings("@@:@", e2); } test "lower: objcTypeEncodingFromSignature unwraps optional to wire type" { const alloc = std.testing.allocator; var module = ir_mod.Module.init(alloc); defer module.deinit(); var lowering = Lowering.init(&module); // Foreign `*NSString` so the encoder recognises it as `@`. const ns_name = module.types.internString("NSString"); const ns_struct = module.types.intern(.{ .@"struct" = .{ .name = ns_name, .fields = &.{} } }); const ns_ptr = module.types.ptrTo(ns_struct); var ns_fcd = ast.ForeignClassDecl{ .name = "NSString", .foreign_path = "NSString", .runtime = .objc_class, .members = &.{}, .is_foreign = true, .is_main = false, }; try lowering.program_index.foreign_class_map.put("NSString", &ns_fcd); // `?s64 -> ?*NSString` collapses to `q -> @` at the Obj-C boundary. const opt_s64 = module.types.optionalOf(.s64); const opt_ns = module.types.optionalOf(ns_ptr); const e1 = try lowering.objc().objcTypeEncodingFromSignature(opt_ns, &.{opt_s64}, null); defer alloc.free(e1); try std.testing.expectEqualStrings("@@:q", e1); // Nested optional unwrap (`??f64`) — same as `f64` at the wire. const opt_f64 = module.types.optionalOf(.f64); const opt_opt_f64 = module.types.optionalOf(opt_f64); const e2 = try lowering.objc().objcTypeEncodingFromSignature(.void, &.{opt_opt_f64}, null); defer alloc.free(e2); try std.testing.expectEqualStrings("v@:d", e2); } test "lower: objcTypeEncodingFromSignature emits structs as {Name=fields...}" { const alloc = std.testing.allocator; var module = ir_mod.Module.init(alloc); defer module.deinit(); var lowering = Lowering.init(&module); // CGPoint :: struct { x: f64; y: f64 } → {CGPoint=dd} const cgpoint_name = module.types.internString("CGPoint"); const cgpoint_x_name = module.types.internString("x"); const cgpoint_y_name = module.types.internString("y"); const cgpoint_fields = [_]ir_mod.types.TypeInfo.StructInfo.Field{ .{ .name = cgpoint_x_name, .ty = .f64 }, .{ .name = cgpoint_y_name, .ty = .f64 }, }; const cgpoint = module.types.intern(.{ .@"struct" = .{ .name = cgpoint_name, .fields = &cgpoint_fields } }); // `-(void)setOrigin:(CGPoint)p` → `v@:{CGPoint=dd}` const e1 = try lowering.objc().objcTypeEncodingFromSignature(.void, &.{cgpoint}, null); defer alloc.free(e1); try std.testing.expectEqualStrings("v@:{CGPoint=dd}", e1); // `-(CGPoint)origin` → `{CGPoint=dd}@:` const e2 = try lowering.objc().objcTypeEncodingFromSignature(cgpoint, &.{}, null); defer alloc.free(e2); try std.testing.expectEqualStrings("{CGPoint=dd}@:", e2); // NSRange ({u64 location; u64 length}) → {_NSRange=QQ} (Apple uses // the underscore-prefixed internal name in practice, but we faithfully // emit whatever the struct is registered as). const nsrange_name = module.types.internString("_NSRange"); const loc_name = module.types.internString("location"); const len_name = module.types.internString("length"); const nsrange_fields = [_]ir_mod.types.TypeInfo.StructInfo.Field{ .{ .name = loc_name, .ty = .u64 }, .{ .name = len_name, .ty = .u64 }, }; const nsrange = module.types.intern(.{ .@"struct" = .{ .name = nsrange_name, .fields = &nsrange_fields } }); const e3 = try lowering.objc().objcTypeEncodingFromSignature(nsrange, &.{ nsrange, .s64 }, null); defer alloc.free(e3); try std.testing.expectEqualStrings("{_NSRange=QQ}@:{_NSRange=QQ}q", e3); } test "lower: objcTypeEncodingFromSignature emits nested structs (CGRect)" { const alloc = std.testing.allocator; var module = ir_mod.Module.init(alloc); defer module.deinit(); var lowering = Lowering.init(&module); // CGPoint and CGSize, both {f64, f64}. const cgpoint_name = module.types.internString("CGPoint"); const cgsize_name = module.types.internString("CGSize"); const x_name = module.types.internString("x"); const y_name = module.types.internString("y"); const w_name = module.types.internString("width"); const h_name = module.types.internString("height"); const cgpoint_fields = [_]ir_mod.types.TypeInfo.StructInfo.Field{ .{ .name = x_name, .ty = .f64 }, .{ .name = y_name, .ty = .f64 }, }; const cgsize_fields = [_]ir_mod.types.TypeInfo.StructInfo.Field{ .{ .name = w_name, .ty = .f64 }, .{ .name = h_name, .ty = .f64 }, }; const cgpoint = module.types.intern(.{ .@"struct" = .{ .name = cgpoint_name, .fields = &cgpoint_fields } }); const cgsize = module.types.intern(.{ .@"struct" = .{ .name = cgsize_name, .fields = &cgsize_fields } }); // CGRect :: struct { origin: CGPoint; size: CGSize } → // {CGRect={CGPoint=dd}{CGSize=dd}} const cgrect_name = module.types.internString("CGRect"); const origin_name = module.types.internString("origin"); const size_name = module.types.internString("size"); const cgrect_fields = [_]ir_mod.types.TypeInfo.StructInfo.Field{ .{ .name = origin_name, .ty = cgpoint }, .{ .name = size_name, .ty = cgsize }, }; const cgrect = module.types.intern(.{ .@"struct" = .{ .name = cgrect_name, .fields = &cgrect_fields } }); // `-(CGRect)frame` → `{CGRect={CGPoint=dd}{CGSize=dd}}@:` const e1 = try lowering.objc().objcTypeEncodingFromSignature(cgrect, &.{}, null); defer alloc.free(e1); try std.testing.expectEqualStrings("{CGRect={CGPoint=dd}{CGSize=dd}}@:", e1); // `-(void)setFrame:(CGRect)f` round-trip. const e2 = try lowering.objc().objcTypeEncodingFromSignature(.void, &.{cgrect}, null); defer alloc.free(e2); try std.testing.expectEqualStrings("v@:{CGRect={CGPoint=dd}{CGSize=dd}}", e2); } // ── A6.1 scaffolding: pure Obj-C decision helpers ─────────────────── // Lock selector derivation, property-kind classification, and Obj-C // class-pointer recognition before they move to `ffi_objc.zig`. fn objcMethod(name: []const u8) ast.ForeignMethodDecl { return .{ .name = name, .params = &.{}, .param_names = &.{}, .return_type = null }; } test "lower: deriveObjcSelector — niladic / keyword / multi-keyword / override" { const alloc = std.testing.allocator; var module = ir_mod.Module.init(alloc); defer module.deinit(); var lowering = Lowering.init(&module); // arity 0 → bare name, no colons, not an override. const niladic = lowering.objc().deriveObjcSelector(objcMethod("count"), 0); try std.testing.expectEqualStrings("count", niladic.sel); try std.testing.expectEqual(@as(usize, 0), niladic.keyword_count); try std.testing.expectEqual(false, niladic.is_override); // arity ≥ 1, no `_` → single trailing colon, one keyword. const single = lowering.objc().deriveObjcSelector(objcMethod("setValue"), 1); defer alloc.free(single.sel); try std.testing.expectEqualStrings("setValue:", single.sel); try std.testing.expectEqual(@as(usize, 1), single.keyword_count); try std.testing.expectEqual(false, single.is_override); // each `_` → `:`, plus a trailing `:`; piece count = (#`_`) + 1. const multi = lowering.objc().deriveObjcSelector(objcMethod("setValue_forKey"), 2); defer alloc.free(multi.sel); try std.testing.expectEqualStrings("setValue:forKey:", multi.sel); try std.testing.expectEqual(@as(usize, 2), multi.keyword_count); try std.testing.expectEqual(false, multi.is_override); // `#selector(...)` override: used verbatim, keyword_count = #colons. var m = objcMethod("init_with_frame_style"); m.selector_override = "initWithFrame:style:"; const overridden = lowering.objc().deriveObjcSelector(m, 2); try std.testing.expectEqualStrings("initWithFrame:style:", overridden.sel); try std.testing.expectEqual(@as(usize, 2), overridden.keyword_count); try std.testing.expectEqual(true, overridden.is_override); } test "lower: isObjcClassPointer recognises pointer-to-foreign-Obj-C-class" { const alloc = std.testing.allocator; var module = ir_mod.Module.init(alloc); defer module.deinit(); var lowering = Lowering.init(&module); // *NSString where NSString is a registered Obj-C class → true. const ns_name = module.types.internString("NSString"); const ns_struct = module.types.intern(.{ .@"struct" = .{ .name = ns_name, .fields = &.{} } }); const ns_ptr = module.types.ptrTo(ns_struct); var ns_fcd = ast.ForeignClassDecl{ .name = "NSString", .foreign_path = "NSString", .runtime = .objc_class, .members = &.{}, .is_foreign = true, .is_main = false, }; try lowering.program_index.foreign_class_map.put("NSString", &ns_fcd); try std.testing.expect(lowering.objc().isObjcClassPointer(ns_ptr)); // *NSCopying where NSCopying is a registered Obj-C *protocol* → also true // (the predicate accepts .objc_class OR .objc_protocol). const proto_name = module.types.internString("NSCopying"); const proto_struct = module.types.intern(.{ .@"struct" = .{ .name = proto_name, .fields = &.{} } }); const proto_ptr = module.types.ptrTo(proto_struct); var proto_fcd = ast.ForeignClassDecl{ .name = "NSCopying", .foreign_path = "NSCopying", .runtime = .objc_protocol, .members = &.{}, .is_foreign = true, .is_main = false, }; try lowering.program_index.foreign_class_map.put("NSCopying", &proto_fcd); try std.testing.expect(lowering.objc().isObjcClassPointer(proto_ptr)); // *Plain where Plain is a non-foreign struct → false. const plain_name = module.types.internString("Plain"); const plain_struct = module.types.intern(.{ .@"struct" = .{ .name = plain_name, .fields = &.{} } }); try std.testing.expect(!lowering.objc().isObjcClassPointer(module.types.ptrTo(plain_struct))); // *void and a builtin scalar → false (not object pointers). try std.testing.expect(!lowering.objc().isObjcClassPointer(module.types.ptrTo(.void))); try std.testing.expect(!lowering.objc().isObjcClassPointer(.s32)); } test "lower: objcPropertyKind defaults + explicit ARC modifiers" { const alloc = std.testing.allocator; var module = ir_mod.Module.init(alloc); defer module.deinit(); var lowering = Lowering.init(&module); // Register NSString so `*NSString` resolves to an object pointer. const ns_name = module.types.internString("NSString"); _ = module.types.intern(.{ .@"struct" = .{ .name = ns_name, .fields = &.{} } }); var ns_fcd = ast.ForeignClassDecl{ .name = "NSString", .foreign_path = "NSString", .runtime = .objc_class, .members = &.{}, .is_foreign = true, .is_main = false, }; try lowering.program_index.foreign_class_map.put("NSString", &ns_fcd); // Primitive field, no modifiers → assign (the non-object default). const prim = ast.ForeignFieldDecl{ .name = "count", .field_type = typeKeyword(alloc, "s32"), .is_property = true }; defer alloc.destroy(prim.field_type); try std.testing.expect(lowering.objc().objcPropertyKind(prim) == .assign); // Object-pointer field, no modifiers → strong (the object default). const obj_ty = typeKeyword(alloc, "*NSString"); defer alloc.destroy(obj_ty); const obj_default = ast.ForeignFieldDecl{ .name = "title", .field_type = obj_ty, .is_property = true }; try std.testing.expect(lowering.objc().objcPropertyKind(obj_default) == .strong); // Protocol-pointer field → also strong by default (same object-pointer // predicate accepts .objc_protocol). const proto_name = module.types.internString("NSCoding"); _ = module.types.intern(.{ .@"struct" = .{ .name = proto_name, .fields = &.{} } }); var proto_fcd = ast.ForeignClassDecl{ .name = "NSCoding", .foreign_path = "NSCoding", .runtime = .objc_protocol, .members = &.{}, .is_foreign = true, .is_main = false, }; try lowering.program_index.foreign_class_map.put("NSCoding", &proto_fcd); const proto_ty = typeKeyword(alloc, "*NSCoding"); defer alloc.destroy(proto_ty); const proto_default = ast.ForeignFieldDecl{ .name = "coder", .field_type = proto_ty, .is_property = true }; try std.testing.expect(lowering.objc().objcPropertyKind(proto_default) == .strong); // Explicit modifiers on an object pointer win over the default. const weak_mods = [_][]const u8{"weak"}; try std.testing.expect(lowering.objc().objcPropertyKind(.{ .name = "delegate", .field_type = obj_ty, .is_property = true, .property_modifiers = &weak_mods }) == .weak); const copy_mods = [_][]const u8{"copy"}; try std.testing.expect(lowering.objc().objcPropertyKind(.{ .name = "name", .field_type = obj_ty, .is_property = true, .property_modifiers = ©_mods }) == .copy); const assign_mods = [_][]const u8{"assign"}; try std.testing.expect(lowering.objc().objcPropertyKind(.{ .name = "raw", .field_type = obj_ty, .is_property = true, .property_modifiers = &assign_mods }) == .assign); } // ── Pack projection name resolution (Feature 1, Step 2.2) ──────────── const errors = @import("../errors.zig"); fn typeKeyword(alloc: std.mem.Allocator, name: []const u8) *Node { const n = alloc.create(Node) catch unreachable; n.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .type_expr = .{ .name = name, .is_generic = false } } }; return n; } fn protoMethod(name: []const u8) ast.ProtocolMethodDecl { return .{ .name = name, .params = &.{}, .param_names = &.{}, .return_type = null, .default_body = null }; } test "pack projection: type-arg vs method namespace lookups" { const alloc = std.testing.allocator; var module = ir_mod.Module.init(alloc); defer module.deinit(); var lowering = Lowering.init(&module); // Wrap :: protocol(Target: Type) { wrap :: () -> Target; value :: () -> Target; } const type_kw = typeKeyword(alloc, "Type"); defer alloc.destroy(type_kw); const type_params = [_]ast.StructTypeParam{.{ .name = "Target", .constraint = type_kw }}; const methods = [_]ast.ProtocolMethodDecl{ protoMethod("wrap"), protoMethod("value") }; const pd = ast.ProtocolDecl{ .name = "Wrap", .methods = &methods, .type_params = &type_params }; lowering.registerProtocolDecl(&pd); // type-arg namespace try std.testing.expectEqual(@as(?u32, 0), lowering.lookupProtocolArg("Wrap", "Target")); try std.testing.expectEqual(@as(?u32, null), lowering.lookupProtocolArg("Wrap", "wrap")); try std.testing.expectEqual(@as(?u32, null), lowering.lookupProtocolArg("Nope", "Target")); // method (runtime-accessor) namespace try std.testing.expectEqual(@as(?u32, 0), lowering.lookupProtocolField("Wrap", "wrap")); try std.testing.expectEqual(@as(?u32, 1), lowering.lookupProtocolField("Wrap", "value")); try std.testing.expectEqual(@as(?u32, null), lowering.lookupProtocolField("Wrap", "Target")); } test "pack projection: position-driven resolution (Decision 4)" { const alloc = std.testing.allocator; var module = ir_mod.Module.init(alloc); defer module.deinit(); var lowering = Lowering.init(&module); const type_kw = typeKeyword(alloc, "Type"); defer alloc.destroy(type_kw); const type_params = [_]ast.StructTypeParam{.{ .name = "Target", .constraint = type_kw }}; const methods = [_]ast.ProtocolMethodDecl{protoMethod("wrap")}; const pd = ast.ProtocolDecl{ .name = "Wrap", .methods = &methods, .type_params = &type_params }; lowering.registerProtocolDecl(&pd); // type position consults type-args only try std.testing.expectEqual(Lowering.PackProjection{ .type_arg = 0 }, lowering.resolvePackProjection("Wrap", "Target", .type_position)); try std.testing.expectEqual(Lowering.PackProjection.not_found, lowering.resolvePackProjection("Wrap", "wrap", .type_position)); // value position consults methods only — no cross-namespace fallback try std.testing.expectEqual(Lowering.PackProjection{ .method = 0 }, lowering.resolvePackProjection("Wrap", "wrap", .value_position)); try std.testing.expectEqual(Lowering.PackProjection.not_found, lowering.resolvePackProjection("Wrap", "Target", .value_position)); } test "pack projection: same-name type-arg + method warns (Decision 4)" { // Arena: DiagnosticList.addFmt allocates messages it never frees in deinit // (mixed ownership with borrowed literals) — an arena keeps the leak // checker clean without changing diagnostic semantics. var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); var module = ir_mod.Module.init(alloc); defer module.deinit(); var diags = errors.DiagnosticList.init(alloc, "", "test.sx"); defer diags.deinit(); var lowering = Lowering.init(&module); lowering.diagnostics = &diags; // A protocol whose type-arg and method share the name `value`. const type_kw = typeKeyword(alloc, "Type"); defer alloc.destroy(type_kw); const type_params = [_]ast.StructTypeParam{.{ .name = "value", .constraint = type_kw }}; const methods = [_]ast.ProtocolMethodDecl{protoMethod("value")}; const pd = ast.ProtocolDecl{ .name = "Shadowy", .methods = &methods, .type_params = &type_params }; lowering.registerProtocolDecl(&pd); var warned = false; for (diags.items.items) |d| { if (d.level == .warn and std.mem.indexOf(u8, d.message, "type-arg and method both named 'value'") != null) warned = true; } try std.testing.expect(warned); // Position still resolves deterministically despite the shadow. try std.testing.expectEqual(Lowering.PackProjection{ .type_arg = 0 }, lowering.resolvePackProjection("Shadowy", "value", .type_position)); try std.testing.expectEqual(Lowering.PackProjection{ .method = 0 }, lowering.resolvePackProjection("Shadowy", "value", .value_position)); } test "E1.4b converge inferred error sets: empty -> warning, raising -> converged set" { // The empty-inferred warning isn't user-visible yet (the compile driver // only renders diagnostics on failure — a LANG follow-up), so validate the // SCC's emission + set computation directly on the DiagnosticList. var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); var module = ir_mod.Module.init(alloc); defer module.deinit(); var diags = errors.DiagnosticList.init(alloc, "", "test.sx"); defer diags.deinit(); var lowering = Lowering.init(&module); lowering.diagnostics = &diags; // stub :: () -> ! { return; } — bare `!`, never raises. const stub_rt = alloc.create(Node) catch unreachable; stub_rt.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .error_type_expr = .{ .name = null } } }; const stub_ret = alloc.create(Node) catch unreachable; stub_ret.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .return_stmt = .{ .value = null } } }; const stub_body = alloc.create(Node) catch unreachable; const stub_stmts: []const *Node = &.{stub_ret}; stub_body.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .block = .{ .stmts = stub_stmts } } }; const stub_fd = ast.FnDecl{ .name = "stub", .params = &.{}, .return_type = stub_rt, .body = stub_body }; // raiser :: () -> ! { raise error.Foo; } — bare `!`, raises Foo. const r_rt = alloc.create(Node) catch unreachable; r_rt.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .error_type_expr = .{ .name = null } } }; const r_err = alloc.create(Node) catch unreachable; r_err.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .identifier = .{ .name = "error" } } }; const r_fa = alloc.create(Node) catch unreachable; r_fa.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .field_access = .{ .object = r_err, .field = "Foo" } } }; const r_raise = alloc.create(Node) catch unreachable; r_raise.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .raise_stmt = .{ .tag = r_fa } } }; const r_body = alloc.create(Node) catch unreachable; const r_stmts: []const *Node = &.{r_raise}; r_body.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .block = .{ .stmts = r_stmts } } }; const raiser_fd = ast.FnDecl{ .name = "raiser", .params = &.{}, .return_type = r_rt, .body = r_body }; lowering.program_index.fn_ast_map.put("stub", &stub_fd) catch unreachable; lowering.program_index.fn_ast_map.put("raiser", &raiser_fd) catch unreachable; lowering.convergeInferredErrorSets(); // raiser converges to {Foo} (non-empty); stub to ∅. try std.testing.expectEqual(@as(usize, 1), (lowering.inferred_error_sets.get("raiser") orelse unreachable).len); try std.testing.expectEqual(@as(usize, 0), (lowering.inferred_error_sets.get("stub") orelse unreachable).len); // The empty-set (stub) warns; the raising one does not. var stub_warned = false; var raiser_warned = false; for (diags.items.items) |d| { if (d.level != .warn) continue; if (std.mem.indexOf(u8, d.message, "stub") != null) stub_warned = true; if (std.mem.indexOf(u8, d.message, "raiser") != null) raiser_warned = true; } try std.testing.expect(stub_warned); try std.testing.expect(!raiser_warned); } test "E1.4c noreturn typing: divergence shapes + if-else unification + block propagation" { const alloc = std.testing.allocator; var module = ir_mod.Module.init(alloc); defer module.deinit(); var lowering = Lowering.init(&module); const mk = struct { fn node(a: std.mem.Allocator, data: ast.Node.Data) *Node { const n = a.create(Node) catch unreachable; n.* = .{ .span = .{ .start = 0, .end = 0 }, .data = data }; return n; } }; // return; / break; / continue; / raise error.X → noreturn const ret = mk.node(alloc, .{ .return_stmt = .{ .value = null } }); defer alloc.destroy(ret); const brk = mk.node(alloc, .{ .break_expr = {} }); defer alloc.destroy(brk); const cont = mk.node(alloc, .{ .continue_expr = {} }); defer alloc.destroy(cont); const err_id = mk.node(alloc, .{ .identifier = .{ .name = "error" } }); defer alloc.destroy(err_id); const fa = mk.node(alloc, .{ .field_access = .{ .object = err_id, .field = "X" } }); defer alloc.destroy(fa); const raise = mk.node(alloc, .{ .raise_stmt = .{ .tag = fa } }); defer alloc.destroy(raise); try std.testing.expectEqual(TypeId.noreturn, lowering.inferExprType(ret)); try std.testing.expectEqual(TypeId.noreturn, lowering.inferExprType(brk)); try std.testing.expectEqual(TypeId.noreturn, lowering.inferExprType(cont)); try std.testing.expectEqual(TypeId.noreturn, lowering.inferExprType(raise)); // Block whose last statement diverges → noreturn. const five = mk.node(alloc, .{ .int_literal = .{ .value = 5 } }); defer alloc.destroy(five); const blk_stmts: []const *Node = &.{ five, ret }; const blk = mk.node(alloc, .{ .block = .{ .stmts = blk_stmts, .produces_value = true } }); defer alloc.destroy(blk); try std.testing.expectEqual(TypeId.noreturn, lowering.inferExprType(blk)); // if-else with one diverging branch unifies to the other branch's type; // both diverging → noreturn. const lit = mk.node(alloc, .{ .int_literal = .{ .value = 1 } }); defer alloc.destroy(lit); const then_div = mk.node(alloc, .{ .if_expr = .{ .condition = lit, .then_branch = ret, .else_branch = lit, .is_inline = false } }); defer alloc.destroy(then_div); try std.testing.expectEqual(TypeId.s64, lowering.inferExprType(then_div)); // then diverges → else (s64) const else_div = mk.node(alloc, .{ .if_expr = .{ .condition = lit, .then_branch = lit, .else_branch = ret, .is_inline = false } }); defer alloc.destroy(else_div); try std.testing.expectEqual(TypeId.s64, lowering.inferExprType(else_div)); // then is s64 const both_div = mk.node(alloc, .{ .if_expr = .{ .condition = lit, .then_branch = ret, .else_branch = brk, .is_inline = false } }); defer alloc.destroy(both_div); try std.testing.expectEqual(TypeId.noreturn, lowering.inferExprType(both_div)); } // ── A4.2 test-first scaffolding: protocol-decl registration ────────── // Lock `registerProtocolDecl`'s method-table output (consumed by protocol // dispatch + impl planning) before the protocol/impl lookup moves to // `src/ir/protocols.zig`. Public surface only (registerProtocolDecl + // getProtocolInfo are pub) — the impl-lookup / conversion plan tests land // with the registry in sub-step 2 (as A4.1's internal tests landed with // GenericResolver). Arena: a non-parameterized protocol dupes its method // infos via the module allocator and never frees them. test "protocols: registerProtocolDecl builds the dispatch method table" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); var module = ir_mod.Module.init(alloc); defer module.deinit(); var lowering = Lowering.init(&module); // Shape :: protocol { area :: () -> f64; scaled :: (factor: f64) -> Self; } const methods = [_]ast.ProtocolMethodDecl{ .{ .name = "area", .params = &.{}, .param_names = &.{}, .return_type = typeKeyword(alloc, "f64"), .default_body = null }, .{ .name = "scaled", .params = &[_]*Node{typeKeyword(alloc, "f64")}, .param_names = &[_][]const u8{"factor"}, .return_type = typeKeyword(alloc, "Self"), .default_body = null, }, }; const pd = ast.ProtocolDecl{ .name = "Shape", .methods = &methods }; lowering.registerProtocolDecl(&pd); // getProtocolInfo resolves the registered protocol struct by type. const shape_ty = module.types.findByName(module.types.internString("Shape")).?; const info = lowering.getProtocolInfo(shape_ty).?; try std.testing.expectEqual(@as(usize, 2), info.methods.len); // area :: () -> f64 — no params (self excluded), concrete f64 return. try std.testing.expectEqualStrings("area", info.methods[0].name); try std.testing.expectEqual(@as(usize, 0), info.methods[0].param_types.len); try std.testing.expectEqual(TypeId.f64, info.methods[0].ret_type); try std.testing.expect(!info.methods[0].ret_is_self); // scaled :: (factor: f64) -> Self — one f64 param; `Self` return is // flagged and encoded as `*void` (the dispatcher auto-unboxes it). try std.testing.expectEqualStrings("scaled", info.methods[1].name); try std.testing.expectEqual(@as(usize, 1), info.methods[1].param_types.len); try std.testing.expectEqual(TypeId.f64, info.methods[1].param_types[0]); try std.testing.expect(info.methods[1].ret_is_self); try std.testing.expectEqual(module.types.ptrTo(.void), info.methods[1].ret_type); } // ── A4.3 test-first scaffolding: coercion planning ─────────────────── // Lock the one coercion-plan decision reachable via the existing public // surface — the optional wrap/flatten rule — before coercion planning moves to // `src/ir/conversions.zig`. The lowerXX / coerceToType / coerceOrErase / // buildProtocolErasure decisions are private + emission-bound, so their // CoercionPlan unit tests land with the extracted module in sub-step 2 (as the // generics/protocols plan tests landed with their modules); behavior is locked // here by the new `.ir` snapshots. test "conversions: optionalOfFlattened wraps once, flattening a nested optional" { const alloc = std.testing.allocator; var module = ir_mod.Module.init(alloc); defer module.deinit(); var l = Lowering.init(&module); const opt_s64 = module.types.optionalOf(.s64); // Wrap a non-optional: T -> ?T. try std.testing.expectEqual(opt_s64, l.optionalOfFlattened(.s64)); // Wrap an already-optional FLATTENS: ?T -> ?T (the coercion never builds ??T). try std.testing.expectEqual(opt_s64, l.optionalOfFlattened(opt_s64)); // Contrast: the plain wrap does NOT flatten — ?T -> ??T (distinct type). try std.testing.expect(module.types.optionalOf(opt_s64) != opt_s64); } test "lower: vectorLaneIndex maps swizzle components, colour aliases, rejects non-lanes" { // Positional swizzle components → lanes 0..3. try std.testing.expectEqual(@as(?u32, 0), Lowering.vectorLaneIndex("x")); try std.testing.expectEqual(@as(?u32, 1), Lowering.vectorLaneIndex("y")); try std.testing.expectEqual(@as(?u32, 2), Lowering.vectorLaneIndex("z")); try std.testing.expectEqual(@as(?u32, 3), Lowering.vectorLaneIndex("w")); // Colour aliases share the same lane indices. try std.testing.expectEqual(@as(?u32, 0), Lowering.vectorLaneIndex("r")); try std.testing.expectEqual(@as(?u32, 1), Lowering.vectorLaneIndex("g")); try std.testing.expectEqual(@as(?u32, 2), Lowering.vectorLaneIndex("b")); try std.testing.expectEqual(@as(?u32, 3), Lowering.vectorLaneIndex("a")); // Any non-lane field is rejected (null) so the read and write paths share // one rule — a non-lane store no longer falls through to an .unresolved // pointee that panics at LLVM emission (issue 0086). try std.testing.expectEqual(@as(?u32, null), Lowering.vectorLaneIndex("q")); try std.testing.expectEqual(@as(?u32, null), Lowering.vectorLaneIndex("xy")); try std.testing.expectEqual(@as(?u32, null), Lowering.vectorLaneIndex("len")); try std.testing.expectEqual(@as(?u32, null), Lowering.vectorLaneIndex("")); } test "lower: assigning to a missing struct field emits field-not-found, no panic (issue 0094)" { // Arena keeps the leak checker quiet — DiagnosticList.addFmt allocates // messages it never frees in deinit (mixed ownership with borrowed literals). var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); var module = ir_mod.Module.init(alloc); defer module.deinit(); var diags = errors.DiagnosticList.init(alloc, "", "test.sx"); defer diags.deinit(); // Register `Point :: struct { x: s64; }` so the struct literal resolves. const fields = [_]ir_mod.types.TypeInfo.StructInfo.Field{ .{ .name = module.types.internString("x"), .ty = .s64 }, }; _ = module.types.intern(.{ .@"struct" = .{ .name = module.types.internString("Point"), .fields = &fields } }); const span = ast.Span{ .start = 0, .end = 0 }; // main :: () { p := Point.{ x = 1 }; p.q = 2; } — `q` is not a field of Point. var x_val = Node{ .span = span, .data = .{ .int_literal = .{ .value = 1 } } }; const field_inits = [_]ast.StructFieldInit{.{ .name = "x", .value = &x_val }}; var lit = Node{ .span = span, .data = .{ .struct_literal = .{ .struct_name = "Point", .field_inits = &field_inits } } }; var decl = Node{ .span = span, .data = .{ .var_decl = .{ .name = "p", .name_span = span, .type_annotation = null, .value = &lit } } }; var p_ident = Node{ .span = span, .data = .{ .identifier = .{ .name = "p" } } }; var target = Node{ .span = span, .data = .{ .field_access = .{ .object = &p_ident, .field = "q" } } }; var rhs = Node{ .span = span, .data = .{ .int_literal = .{ .value = 2 } } }; var assign = Node{ .span = span, .data = .{ .assignment = .{ .target = &target, .op = .assign, .value = &rhs } } }; const stmts = [_]*Node{ &decl, &assign }; var body = Node{ .span = span, .data = .{ .block = .{ .stmts = &stmts } } }; const fd = ast.FnDecl{ .name = "main", .params = &.{}, .return_type = null, .body = &body }; var lowering = Lowering.init(&module); lowering.diagnostics = &diags; // Pre-fix this stored through a pointer-to-`.unresolved` that panicked at LLVM // emission; the fix bails with the read path's field-not-found diagnostic. lowering.lowerFunction(&fd, "main", false); var found = false; for (diags.items.items) |d| { if (d.level == .err and std.mem.indexOf(u8, d.message, "field 'q' not found on type 'Point'") != null) found = true; } try std.testing.expect(found); } test "lower: multi-assign to a missing struct field emits field-not-found, no corruption (issue 0094)" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); var module = ir_mod.Module.init(alloc); defer module.deinit(); var diags = errors.DiagnosticList.init(alloc, "", "test.sx"); defer diags.deinit(); // Register `Point :: struct { x: s64; }` so the struct literal resolves. const fields = [_]ir_mod.types.TypeInfo.StructInfo.Field{ .{ .name = module.types.internString("x"), .ty = .s64 }, }; _ = module.types.intern(.{ .@"struct" = .{ .name = module.types.internString("Point"), .fields = &fields } }); const span = ast.Span{ .start = 0, .end = 0 }; // main :: () { p := Point.{ x = 1 }; y := 0; p.r, y = 3, 4; } — `r` is not a field of Point. var x_val = Node{ .span = span, .data = .{ .int_literal = .{ .value = 1 } } }; const field_inits = [_]ast.StructFieldInit{.{ .name = "x", .value = &x_val }}; var lit = Node{ .span = span, .data = .{ .struct_literal = .{ .struct_name = "Point", .field_inits = &field_inits } } }; var decl = Node{ .span = span, .data = .{ .var_decl = .{ .name = "p", .name_span = span, .type_annotation = null, .value = &lit } } }; var y_init = Node{ .span = span, .data = .{ .int_literal = .{ .value = 0 } } }; var y_decl = Node{ .span = span, .data = .{ .var_decl = .{ .name = "y", .name_span = span, .type_annotation = null, .value = &y_init } } }; var p_ident = Node{ .span = span, .data = .{ .identifier = .{ .name = "p" } } }; var target0 = Node{ .span = span, .data = .{ .field_access = .{ .object = &p_ident, .field = "r" } } }; var target1 = Node{ .span = span, .data = .{ .identifier = .{ .name = "y" } } }; var v0 = Node{ .span = span, .data = .{ .int_literal = .{ .value = 3 } } }; var v1 = Node{ .span = span, .data = .{ .int_literal = .{ .value = 4 } } }; const targets = [_]*Node{ &target0, &target1 }; const values = [_]*Node{ &v0, &v1 }; var massign = Node{ .span = span, .data = .{ .multi_assign = .{ .targets = &targets, .values = &values } } }; const stmts = [_]*Node{ &decl, &y_decl, &massign }; var body = Node{ .span = span, .data = .{ .block = .{ .stmts = &stmts } } }; const fd = ast.FnDecl{ .name = "main", .params = &.{}, .return_type = null, .body = &body }; var lowering = Lowering.init(&module); lowering.diagnostics = &diags; // Pre-fix the struct-only loop defaulted field_idx 0 / field_ty .unresolved on // a miss, silently storing into field 0 (no diagnostic); the fix resolves the // target via the shared fieldLvaluePtr and bails with field-not-found. lowering.lowerFunction(&fd, "main", false); var found = false; for (diags.items.items) |d| { if (d.level == .err and std.mem.indexOf(u8, d.message, "field 'r' not found on type 'Point'") != null) found = true; } try std.testing.expect(found); } test "lower: shared resolver types a pointer-typed field GEP as *field_ty, not field_ty (issue 0094 clobber)" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); var module = ir_mod.Module.init(alloc); defer module.deinit(); const span = ast.Span{ .start = 0, .end = 0 }; // Register `S :: struct { p: *s64; }` — the field's own type is a pointer. const ptr_s64 = module.types.ptrTo(.s64); const fields = [_]ir_mod.types.TypeInfo.StructInfo.Field{ .{ .name = module.types.internString("p"), .ty = ptr_s64 }, }; _ = module.types.intern(.{ .@"struct" = .{ .name = module.types.internString("S"), .fields = &fields } }); // mutate :: (s: *S, q: *s64) { d := 0; s.p, d = q, 1; } // The multi-assign target routes `s.p` through the shared fieldLvaluePtr // resolver. Pre-fix that resolver typed the field GEP with the bare field // value type (`*s64`), so emitStore unwrapped one level to `s64` and // coerceArg's closure auto-promotion stored a 16-byte struct over the // 8-byte field, clobbering the neighbour. The resolver now types the GEP // `*(*s64)` so emitStore stops at the field's own pointer type. var s_pointee = Node{ .span = span, .data = .{ .type_expr = .{ .name = "S", .is_generic = false } } }; var s_ty = Node{ .span = span, .data = .{ .pointer_type_expr = .{ .pointee_type = &s_pointee } } }; var q_pointee = Node{ .span = span, .data = .{ .type_expr = .{ .name = "s64", .is_generic = false } } }; var q_ty = Node{ .span = span, .data = .{ .pointer_type_expr = .{ .pointee_type = &q_pointee } } }; var d_init = Node{ .span = span, .data = .{ .int_literal = .{ .value = 0 } } }; var d_decl = Node{ .span = span, .data = .{ .var_decl = .{ .name = "d", .name_span = span, .type_annotation = null, .value = &d_init } } }; var s_ident = Node{ .span = span, .data = .{ .identifier = .{ .name = "s" } } }; var target0 = Node{ .span = span, .data = .{ .field_access = .{ .object = &s_ident, .field = "p" } } }; var target1 = Node{ .span = span, .data = .{ .identifier = .{ .name = "d" } } }; var q_rhs = Node{ .span = span, .data = .{ .identifier = .{ .name = "q" } } }; var v1 = Node{ .span = span, .data = .{ .int_literal = .{ .value = 1 } } }; const targets = [_]*Node{ &target0, &target1 }; const values = [_]*Node{ &q_rhs, &v1 }; var massign = Node{ .span = span, .data = .{ .multi_assign = .{ .targets = &targets, .values = &values } } }; const stmts = [_]*Node{ &d_decl, &massign }; var body = Node{ .span = span, .data = .{ .block = .{ .stmts = &stmts } } }; const params = [_]ast.Param{ .{ .name = "s", .name_span = span, .type_expr = &s_ty }, .{ .name = "q", .name_span = span, .type_expr = &q_ty }, }; const fd = ast.FnDecl{ .name = "mutate", .params = ¶ms, .return_type = null, .body = &body }; var lowering = Lowering.init(&module); lowering.lowerFunction(&fd, "mutate", false); // The field-store GEP must be typed `*(*s64)`: its pointee is the field's // own type (`*s64`), not the field's pointee (`s64`). const func = module.getFunction(FuncId.fromIndex(0)); var found = false; for (func.blocks.items) |blk| { for (blk.insts.items) |inst| { if (inst.op == .struct_gep) { const info = module.types.get(inst.ty); try std.testing.expect(info == .pointer); try std.testing.expectEqual(ptr_s64, info.pointer.pointee); found = true; } } } try std.testing.expect(found); } test "lower: reflectionArgIsType accepts spelled types, rejects plain values (issue 0090)" { const alloc = std.testing.allocator; var module = ir_mod.Module.init(alloc); defer module.deinit(); var l = Lowering.init(&module); const span = ast.Span{ .start = 0, .end = 0 }; const ty_node = Node{ .span = span, .data = .{ .type_expr = .{ .name = "s64", .is_generic = false } } }; const int_node = Node{ .span = span, .data = .{ .int_literal = .{ .value = 6 } } }; const float_node = Node{ .span = span, .data = .{ .float_literal = .{ .value = 1.5 } } }; const bool_node = Node{ .span = span, .data = .{ .bool_literal = .{ .value = true } } }; // A spelled type is a type → the introspection builtins accept it. try std.testing.expect(l.reflectionArgIsType(&ty_node)); // Plain values are NOT types — these are exactly the arguments issue // 0090's strict `$T: Type` guard rejects, before a builtin could // reinterpret the value as a TypeId index (`type_is_unsigned(6)` → true) // or size its `typeof` (`size_of(true)` → 8). try std.testing.expect(!l.reflectionArgIsType(&int_node)); try std.testing.expect(!l.reflectionArgIsType(&float_node)); try std.testing.expect(!l.reflectionArgIsType(&bool_node)); } var g_lower_test_threaded: ?std.Io.Threaded = null; fn lowerTestIo() std.Io { if (g_lower_test_threaded == null) { g_lower_test_threaded = std.Io.Threaded.init(std.heap.page_allocator, .{}); } return g_lower_test_threaded.?.io(); } /// Count functions named `name` that carry a REAL body (promoted from the extern /// stub: not `is_extern`, at least one basic block). fn countRealBodies(module: *ir_mod.Module, name: []const u8) usize { var n: usize = 0; for (module.functions.items) |func| { if (!std.mem.eql(u8, module.types.getString(func.name), name)) continue; if (func.is_extern) continue; if (func.blocks.items.len == 0) continue; n += 1; } return n; } // fix-0102b: two flat-imported modules each author `greet`. The first-wins merge // keeps a.sx's author in the merged decl list (the WINNER) and drops b.sx's, // which `module_fns` still retains (0102a). `main` itself can't bare-call `greet` // — under fix-0102c two flat authors make that ambiguous — so it calls a.sx's // `use_greet` wrapper, whose own-author call to `greet` binds a.sx's winner. // BEFORE the identity-addressable pass, only the winner has a real body — the // shadowed author has no slot at all (the pre-fix symptom: one `greet`). // `lowerRetainedSameNameAuthors` declares the shadowed author its OWN same-name // FuncId and lowers its body there, so BOTH authors carry distinct, non-extern // bodies, and `resolveFuncByName` still returns the winner (the name-keyed slot). test "lower: shadowed same-name author gets its own FuncId + real body (fix-0102b)" { var arena = std.heap.ArenaAllocator.init(std.testing.allocator); defer arena.deinit(); const alloc = arena.allocator(); const io = lowerTestIo(); var tmp = std.testing.tmpDir(.{}); defer tmp.cleanup(); try tmp.dir.writeFile(io, .{ .sub_path = "a.sx", .data = "greet :: () -> s64 { 1 }\nuse_greet :: () -> s64 { greet() }\n" }); try tmp.dir.writeFile(io, .{ .sub_path = "b.sx", .data = "greet :: () -> s64 { 2 }\n" }); const main_src = \\#import "a.sx"; \\#import "b.sx"; \\main :: () -> s64 { use_greet() } \\ ; try tmp.dir.writeFile(io, .{ .sub_path = "main.sx", .data = main_src }); var dirbuf: [4096]u8 = undefined; const dirlen = try tmp.dir.realPath(io, &dirbuf); const absdir = dirbuf[0..dirlen]; const main_path = try std.fmt.allocPrint(alloc, "{s}/main.sx", .{absdir}); const main_bytes = try std.Io.Dir.readFileAlloc(.cwd(), io, main_path, alloc, .limited(1 << 20)); const main_source = try alloc.dupeZ(u8, main_bytes); var p = parser.Parser.init(alloc, main_source); const root = p.parse() catch return error.ParseFailed; var chain = std.StringHashMap(void).init(alloc); var cache = imports.ModuleCache.init(alloc); var import_graph = std.StringHashMap(std.StringHashMap(void)).init(alloc); var flat_import_graph = std.StringHashMap(std.StringHashMap(void)).init(alloc); const stdlib_paths = [_][]const u8{}; const mod = try imports.resolveImports( alloc, io, root, absdir, main_path, &chain, &cache, null, null, &stdlib_paths, &import_graph, &flat_import_graph, .{}, ); // Per-module visibility scopes + authored-function index, wired exactly as // `core.zig` does before `lowerRoot`. var module_scopes = std.StringHashMap(std.StringHashMap(void)).init(alloc); try module_scopes.put(main_path, mod.scope); var cache_it = cache.iterator(); while (cache_it.next()) |entry| { try module_scopes.put(entry.key_ptr.*, entry.value_ptr.scope); } var module_fns = imports.ModuleFns.init(alloc); try imports.buildModuleFns(alloc, main_path, mod, &cache, &module_fns); const resolved_root = try alloc.create(Node); resolved_root.* = .{ .span = root.span, .data = .{ .root = .{ .decls = mod.decls } } }; var module = ir_mod.Module.init(alloc); defer module.deinit(); var diagnostics = errors.DiagnosticList.init(alloc, main_source, main_path); var lowering = Lowering.init(&module); lowering.main_file = main_path; lowering.resolved_root = resolved_root; lowering.diagnostics = &diagnostics; lowering.program_index.module_scopes = &module_scopes; lowering.program_index.import_graph = &import_graph; lowering.program_index.flat_import_graph = &flat_import_graph; lowering.program_index.module_fns = &module_fns; lowering.lowerRoot(resolved_root); try std.testing.expect(!diagnostics.hasErrors()); // Pre-fix symptom: only the winner `greet` (a.sx) has a real body — lowered // because `main` calls it; the shadowed author (b.sx) was dropped entirely. try std.testing.expectEqual(@as(usize, 1), countRealBodies(&module, "greet")); // Identity-addressable pass: the shadowed author gets its OWN FuncId + body. lowering.lowerRetainedSameNameAuthors(); try std.testing.expect(!diagnostics.hasErrors()); // Both `greet` authors now carry distinct, real (non-extern) bodies, and the // two FuncIds are distinct. try std.testing.expectEqual(@as(usize, 2), countRealBodies(&module, "greet")); const name_id = module.types.internString("greet"); var first: ?FuncId = null; var second: ?FuncId = null; for (module.functions.items, 0..) |func, i| { if (func.name != name_id) continue; if (func.is_extern or func.blocks.items.len == 0) continue; if (first == null) first = FuncId.fromIndex(@intCast(i)) else second = FuncId.fromIndex(@intCast(i)); } try std.testing.expect(first != null and second != null); try std.testing.expect(first.? != second.?); // F1 (attempt-2): the identity map must be keyed by the STABLE AST field // pointer for BOTH same-name authors — the exact pointers `fn_ast_map` and // `module_fns` carry — not a per-iteration switch-capture temporary. If the // winner were keyed by `&fd` (the scanDecls bug), this lookup by the stable // `fn_ast_map` pointer would miss (null). fix-0102c routes calls through // exactly these pointers, so the round-trip must hold here. const winner_fd = lowering.program_index.fn_ast_map.get("greet").?; const winner_fid = lowering.fn_decl_fids.get(winner_fd); try std.testing.expect(winner_fid != null); // Round-trips to the first-wins winner FuncId (resolveFuncByName's pick). try std.testing.expectEqual(lowering.resolveFuncByName("greet").?, winner_fid.?); // The shadowed author's stable pointer lives in `module_fns`; find the one // that is NOT the winner and confirm IT round-trips to a DISTINCT FuncId. var shadow_fd: ?*const ast.FnDecl = null; var mf_it = module_fns.iterator(); while (mf_it.next()) |path_entry| { if (path_entry.value_ptr.get("greet")) |fd| { if (fd != winner_fd) shadow_fd = fd; } } try std.testing.expect(shadow_fd != null); const shadow_fid = lowering.fn_decl_fids.get(shadow_fd.?); try std.testing.expect(shadow_fid != null); try std.testing.expect(shadow_fid.? != winner_fid.?); // fix-0102c: THE bare-name resolver routes per caller file. `main` flat- // imports two `greet` authors and is its own author of neither → a bare // `greet()` from `main` is ambiguous. a.sx authors the WINNER, so its bare // `greet` resolves through the existing path (`.none`). b.sx authors the // SHADOW, so own-author-wins binds b.sx's distinct FuncId — not first-wins. const a_path = try std.fmt.allocPrint(alloc, "{s}/a.sx", .{absdir}); const b_path = try std.fmt.allocPrint(alloc, "{s}/b.sx", .{absdir}); try std.testing.expect(lowering.resolveBareCallee("greet", main_path) == .ambiguous); try std.testing.expect(lowering.resolveBareCallee("greet", a_path) == .none); switch (lowering.resolveBareCallee("greet", b_path)) { .func => |fid| try std.testing.expectEqual(shadow_fid.?, fid), else => return error.TestUnexpectedResult, } // A name no module authors (and no flat import provides) never routes. try std.testing.expect(lowering.resolveBareCallee("nonexistent", b_path) == .none); }