The JNI/runtime-class path (Decision 5, Runtime* family). Coordinated across the hook boundary so the BuildOptions accessor + its registered hook string stay in sync: - src/: RuntimeClassDecl.foreign_path→runtime_path, splitForeignPath→splitRuntimePath, foreignPathToJavaName→runtimePathToJavaName, jni_main_foreign_paths→ jni_main_runtime_paths, hookJniMainForeignPathAt→hookJniMainRuntimePathAt, and the hook string 'BuildOptions.jni_main_foreign_path_at'→'…runtime_path_at'. - library/: build.sx accessor jni_main_foreign_path_at→jni_main_runtime_path_at + bundle.sx call sites + the local var → runtime_path + a comment. - specs.md: the accessor name + <foreign_path_with_dots> doc refs. - Regenerated 37 .ir snapshots: every program importing build declares the renamed @BuildOptions.jni_main_runtime_path_at hook stub — symbol-name change only (verified the .ir diff is ONLY this rename; reverted orthogonal empty-file normalization). Suite green (646 corpus / 444 unit, 0 failed).
1578 lines
74 KiB
Zig
1578 lines
74 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;
|
|
|
|
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: i64, b: i64) -> i64 { return a + b; }
|
|
const a_type = alloc.create(Node) catch unreachable;
|
|
a_type.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .type_expr = .{ .name = "i64", .is_generic = false } } };
|
|
const b_type = alloc.create(Node) catch unreachable;
|
|
b_type.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .type_expr = .{ .name = "i64", .is_generic = false } } };
|
|
const ret_type = alloc.create(Node) catch unreachable;
|
|
ret_type.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .type_expr = .{ .name = "i64", .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.i64, 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: i64, b: i64) -> i64 { 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 = "i64", .is_generic = false } } };
|
|
const b_type = alloc.create(Node) catch unreachable;
|
|
b_type.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .type_expr = .{ .name = "i64", .is_generic = false } } };
|
|
const ret_type = alloc.create(Node) catch unreachable;
|
|
ret_type.* = .{ .span = .{ .start = 0, .end = 0 }, .data = .{ .type_expr = .{ .name = "i64", .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) -> i64 { 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 = "i64", .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 i32, takes i32: -(int)add:(int)x → "i@:i"
|
|
const e2 = try lowering.objc().objcTypeEncodingFromSignature(.i32, &.{.i32}, null);
|
|
defer alloc.free(e2);
|
|
try std.testing.expectEqualStrings("i@:i", e2);
|
|
|
|
// i64 return, two i64 args: "q@:qq"
|
|
const e3 = try lowering.objc().objcTypeEncodingFromSignature(.i64, &.{ .i64, .i64 }, null);
|
|
defer alloc.free(e3);
|
|
try std.testing.expectEqualStrings("q@:qq", e3);
|
|
|
|
// BOOL return (i8): "c@:"
|
|
const e4 = try lowering.objc().objcTypeEncodingFromSignature(.i8, &.{}, 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);
|
|
|
|
// `[*]i32` (non-u8 many-pointer) → `^v`.
|
|
const i32_many = module.types.intern(.{ .many_pointer = .{ .element = .i32 } });
|
|
const e3 = try lowering.objc().objcTypeEncodingFromSignature(.void, &.{i32_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: i32; ticks: i64; } 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 = "i32", .is_generic = false } } };
|
|
const ticks_type = try alloc.create(Node);
|
|
defer alloc.destroy(ticks_type);
|
|
ticks_type.* = .{ .span = span, .data = .{ .type_expr = .{ .name = "i64", .is_generic = false } } };
|
|
|
|
const members = [_]ast.RuntimeClassMember{
|
|
.{ .field = .{ .name = "counter", .field_type = counter_type } },
|
|
.{ .field = .{ .name = "ticks", .field_type = ticks_type } },
|
|
};
|
|
const fcd = ast.RuntimeClassDecl{
|
|
.name = "SxFoo",
|
|
.runtime_path = "SxFoo",
|
|
.runtime = .objc_class,
|
|
.members = &members,
|
|
.is_extern = 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.i32, s.fields[0].ty);
|
|
try std.testing.expectEqualStrings("ticks", module.types.getString(s.fields[1].name));
|
|
try std.testing.expectEqual(TypeId.i64, 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.RuntimeClassDecl{
|
|
.name = "SxEmpty",
|
|
.runtime_path = "SxEmpty",
|
|
.runtime = .objc_class,
|
|
.members = &.{},
|
|
.is_extern = 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 = "i32", .is_generic = false } } };
|
|
|
|
const members = [_]ast.RuntimeClassMember{
|
|
.{ .extends = "NSObject" },
|
|
.{ .field = .{ .name = "counter", .field_type = counter_type } },
|
|
.{ .implements = "UIApplicationDelegate" },
|
|
};
|
|
const fcd = ast.RuntimeClassDecl{
|
|
.name = "SxMixed",
|
|
.runtime_path = "SxMixed",
|
|
.runtime = .objc_class,
|
|
.members = &members,
|
|
.is_extern = 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" {
|
|
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);
|
|
|
|
// 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.RuntimeClassDecl{
|
|
.name = "NSString",
|
|
.runtime_path = "NSString",
|
|
.runtime = .objc_class,
|
|
.members = &.{},
|
|
.is_extern = true,
|
|
.is_main = false,
|
|
};
|
|
try lowering.program_index.runtime_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" {
|
|
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);
|
|
|
|
// 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.RuntimeClassDecl{
|
|
.name = "NSString",
|
|
.runtime_path = "NSString",
|
|
.runtime = .objc_class,
|
|
.members = &.{},
|
|
.is_extern = true,
|
|
.is_main = false,
|
|
};
|
|
try lowering.program_index.runtime_class_map.put("NSString", &ns_fcd);
|
|
|
|
// `?i64 -> ?*NSString` collapses to `q -> @` at the Obj-C boundary.
|
|
const opt_i64 = module.types.optionalOf(.i64);
|
|
const opt_ns = module.types.optionalOf(ns_ptr);
|
|
const e1 = try lowering.objc().objcTypeEncodingFromSignature(opt_ns, &.{opt_i64}, 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, .i64 }, 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.RuntimeMethodDecl {
|
|
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" {
|
|
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);
|
|
|
|
// *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.RuntimeClassDecl{
|
|
.name = "NSString",
|
|
.runtime_path = "NSString",
|
|
.runtime = .objc_class,
|
|
.members = &.{},
|
|
.is_extern = true,
|
|
.is_main = false,
|
|
};
|
|
try lowering.program_index.runtime_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.RuntimeClassDecl{
|
|
.name = "NSCopying",
|
|
.runtime_path = "NSCopying",
|
|
.runtime = .objc_protocol,
|
|
.members = &.{},
|
|
.is_extern = true,
|
|
.is_main = false,
|
|
};
|
|
try lowering.program_index.runtime_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(.i32));
|
|
}
|
|
|
|
test "lower: objcPropertyKind defaults + explicit ARC modifiers" {
|
|
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);
|
|
|
|
// 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.RuntimeClassDecl{
|
|
.name = "NSString",
|
|
.runtime_path = "NSString",
|
|
.runtime = .objc_class,
|
|
.members = &.{},
|
|
.is_extern = true,
|
|
.is_main = false,
|
|
};
|
|
try lowering.program_index.runtime_class_map.put("NSString", &ns_fcd);
|
|
|
|
// Primitive field, no modifiers → assign (the non-object default).
|
|
const prim = ast.RuntimeFieldDecl{ .name = "count", .field_type = typeKeyword(alloc, "i32"), .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.RuntimeFieldDecl{ .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.RuntimeClassDecl{
|
|
.name = "NSCoding",
|
|
.runtime_path = "NSCoding",
|
|
.runtime = .objc_protocol,
|
|
.members = &.{},
|
|
.is_extern = true,
|
|
.is_main = false,
|
|
};
|
|
try lowering.program_index.runtime_class_map.put("NSCoding", &proto_fcd);
|
|
const proto_ty = typeKeyword(alloc, "*NSCoding");
|
|
defer alloc.destroy(proto_ty);
|
|
const proto_default = ast.RuntimeFieldDecl{ .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" {
|
|
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);
|
|
|
|
// 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)" {
|
|
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);
|
|
|
|
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.i64, lowering.inferExprType(then_div)); // then diverges → else (i64)
|
|
|
|
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.i64, lowering.inferExprType(else_div)); // then is i64
|
|
|
|
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_i64 = module.types.optionalOf(.i64);
|
|
// Wrap a non-optional: T -> ?T.
|
|
try std.testing.expectEqual(opt_i64, l.optionalOfFlattened(.i64));
|
|
// Wrap an already-optional FLATTENS: ?T -> ?T (the coercion never builds ??T).
|
|
try std.testing.expectEqual(opt_i64, l.optionalOfFlattened(opt_i64));
|
|
// Contrast: the plain wrap does NOT flatten — ?T -> ??T (distinct type).
|
|
try std.testing.expect(module.types.optionalOf(opt_i64) != opt_i64);
|
|
}
|
|
|
|
|
|
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.
|
|
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: i64; }` so the struct literal resolves.
|
|
const fields = [_]ir_mod.types.TypeInfo.StructInfo.Field{
|
|
.{ .name = module.types.internString("x"), .ty = .i64 },
|
|
};
|
|
_ = 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: i64; }` so the struct literal resolves.
|
|
const fields = [_]ir_mod.types.TypeInfo.StructInfo.Field{
|
|
.{ .name = module.types.internString("x"), .ty = .i64 },
|
|
};
|
|
_ = 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: *i64; }` — the field's own type is a pointer.
|
|
const ptr_i64 = module.types.ptrTo(.i64);
|
|
const fields = [_]ir_mod.types.TypeInfo.StructInfo.Field{
|
|
.{ .name = module.types.internString("p"), .ty = ptr_i64 },
|
|
};
|
|
_ = module.types.intern(.{ .@"struct" = .{ .name = module.types.internString("S"), .fields = &fields } });
|
|
|
|
// mutate :: (s: *S, q: *i64) { 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 (`*i64`), so emitStore unwrapped one level to `i64` 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
|
|
// `*(*i64)` 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 = "i64", .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 `*(*i64)`: its pointee is the field's
|
|
// own type (`*i64`), not the field's pointee (`i64`).
|
|
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_i64, 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 = "i64", .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;
|
|
}
|
|
|
|
// 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 the `module_decls` raw facts still retain (0102a). `main` itself can't bare-call `greet`
|
|
// — with two flat authors this is ambiguous; 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 :: () -> i64 { 1 }\nuse_greet :: () -> i64 { greet() }\n" });
|
|
try tmp.dir.writeFile(io, .{ .sub_path = "b.sx", .data = "greet :: () -> i64 { 2 }\n" });
|
|
const main_src =
|
|
\\#import "a.sx";
|
|
\\#import "b.sx";
|
|
\\main :: () -> i64 { 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);
|
|
}
|
|
// Phase A raw facts: both `selectPlainCallableAuthor` (Phase C) and
|
|
// `lowerRetainedSameNameAuthors` read function authors out of `module_decls`.
|
|
// Wired exactly as `core.zig` does.
|
|
var facts = try imports.buildImportFacts(alloc, main_path, mod, &cache);
|
|
|
|
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_decls = &facts.decls;
|
|
|
|
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
|
|
// the `module_decls` raw facts 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). Bare-call
|
|
// routing goes through exactly these pointers, so the round-trip must hold.
|
|
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_decls`; 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 md_it = facts.decls.iterator();
|
|
while (md_it.next()) |path_entry| {
|
|
if (path_entry.value_ptr.names.get("greet")) |ref| {
|
|
if (ref == .fn_decl and ref.fn_decl != winner_fd) shadow_fd = ref.fn_decl;
|
|
}
|
|
}
|
|
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.?);
|
|
|
|
// Phase C: THE bare-name selector routes per caller file over the
|
|
// Phase A author collector. `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 selects b.sx's
|
|
// author — its `*FnDecl` + source, NOT first-wins. The selector does NOT
|
|
// eagerly materialize: it returns the decl, and the FuncId still round-trips
|
|
// to the shadow slot via the identity map (`fn_decl_fids`).
|
|
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.selectPlainCallableAuthor("greet", main_path) == .ambiguous);
|
|
try std.testing.expect(lowering.selectPlainCallableAuthor("greet", a_path) == .none);
|
|
switch (lowering.selectPlainCallableAuthor("greet", b_path)) {
|
|
.func => |sf| {
|
|
try std.testing.expectEqual(shadow_fd.?, sf.decl);
|
|
try std.testing.expectEqualStrings(b_path, sf.source);
|
|
try std.testing.expect(sf.materialized == null);
|
|
try std.testing.expectEqual(shadow_fid.?, lowering.fn_decl_fids.get(sf.decl).?);
|
|
},
|
|
else => return error.TestUnexpectedResult,
|
|
}
|
|
// A name no module authors (and no flat import provides) never routes.
|
|
try std.testing.expect(lowering.selectPlainCallableAuthor("nonexistent", b_path) == .none);
|
|
}
|
|
|
|
// E0 (R5 §#4): the scan populates the source-keyed caches partitioned by the
|
|
// registering decl's source. Two namespaced modules each author the SAME alias
|
|
// name `Color` AND the SAME const name `K`; the scan recurses into each
|
|
// namespace's decls (per-source). After lowering, the by-source maps hold TWO
|
|
// distinct entries under the two source keys (not last-wins), while the legacy
|
|
// global maps stay single-keyed by name — the compat readers are unchanged.
|
|
test "lower: scan populates source-keyed caches per declaring source (E0)" {
|
|
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 = "Color :: *u8;\nK :: 5;\n" });
|
|
try tmp.dir.writeFile(io, .{ .sub_path = "b.sx", .data = "Color :: *u16;\nK :: 7;\n" });
|
|
const main_src =
|
|
\\na :: #import "a.sx";
|
|
\\nb :: #import "b.sx";
|
|
\\main :: () -> i32 { 0 }
|
|
\\
|
|
;
|
|
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,
|
|
.{},
|
|
);
|
|
|
|
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 facts = try imports.buildImportFacts(alloc, main_path, mod, &cache);
|
|
|
|
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_decls = &facts.decls;
|
|
|
|
lowering.lowerRoot(resolved_root);
|
|
try std.testing.expect(!diagnostics.hasErrors());
|
|
|
|
const a_path = try std.fmt.allocPrint(alloc, "{s}/a.sx", .{absdir});
|
|
const b_path = try std.fmt.allocPrint(alloc, "{s}/b.sx", .{absdir});
|
|
const idx = &lowering.program_index;
|
|
|
|
// SAME alias name `Color` lands a DISTINCT entry under each source key.
|
|
const color_a = idx.type_aliases_by_source.get(a_path).?.get("Color").?;
|
|
const color_b = idx.type_aliases_by_source.get(b_path).?.get("Color").?;
|
|
try std.testing.expect(color_a != color_b); // *u8 vs *u16 — source-partitioned
|
|
|
|
// SAME const name `K` lands a DISTINCT entry (distinct value node) per source.
|
|
const k_a = idx.module_consts_by_source.get(a_path).?.get("K").?;
|
|
const k_b = idx.module_consts_by_source.get(b_path).?.get("K").?;
|
|
try std.testing.expect(k_a.value != k_b.value);
|
|
|
|
// Compat readers: the legacy global maps stay keyed by NAME alone — a
|
|
// hashmap key holds exactly one value, so a same-name author is last-wins
|
|
// there (one entry for `Color` / `K`), unchanged by the by-source writes.
|
|
// The single global `Color` is one of the two source-keyed authors (not a
|
|
// merged/duplicated value).
|
|
const global_color = idx.type_alias_map.get("Color").?;
|
|
try std.testing.expect(global_color == color_a or global_color == color_b);
|
|
const global_k = idx.module_const_map.get("K").?;
|
|
try std.testing.expect(global_k.value == k_a.value or global_k.value == k_b.value);
|
|
}
|
|
|