// 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; 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"); // 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: if/else generates basic blocks" { const alloc = std.testing.allocator; var module = ir_mod.Module.init(alloc); defer module.deinit(); // Build AST: test :: () -> s64 { if true { return 1; } else { return 2; } } 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 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 = &.{}, .return_type = ret_type, .body = fn_body, }; var lowering = Lowering.init(&module); lowering.lowerFunction(&fn_decl, "test_if"); // 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"); // 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.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.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.objcTypeEncodingFromSignature(.s64, &.{ .s64, .s64 }, null); defer alloc.free(e3); try std.testing.expectEqualStrings("q@:qq", e3); // BOOL return (s8): "c@:" const e4 = try lowering.objcTypeEncodingFromSignature(.s8, &.{}, null); defer alloc.free(e4); try std.testing.expectEqualStrings("c@:", e4); // Float/double: "f@:d" const e5 = try lowering.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.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.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.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.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.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.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.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.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.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.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.foreign_class_map.put("NSString", &ns_fcd); // Return *NSString, no args: "@@:" const e1 = try lowering.objcTypeEncodingFromSignature(ns_ptr, &.{}, null); defer alloc.free(e1); try std.testing.expectEqualStrings("@@:", e1); // Return *NSString, take *NSString: "@@:@" const e2 = try lowering.objcTypeEncodingFromSignature(ns_ptr, &.{ns_ptr}, null); defer alloc.free(e2); try std.testing.expectEqualStrings("@@:@", e2); }