Files
sx/src/ir/lower.test.zig
agra 7b98b3ae78 ffi M1.2 A.2a: objcDefinedStateStructType helper
Builds (and interns) the hidden sx-state struct type for an
sx-defined '#objc_class'. Layout:

    __<ClassName>State {
        user_field_0,
        user_field_1,
        ...
    }

This struct is what the runtime's '__sx_state' ivar points at —
separate from the Obj-C object itself, which stays opaque. The
sx method bodies will operate on '*__SxFooState' (after '*Self'
substitution in A.2b) so 'self.field' resolves to a plain struct
field access — A.3's 'free if types align' premise.

M1.2 A.5 will prepend '__sx_allocator: Allocator' so dealloc can
free through the per-instance allocator. Field-by-name access
stays correct across the future repositioning.

Methods / '#extends' / '#implements' members are ignored — only
'.field' contributes. Three unit tests pin: typical-field case,
empty-class case, mixed-member case.

Dead code at this commit — helper isn't called yet. A.2b (body
lowering with '*Self' substitution) wires it in. 170 example
tests + zig build test green.
2026-05-25 21:51:07 +03:00

428 lines
17 KiB
Zig

// 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);
}