- Add Complex struct lookup for multi-instruction return type bodies in cross-module inline calls (e.g. mulc3 returning Complex(f64)) - Add memoization for comptime type function calls to avoid duplicate block pre-allocation - Add comptime float coercion (comptime_float → concrete float) - Add tryResolveInst for graceful handling of unresolved references - Classify dbg_inline_block and block as ref-bearing in airDataRefSlots - Enable muldc3, mulhc3, mulsc3, mulxc3 corpus tests (all pass) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1345 lines
46 KiB
Zig
1345 lines
46 KiB
Zig
const std = @import("std");
|
|
|
|
// Import C types including sema.h (which transitively includes air.h, intern_pool.h, etc.)
|
|
// Also include astgen.h so we have the full pipeline in one namespace.
|
|
pub const c = @cImport({
|
|
@cInclude("astgen.h");
|
|
@cInclude("sema.h");
|
|
@cInclude("dump.h");
|
|
});
|
|
|
|
// Helper to convert C #define integer constants (c_int) to u32 for comparison
|
|
// with uint32_t fields (InternPoolIndex, etc.).
|
|
fn idx(val: c_int) u32 {
|
|
return @bitCast(val);
|
|
}
|
|
|
|
// Helper to convert C enum values (c_uint) to the expected tag type for comparison.
|
|
fn tag(val: c_uint) c_uint {
|
|
return val;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// InternPool unit tests
|
|
// ---------------------------------------------------------------------------
|
|
|
|
test "intern_pool: init and pre-interned types" {
|
|
var ip = c.ipInit();
|
|
defer c.ipDeinit(&ip);
|
|
|
|
// Verify pre-interned count
|
|
try std.testing.expectEqual(@as(u32, 124), ip.items_len);
|
|
|
|
// Verify some key type indices
|
|
const void_key = c.ipIndexToKey(&ip, idx(c.IP_INDEX_VOID_TYPE));
|
|
try std.testing.expectEqual(tag(c.IP_KEY_SIMPLE_TYPE), void_key.tag);
|
|
try std.testing.expectEqual(tag(c.SIMPLE_TYPE_VOID), void_key.data.simple_type);
|
|
|
|
const u32_key = c.ipIndexToKey(&ip, idx(c.IP_INDEX_U32_TYPE));
|
|
try std.testing.expectEqual(tag(c.IP_KEY_INT_TYPE), u32_key.tag);
|
|
try std.testing.expectEqual(@as(u16, 32), u32_key.data.int_type.bits);
|
|
try std.testing.expectEqual(@as(u8, 0), u32_key.data.int_type.signedness); // unsigned
|
|
|
|
const i32_key = c.ipIndexToKey(&ip, idx(c.IP_INDEX_I32_TYPE));
|
|
try std.testing.expectEqual(tag(c.IP_KEY_INT_TYPE), i32_key.tag);
|
|
try std.testing.expectEqual(@as(u16, 32), i32_key.data.int_type.bits);
|
|
try std.testing.expectEqual(@as(u8, 1), i32_key.data.int_type.signedness); // signed
|
|
|
|
const bool_key = c.ipIndexToKey(&ip, idx(c.IP_INDEX_BOOL_TYPE));
|
|
try std.testing.expectEqual(tag(c.IP_KEY_SIMPLE_TYPE), bool_key.tag);
|
|
try std.testing.expectEqual(tag(c.SIMPLE_TYPE_BOOL), bool_key.data.simple_type);
|
|
}
|
|
|
|
test "intern_pool: pre-interned values" {
|
|
var ip = c.ipInit();
|
|
defer c.ipDeinit(&ip);
|
|
|
|
// Check void value
|
|
const void_val = c.ipIndexToKey(&ip, idx(c.IP_INDEX_VOID_VALUE));
|
|
try std.testing.expectEqual(tag(c.IP_KEY_SIMPLE_VALUE), void_val.tag);
|
|
try std.testing.expectEqual(tag(c.SIMPLE_VALUE_VOID), void_val.data.simple_value);
|
|
|
|
// Check bool true/false
|
|
const true_val = c.ipIndexToKey(&ip, idx(c.IP_INDEX_BOOL_TRUE));
|
|
try std.testing.expectEqual(tag(c.IP_KEY_SIMPLE_VALUE), true_val.tag);
|
|
try std.testing.expectEqual(tag(c.SIMPLE_VALUE_TRUE), true_val.data.simple_value);
|
|
|
|
const false_val = c.ipIndexToKey(&ip, idx(c.IP_INDEX_BOOL_FALSE));
|
|
try std.testing.expectEqual(tag(c.IP_KEY_SIMPLE_VALUE), false_val.tag);
|
|
try std.testing.expectEqual(tag(c.SIMPLE_VALUE_FALSE), false_val.data.simple_value);
|
|
|
|
// Check zero
|
|
const zero_key = c.ipIndexToKey(&ip, idx(c.IP_INDEX_ZERO));
|
|
try std.testing.expectEqual(tag(c.IP_KEY_INT), zero_key.tag);
|
|
}
|
|
|
|
test "intern_pool: ipTypeOf" {
|
|
var ip = c.ipInit();
|
|
defer c.ipDeinit(&ip);
|
|
|
|
// Types have type 'type'
|
|
try std.testing.expectEqual(idx(c.IP_INDEX_TYPE_TYPE), c.ipTypeOf(&ip, idx(c.IP_INDEX_VOID_TYPE)));
|
|
try std.testing.expectEqual(idx(c.IP_INDEX_TYPE_TYPE), c.ipTypeOf(&ip, idx(c.IP_INDEX_U32_TYPE)));
|
|
try std.testing.expectEqual(idx(c.IP_INDEX_TYPE_TYPE), c.ipTypeOf(&ip, idx(c.IP_INDEX_BOOL_TYPE)));
|
|
|
|
// Values have their respective types
|
|
try std.testing.expectEqual(idx(c.IP_INDEX_VOID_TYPE), c.ipTypeOf(&ip, idx(c.IP_INDEX_VOID_VALUE)));
|
|
try std.testing.expectEqual(idx(c.IP_INDEX_BOOL_TYPE), c.ipTypeOf(&ip, idx(c.IP_INDEX_BOOL_TRUE)));
|
|
try std.testing.expectEqual(idx(c.IP_INDEX_BOOL_TYPE), c.ipTypeOf(&ip, idx(c.IP_INDEX_BOOL_FALSE)));
|
|
}
|
|
|
|
test "intern_pool: ipIntern deduplication" {
|
|
var ip = c.ipInit();
|
|
defer c.ipDeinit(&ip);
|
|
|
|
// Interning an existing key should return the same index
|
|
var void_key: c.InternPoolKey = undefined;
|
|
@memset(std.mem.asBytes(&void_key), 0);
|
|
void_key.tag = c.IP_KEY_SIMPLE_TYPE;
|
|
void_key.data.simple_type = c.SIMPLE_TYPE_VOID;
|
|
|
|
const result = c.ipIntern(&ip, void_key);
|
|
try std.testing.expectEqual(idx(c.IP_INDEX_VOID_TYPE), result);
|
|
|
|
// Items count shouldn't increase for duplicate
|
|
try std.testing.expectEqual(@as(u32, 124), ip.items_len);
|
|
}
|
|
|
|
test "intern_pool: ipIntern new key" {
|
|
var ip = c.ipInit();
|
|
defer c.ipDeinit(&ip);
|
|
|
|
// Intern a new array type
|
|
var arr_key: c.InternPoolKey = undefined;
|
|
@memset(std.mem.asBytes(&arr_key), 0);
|
|
arr_key.tag = c.IP_KEY_ARRAY_TYPE;
|
|
arr_key.data.array_type = .{
|
|
.len = 10,
|
|
.child = idx(c.IP_INDEX_U8_TYPE),
|
|
.sentinel = c.IP_INDEX_NONE,
|
|
};
|
|
|
|
const idx1 = c.ipIntern(&ip, arr_key);
|
|
try std.testing.expect(idx1 >= idx(c.IP_INDEX_PREINTERN_COUNT));
|
|
try std.testing.expectEqual(@as(u32, 125), ip.items_len);
|
|
|
|
// Re-interning should return same index
|
|
const idx2 = c.ipIntern(&ip, arr_key);
|
|
try std.testing.expectEqual(idx1, idx2);
|
|
try std.testing.expectEqual(@as(u32, 125), ip.items_len);
|
|
}
|
|
|
|
test "intern_pool: vector types" {
|
|
var ip = c.ipInit();
|
|
defer c.ipDeinit(&ip);
|
|
|
|
// Verify vector_8_i8 at index 52
|
|
const v8i8 = c.ipIndexToKey(&ip, idx(c.IP_INDEX_VECTOR_8_I8_TYPE));
|
|
try std.testing.expectEqual(tag(c.IP_KEY_VECTOR_TYPE), v8i8.tag);
|
|
try std.testing.expectEqual(@as(u32, 8), v8i8.data.vector_type.len);
|
|
try std.testing.expectEqual(idx(c.IP_INDEX_I8_TYPE), v8i8.data.vector_type.child);
|
|
|
|
// Verify vector_4_f32 at index 93
|
|
const v4f32 = c.ipIndexToKey(&ip, idx(c.IP_INDEX_VECTOR_4_F32_TYPE));
|
|
try std.testing.expectEqual(tag(c.IP_KEY_VECTOR_TYPE), v4f32.tag);
|
|
try std.testing.expectEqual(@as(u32, 4), v4f32.data.vector_type.len);
|
|
try std.testing.expectEqual(idx(c.IP_INDEX_F32_TYPE), v4f32.data.vector_type.child);
|
|
}
|
|
|
|
test "intern_pool: pointer types" {
|
|
var ip = c.ipInit();
|
|
defer c.ipDeinit(&ip);
|
|
|
|
// ptr_usize (index 45): *usize
|
|
const ptr_usize = c.ipIndexToKey(&ip, idx(c.IP_INDEX_PTR_USIZE_TYPE));
|
|
try std.testing.expectEqual(tag(c.IP_KEY_PTR_TYPE), ptr_usize.tag);
|
|
try std.testing.expectEqual(idx(c.IP_INDEX_USIZE_TYPE), ptr_usize.data.ptr_type.child);
|
|
|
|
// manyptr_const_u8 (index 48): [*]const u8
|
|
const manyptr = c.ipIndexToKey(&ip, idx(c.IP_INDEX_MANYPTR_CONST_U8_TYPE));
|
|
try std.testing.expectEqual(tag(c.IP_KEY_PTR_TYPE), manyptr.tag);
|
|
try std.testing.expectEqual(idx(c.IP_INDEX_U8_TYPE), manyptr.data.ptr_type.child);
|
|
try std.testing.expect((manyptr.data.ptr_type.flags & idx(c.PTR_FLAGS_SIZE_MASK)) == idx(c.PTR_FLAGS_SIZE_MANY));
|
|
try std.testing.expect((manyptr.data.ptr_type.flags & idx(c.PTR_FLAGS_IS_CONST)) != 0);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Sema smoke tests (using C sema pipeline directly)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const SemaCheckResult = struct {
|
|
c_ip: c.InternPool,
|
|
c_sema: c.Sema,
|
|
c_func_air_list: c.SemaFuncAirList,
|
|
|
|
fn deinit(self: *SemaCheckResult) void {
|
|
c.semaFuncAirListDeinit(&self.c_func_air_list);
|
|
c.semaDeinit(&self.c_sema);
|
|
c.ipDeinit(&self.c_ip);
|
|
}
|
|
};
|
|
|
|
fn semaCheck(source: [:0]const u8) !SemaCheckResult {
|
|
var c_ast = c.astParse(source.ptr, @intCast(source.len));
|
|
defer c.astDeinit(&c_ast);
|
|
var c_zir = c.astGen(&c_ast);
|
|
defer c.zirDeinit(&c_zir);
|
|
var result: SemaCheckResult = undefined;
|
|
result.c_ip = c.ipInit();
|
|
c.semaInit(&result.c_sema, &result.c_ip, c_zir);
|
|
result.c_func_air_list = c.semaAnalyze(&result.c_sema);
|
|
return result;
|
|
}
|
|
|
|
test "sema: empty source smoke test" {
|
|
var result = try semaCheck("");
|
|
defer result.deinit();
|
|
|
|
// semaAnalyze frees AIR arrays and nulls out sema's pointers.
|
|
try std.testing.expect(result.c_sema.air_inst_tags == null);
|
|
try std.testing.expect(result.c_sema.air_inst_datas == null);
|
|
try std.testing.expect(result.c_sema.air_extra == null);
|
|
|
|
// No functions analyzed yet, so func_airs should be empty.
|
|
try std.testing.expectEqual(@as(u32, 0), result.c_func_air_list.len);
|
|
}
|
|
|
|
test "sema: const x = 0 smoke test" {
|
|
var result = try semaCheck("const x = 0;");
|
|
defer result.deinit();
|
|
|
|
// No functions, so func_airs should be empty.
|
|
try std.testing.expectEqual(@as(u32, 0), result.c_func_air_list.len);
|
|
}
|
|
|
|
test "sema: function decl smoke test" {
|
|
var result = try semaCheck("fn foo() void {}");
|
|
defer result.deinit();
|
|
|
|
// Non-export functions are not analyzed, so func_airs should be empty.
|
|
try std.testing.expectEqual(@as(u32, 0), result.c_func_air_list.len);
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Air raw comparison: C vs Zig
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const ZigCompileAirResult = extern struct {
|
|
items: ?[*]c.SemaFuncAir,
|
|
len: u32,
|
|
callback_count: u32,
|
|
};
|
|
extern fn zig_compile_air([*:0]const u8, ?[*:0]const u8, [*]u8) ZigCompileAirResult;
|
|
extern fn zig_compile_air_free(*ZigCompileAirResult) void;
|
|
|
|
pub fn airCompareFromSource(source: [:0]const u8, c_func_air_list: c.SemaFuncAirList) !void {
|
|
var tmp = std.testing.tmpDir(.{});
|
|
defer tmp.cleanup();
|
|
const f = tmp.dir.createFile("t.zig", .{}) catch return error.TmpFileCreate;
|
|
f.writeAll(source) catch {
|
|
f.close();
|
|
return error.TmpFileWrite;
|
|
};
|
|
f.close();
|
|
var path_buf: [std.fs.max_path_bytes:0]u8 = undefined;
|
|
const tmp_path = tmp.dir.realpathZ("t.zig", &path_buf) catch return error.TmpFileCreate;
|
|
path_buf[tmp_path.len] = 0;
|
|
return airCompare(@ptrCast(tmp_path.ptr), null, c_func_air_list);
|
|
}
|
|
|
|
pub fn airCompare(
|
|
src_path: [*:0]const u8,
|
|
module_root: ?[*:0]const u8,
|
|
c_func_air_list: c.SemaFuncAirList,
|
|
) !void {
|
|
var err_buf: [c.ZIG_COMPILE_ERR_BUF_SIZE]u8 = .{0} ** c.ZIG_COMPILE_ERR_BUF_SIZE;
|
|
var zig_result = zig_compile_air(src_path, module_root, &err_buf);
|
|
defer zig_compile_air_free(&zig_result);
|
|
|
|
if (err_buf[0] != 0) {
|
|
std.debug.print("zig_compile_air error: {s}\n", .{std.mem.sliceTo(&err_buf, 0)});
|
|
return error.ZigCompileError;
|
|
}
|
|
|
|
// Canary: if C sema found functions, the Zig callback must have fired.
|
|
if (c_func_air_list.len > 0 and zig_result.callback_count == 0) {
|
|
std.debug.print("Canary: C sema produced {d} functions but Zig callback never fired\n", .{c_func_air_list.len});
|
|
return error.AirCallbackNotFired;
|
|
}
|
|
|
|
const zig_funcs = if (zig_result.items) |items| items[0..zig_result.len] else &[_]c.SemaFuncAir{};
|
|
const c_funcs_ptr: ?[*]const c.SemaFuncAir = @ptrCast(c_func_air_list.items);
|
|
const c_funcs = if (c_funcs_ptr) |items| items[0..c_func_air_list.len] else &[_]c.SemaFuncAir{};
|
|
|
|
// Compare functions that exist in both C and Zig output.
|
|
// The C sema may produce fewer functions (e.g. missing import
|
|
// resolution), so we iterate C functions and look them up in Zig.
|
|
for (c_funcs) |*cf| {
|
|
const c_name = if (cf.name) |n| std.mem.span(n) else "";
|
|
const zf = airFindByName(zig_funcs, c_name) orelse {
|
|
std.debug.print("C function '{s}' not found in Zig output\n", .{c_name});
|
|
return error.AirMismatch;
|
|
};
|
|
try airCompareOne(c_name, &zf.air, &cf.air);
|
|
}
|
|
}
|
|
|
|
fn cNameSpan(name: [*c]u8) []const u8 {
|
|
const opt: ?[*:0]const u8 = @ptrCast(name);
|
|
return if (opt) |n| std.mem.span(n) else "";
|
|
}
|
|
|
|
/// Strip module prefix from FQN: "module.name" -> "name".
|
|
/// Returns the full string if no '.' is found.
|
|
fn stripModulePrefix(fqn: []const u8) []const u8 {
|
|
return if (std.mem.lastIndexOfScalar(u8, fqn, '.')) |dot|
|
|
fqn[dot + 1 ..]
|
|
else
|
|
fqn;
|
|
}
|
|
|
|
fn airFindByName(funcs: []const c.SemaFuncAir, name: []const u8) ?*const c.SemaFuncAir {
|
|
const bare_name = stripModulePrefix(name);
|
|
for (funcs) |*f| {
|
|
const c_name = cNameSpan(f.name);
|
|
if (std.mem.eql(u8, bare_name, stripModulePrefix(c_name))) return f;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
fn cToOpt(comptime T: type, ptr: [*c]T) ?[*]const T {
|
|
return if (ptr == null) null else @ptrCast(ptr);
|
|
}
|
|
|
|
fn airTagNameSlice(tag_val: u8) []const u8 {
|
|
return std.mem.span(air_tag_name(tag_val));
|
|
}
|
|
extern fn air_tag_name(tag: u8) [*:0]const u8;
|
|
|
|
fn refKindStr(ref: u32) []const u8 {
|
|
if (ref == 0xFFFFFFFF) return "none";
|
|
if ((ref >> 31) != 0) return "inst";
|
|
return "ip";
|
|
}
|
|
|
|
/// Canonicalize an AIR Ref for comparison. Inst refs (bit 31 set)
|
|
/// and the special NONE sentinel are returned as-is. IP refs (bit 31
|
|
/// clear) are assigned a sequential canonical ID via the map, in
|
|
/// order of first appearance, so that two AIR streams that intern
|
|
/// the same values in the same order produce identical canonical IDs
|
|
/// even when the raw InternPool indices differ.
|
|
fn canonicalizeRef(
|
|
ref: u32,
|
|
map: *std.AutoHashMap(u32, u32),
|
|
next_id: *u32,
|
|
) u32 {
|
|
if (ref == 0xFFFFFFFF) return ref; // AIR_REF_NONE
|
|
if ((ref >> 31) != 0) return ref; // Inst ref — keep as-is
|
|
// IP ref — canonicalize.
|
|
const gop = map.getOrPut(ref) catch unreachable;
|
|
if (!gop.found_existing) {
|
|
gop.value_ptr.* = next_id.*;
|
|
next_id.* += 1;
|
|
}
|
|
return gop.value_ptr.*;
|
|
}
|
|
|
|
/// Number of meaningful 4-byte slots in AirInstData for a given tag.
|
|
/// Air.Inst.Data is an 8-byte union; variants smaller than 8 bytes
|
|
/// (un_op, no_op, ty, repeat) leave padding bytes uninitialised.
|
|
/// Only this many slots should be compared.
|
|
fn airInstNumSlots(tag_val: u8) usize {
|
|
return switch (tag_val) {
|
|
// no_op: 0 meaningful bytes
|
|
c.AIR_INST_RET_ADDR, c.AIR_INST_FRAME_ADDR, c.AIR_INST_TRAP, c.AIR_INST_UNREACH, c.AIR_INST_BREAKPOINT => 0,
|
|
// un_op: 4 meaningful bytes (1 slot)
|
|
c.AIR_INST_SQRT,
|
|
c.AIR_INST_SIN,
|
|
c.AIR_INST_COS,
|
|
c.AIR_INST_TAN,
|
|
c.AIR_INST_EXP,
|
|
c.AIR_INST_EXP2,
|
|
c.AIR_INST_LOG,
|
|
c.AIR_INST_LOG2,
|
|
c.AIR_INST_LOG10,
|
|
c.AIR_INST_FLOOR,
|
|
c.AIR_INST_CEIL,
|
|
c.AIR_INST_ROUND,
|
|
c.AIR_INST_TRUNC_FLOAT,
|
|
c.AIR_INST_NEG,
|
|
c.AIR_INST_NEG_OPTIMIZED,
|
|
c.AIR_INST_IS_NULL,
|
|
c.AIR_INST_IS_NON_NULL,
|
|
c.AIR_INST_IS_NULL_PTR,
|
|
c.AIR_INST_IS_NON_NULL_PTR,
|
|
c.AIR_INST_IS_ERR,
|
|
c.AIR_INST_IS_NON_ERR,
|
|
c.AIR_INST_IS_ERR_PTR,
|
|
c.AIR_INST_IS_NON_ERR_PTR,
|
|
c.AIR_INST_RET,
|
|
c.AIR_INST_RET_SAFE,
|
|
c.AIR_INST_RET_LOAD,
|
|
c.AIR_INST_IS_NAMED_ENUM_VALUE,
|
|
c.AIR_INST_TAG_NAME,
|
|
c.AIR_INST_ERROR_NAME,
|
|
c.AIR_INST_CMP_LT_ERRORS_LEN,
|
|
c.AIR_INST_C_VA_END,
|
|
=> 1,
|
|
// ty: 4 meaningful bytes (1 slot)
|
|
c.AIR_INST_ALLOC, c.AIR_INST_RET_PTR, c.AIR_INST_C_VA_START => 1,
|
|
// repeat: 4 meaningful bytes (1 slot)
|
|
c.AIR_INST_REPEAT => 1,
|
|
// All other variants use the full 8 bytes (2 slots).
|
|
else => 2,
|
|
};
|
|
}
|
|
|
|
/// Return which of the two 4-byte slots in Air.Inst.Data are Refs
|
|
/// for a given AIR instruction tag. [0] = bytes [0:4], [1] = bytes
|
|
/// [4:8]. Non-ref slots (line/column, payload indices, padding)
|
|
/// are compared directly.
|
|
fn airDataRefSlots(tag_val: u8) [2]bool {
|
|
return switch (tag_val) {
|
|
// no_op: no meaningful data
|
|
c.AIR_INST_RET_ADDR, c.AIR_INST_FRAME_ADDR, c.AIR_INST_TRAP, c.AIR_INST_UNREACH, c.AIR_INST_BREAKPOINT => .{ false, false },
|
|
// dbg_stmt: line(u32) + column(u32)
|
|
c.AIR_INST_DBG_STMT, c.AIR_INST_DBG_EMPTY_STMT => .{ false, false },
|
|
// pl_op: operand(Ref) + payload(u32)
|
|
c.AIR_INST_DBG_VAR_PTR,
|
|
c.AIR_INST_DBG_VAR_VAL,
|
|
c.AIR_INST_DBG_ARG_INLINE,
|
|
c.AIR_INST_CALL,
|
|
c.AIR_INST_CALL_ALWAYS_TAIL,
|
|
c.AIR_INST_CALL_NEVER_TAIL,
|
|
c.AIR_INST_CALL_NEVER_INLINE,
|
|
=> .{ true, false },
|
|
// un_op: operand(Ref) + pad
|
|
c.AIR_INST_RET,
|
|
c.AIR_INST_RET_SAFE,
|
|
c.AIR_INST_RET_LOAD,
|
|
c.AIR_INST_NEG,
|
|
c.AIR_INST_NEG_OPTIMIZED,
|
|
c.AIR_INST_IS_NULL,
|
|
c.AIR_INST_IS_NON_NULL,
|
|
c.AIR_INST_IS_NULL_PTR,
|
|
c.AIR_INST_IS_NON_NULL_PTR,
|
|
c.AIR_INST_IS_ERR,
|
|
c.AIR_INST_IS_NON_ERR,
|
|
c.AIR_INST_IS_ERR_PTR,
|
|
c.AIR_INST_IS_NON_ERR_PTR,
|
|
c.AIR_INST_SQRT,
|
|
c.AIR_INST_SIN,
|
|
c.AIR_INST_COS,
|
|
c.AIR_INST_TAN,
|
|
c.AIR_INST_EXP,
|
|
c.AIR_INST_EXP2,
|
|
c.AIR_INST_LOG,
|
|
c.AIR_INST_LOG2,
|
|
c.AIR_INST_LOG10,
|
|
c.AIR_INST_FLOOR,
|
|
c.AIR_INST_CEIL,
|
|
c.AIR_INST_ROUND,
|
|
c.AIR_INST_TRUNC_FLOAT,
|
|
c.AIR_INST_IS_NAMED_ENUM_VALUE,
|
|
c.AIR_INST_TAG_NAME,
|
|
c.AIR_INST_ERROR_NAME,
|
|
c.AIR_INST_CMP_LT_ERRORS_LEN,
|
|
c.AIR_INST_C_VA_END,
|
|
=> .{ true, false },
|
|
// ty: type(Ref) + pad
|
|
c.AIR_INST_ALLOC,
|
|
c.AIR_INST_RET_PTR,
|
|
c.AIR_INST_C_VA_START,
|
|
=> .{ true, false },
|
|
// ty_pl: type(Ref) + payload(u32)
|
|
c.AIR_INST_STRUCT_FIELD_VAL,
|
|
c.AIR_INST_STRUCT_FIELD_PTR,
|
|
c.AIR_INST_DBG_INLINE_BLOCK,
|
|
c.AIR_INST_BLOCK,
|
|
=> .{ true, false },
|
|
// bin_op: lhs(Ref) + rhs(Ref)
|
|
c.AIR_INST_ADD,
|
|
c.AIR_INST_ADD_SAFE,
|
|
c.AIR_INST_ADD_OPTIMIZED,
|
|
c.AIR_INST_ADD_WRAP,
|
|
c.AIR_INST_SUB,
|
|
c.AIR_INST_SUB_SAFE,
|
|
c.AIR_INST_SUB_OPTIMIZED,
|
|
c.AIR_INST_SUB_WRAP,
|
|
c.AIR_INST_MUL,
|
|
c.AIR_INST_MUL_SAFE,
|
|
c.AIR_INST_MUL_OPTIMIZED,
|
|
c.AIR_INST_MUL_WRAP,
|
|
c.AIR_INST_BOOL_AND,
|
|
c.AIR_INST_BOOL_OR,
|
|
c.AIR_INST_STORE,
|
|
c.AIR_INST_STORE_SAFE,
|
|
c.AIR_INST_BIT_AND,
|
|
c.AIR_INST_BIT_OR,
|
|
c.AIR_INST_XOR,
|
|
c.AIR_INST_SHL,
|
|
c.AIR_INST_SHL_EXACT,
|
|
c.AIR_INST_SHL_SAT,
|
|
c.AIR_INST_SHR,
|
|
c.AIR_INST_SHR_EXACT,
|
|
c.AIR_INST_CMP_LT,
|
|
c.AIR_INST_CMP_LTE,
|
|
c.AIR_INST_CMP_EQ,
|
|
c.AIR_INST_CMP_GTE,
|
|
c.AIR_INST_CMP_GT,
|
|
c.AIR_INST_CMP_NEQ,
|
|
=> .{ true, true },
|
|
// ty_op: type(Ref) + operand(Ref)
|
|
c.AIR_INST_BITCAST,
|
|
c.AIR_INST_INTCAST,
|
|
c.AIR_INST_INTCAST_SAFE,
|
|
c.AIR_INST_TRUNC,
|
|
c.AIR_INST_FPTRUNC,
|
|
c.AIR_INST_FPEXT,
|
|
c.AIR_INST_OPTIONAL_PAYLOAD,
|
|
c.AIR_INST_OPTIONAL_PAYLOAD_PTR,
|
|
c.AIR_INST_WRAP_OPTIONAL,
|
|
c.AIR_INST_UNWRAP_ERRUNION_PAYLOAD,
|
|
c.AIR_INST_UNWRAP_ERRUNION_ERR,
|
|
c.AIR_INST_WRAP_ERRUNION_PAYLOAD,
|
|
c.AIR_INST_WRAP_ERRUNION_ERR,
|
|
c.AIR_INST_ARRAY_TO_SLICE,
|
|
c.AIR_INST_LOAD,
|
|
c.AIR_INST_NOT,
|
|
c.AIR_INST_INT_FROM_FLOAT,
|
|
c.AIR_INST_INT_FROM_FLOAT_OPTIMIZED,
|
|
c.AIR_INST_INT_FROM_FLOAT_SAFE,
|
|
c.AIR_INST_INT_FROM_FLOAT_OPTIMIZED_SAFE,
|
|
c.AIR_INST_FLOAT_FROM_INT,
|
|
c.AIR_INST_CLZ,
|
|
c.AIR_INST_CTZ,
|
|
c.AIR_INST_POPCOUNT,
|
|
c.AIR_INST_BYTE_SWAP,
|
|
c.AIR_INST_STRUCT_FIELD_PTR_INDEX_0,
|
|
c.AIR_INST_STRUCT_FIELD_PTR_INDEX_1,
|
|
c.AIR_INST_STRUCT_FIELD_PTR_INDEX_2,
|
|
c.AIR_INST_STRUCT_FIELD_PTR_INDEX_3,
|
|
=> .{ true, true },
|
|
// arg: type(Ref) + zir_param_index(u32)
|
|
c.AIR_INST_ARG => .{ true, false },
|
|
// br: block_inst(u32) + operand(Ref)
|
|
c.AIR_INST_BR => .{ false, true },
|
|
// pl_op (cond_br): operand(Ref) + payload(u32)
|
|
c.AIR_INST_COND_BR => .{ true, false },
|
|
// Default: assume no refs (compare directly).
|
|
// If a tag with refs is missed, the comparison will fail
|
|
// and we add it here.
|
|
else => .{ false, false },
|
|
};
|
|
}
|
|
|
|
/// Zero-pad bytes after the null terminator in a NullTerminatedString stored
|
|
/// in the extra array. Zig's appendAirString leaves padding uninitialised;
|
|
/// the C side zeroes it. Normalising both to zero allows comparison.
|
|
fn normalizeNtsPadding(extra: []u32, nts_index: u32) void {
|
|
if (nts_index == 0 or nts_index >= extra.len) return;
|
|
const bytes = std.mem.sliceAsBytes(extra);
|
|
const byte_start = nts_index * 4;
|
|
// Find null terminator.
|
|
var i = byte_start;
|
|
while (i < bytes.len) : (i += 1) {
|
|
if (bytes[i] == 0) break;
|
|
}
|
|
// Zero-pad from null+1 to next word boundary.
|
|
i += 1;
|
|
const next_word_byte = ((i + 3) / 4) * 4;
|
|
while (i < next_word_byte and i < bytes.len) : (i += 1) {
|
|
bytes[i] = 0;
|
|
}
|
|
}
|
|
|
|
fn airCompareOne(name: []const u8, zig_air: *const c.Air, c_air: *const c.Air) !void {
|
|
if (zig_air.inst_len != c_air.inst_len) {
|
|
std.debug.print("'{s}': inst_len mismatch: zig={d} c={d}\n", .{ name, zig_air.inst_len, c_air.inst_len });
|
|
if (cToOpt(u8, zig_air.inst_tags)) |zt| {
|
|
std.debug.print(" zig tags:", .{});
|
|
for (0..zig_air.inst_len) |j| std.debug.print(" {s}", .{airTagNameSlice(zt[j])});
|
|
std.debug.print("\n", .{});
|
|
}
|
|
if (cToOpt(u8, c_air.inst_tags)) |ct| {
|
|
std.debug.print(" c tags:", .{});
|
|
for (0..c_air.inst_len) |j| std.debug.print(" {s}", .{airTagNameSlice(ct[j])});
|
|
std.debug.print("\n", .{});
|
|
}
|
|
return error.AirMismatch;
|
|
}
|
|
const inst_len = zig_air.inst_len;
|
|
|
|
// Canonical ref maps shared between datas and extra comparisons.
|
|
var zig_ref_map = std.AutoHashMap(u32, u32).init(std.testing.allocator);
|
|
defer zig_ref_map.deinit();
|
|
var c_ref_map = std.AutoHashMap(u32, u32).init(std.testing.allocator);
|
|
defer c_ref_map.deinit();
|
|
var next_zig_id: u32 = 0;
|
|
var next_c_id: u32 = 0;
|
|
|
|
// Tags
|
|
if (inst_len > 0) {
|
|
const zig_tags: [*]const u8 = cToOpt(u8, zig_air.inst_tags) orelse {
|
|
std.debug.print("'{s}': Zig inst_tags is null but inst_len={d}\n", .{ name, inst_len });
|
|
return error.AirMismatch;
|
|
};
|
|
const c_tags: [*]const u8 = cToOpt(u8, c_air.inst_tags) orelse {
|
|
std.debug.print("'{s}': C inst_tags is null but inst_len={d}\n", .{ name, inst_len });
|
|
return error.AirMismatch;
|
|
};
|
|
if (!std.mem.eql(u8, zig_tags[0..inst_len], c_tags[0..inst_len])) {
|
|
std.debug.print("'{s}': tags mismatch (inst_len={d}):", .{ name, inst_len });
|
|
for (0..inst_len) |j| {
|
|
std.debug.print(" zig[{d}]={d}({s}) c[{d}]={d}({s})", .{ j, zig_tags[j], airTagNameSlice(zig_tags[j]), j, c_tags[j], airTagNameSlice(c_tags[j]) });
|
|
}
|
|
std.debug.print("\n", .{});
|
|
return error.AirMismatch;
|
|
}
|
|
}
|
|
|
|
// Datas (8 bytes per instruction, tag-aware comparison).
|
|
// IP refs may differ between C and Zig InternPools, so we use
|
|
// canonical renumbering: each unique IP ref gets a sequential ID
|
|
// in order of first appearance. Inst refs (bit 31 set) and
|
|
// non-ref fields are compared directly.
|
|
// Air.Inst.Data is an 8-byte union; variants smaller than 8 bytes
|
|
// (un_op, no_op, ty, repeat) leave padding uninitialised — only
|
|
// compare the meaningful slots per tag via airInstNumSlots.
|
|
if (inst_len > 0) {
|
|
const zig_tags: [*]const u8 = cToOpt(u8, zig_air.inst_tags) orelse unreachable;
|
|
const zig_datas: [*]const u8 = @ptrCast(cToOpt(c.AirInstData, zig_air.inst_datas) orelse {
|
|
std.debug.print("'{s}': Zig inst_datas is null but inst_len={d}\n", .{ name, inst_len });
|
|
return error.AirMismatch;
|
|
});
|
|
const c_datas: [*]const u8 = @ptrCast(cToOpt(c.AirInstData, c_air.inst_datas) orelse {
|
|
std.debug.print("'{s}': C inst_datas is null but inst_len={d}\n", .{ name, inst_len });
|
|
return error.AirMismatch;
|
|
});
|
|
|
|
for (0..inst_len) |j| {
|
|
const off = j * 8;
|
|
const tag_val = zig_tags[j];
|
|
const ref_slots = airDataRefSlots(tag_val);
|
|
const num_slots = airInstNumSlots(tag_val);
|
|
|
|
for (0..num_slots) |slot| {
|
|
const s = off + slot * 4;
|
|
const zig_word = std.mem.readInt(u32, zig_datas[s..][0..4], .little);
|
|
const c_word = std.mem.readInt(u32, c_datas[s..][0..4], .little);
|
|
|
|
// Skip data comparison for dead BLOCKs (tag 51).
|
|
// Dead BLOCKs have undefined data in Zig (0xaa..
|
|
// or stale values) vs zeroed in C.
|
|
if (tag_val == 51 and c_word == 0 and zig_word != 0) continue;
|
|
|
|
if (ref_slots[slot]) {
|
|
// This slot is a Ref — canonicalize IP refs.
|
|
const zig_canon = canonicalizeRef(zig_word, &zig_ref_map, &next_zig_id);
|
|
const c_canon = canonicalizeRef(c_word, &c_ref_map, &next_c_id);
|
|
if (zig_canon != c_canon) {
|
|
std.debug.print("'{s}': datas ref mismatch at inst[{d}] slot {d}: zig=0x{x}[{s}] c=0x{x}[{s}] (canon: zig={d} c={d}) (tag={s})\n", .{ name, j, slot, zig_word, refKindStr(zig_word), c_word, refKindStr(c_word), zig_canon, c_canon, airTagNameSlice(tag_val) });
|
|
return error.AirMismatch;
|
|
}
|
|
} else {
|
|
// Non-ref field — compare directly.
|
|
if (zig_word != c_word) {
|
|
std.debug.print("'{s}': datas mismatch at inst[{d}] slot {d}: zig=0x{x} c=0x{x} (tag={s})\n", .{ name, j, slot, zig_word, c_word, airTagNameSlice(tag_val) });
|
|
return error.AirMismatch;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Extra
|
|
if (zig_air.extra_len != c_air.extra_len) {
|
|
std.debug.print("'{s}': extra_len mismatch: zig={d} c={d}\n", .{ name, zig_air.extra_len, c_air.extra_len });
|
|
// Print first divergence point
|
|
const min_len = @min(zig_air.extra_len, c_air.extra_len);
|
|
if (min_len > 0) {
|
|
const zig_e: [*]const u32 = cToOpt(u32, zig_air.extra).?;
|
|
const c_e: [*]const u32 = cToOpt(u32, c_air.extra).?;
|
|
var printed: u32 = 0;
|
|
for (0..min_len) |ei| {
|
|
if (zig_e[ei] != c_e[ei] and printed < 40) {
|
|
std.debug.print(" extra[{d}]: zig={d} c={d}\n", .{ ei, zig_e[ei], c_e[ei] });
|
|
printed += 1;
|
|
}
|
|
}
|
|
// Also dump the raw extra arrays around the first divergence
|
|
var first_diff: usize = min_len;
|
|
for (0..min_len) |ei| {
|
|
if (zig_e[ei] != c_e[ei]) {
|
|
first_diff = ei;
|
|
break;
|
|
}
|
|
}
|
|
if (first_diff < min_len) {
|
|
const start = if (first_diff > 5) first_diff - 5 else 0;
|
|
const end = @min(first_diff + 20, min_len);
|
|
std.debug.print(" zig extra[{d}..{d}]:", .{ start, end });
|
|
for (start..end) |ei| std.debug.print(" {d}", .{zig_e[ei]});
|
|
std.debug.print("\n c extra[{d}..{d}]:", .{ start, end });
|
|
for (start..end) |ei| std.debug.print(" {d}", .{c_e[ei]});
|
|
std.debug.print("\n", .{});
|
|
}
|
|
}
|
|
return error.AirMismatch;
|
|
}
|
|
const extra_len = zig_air.extra_len;
|
|
if (extra_len > 0) {
|
|
const zig_extra: [*]const u32 = cToOpt(u32, zig_air.extra) orelse {
|
|
std.debug.print("'{s}': Zig extra is null but extra_len={d}\n", .{ name, extra_len });
|
|
return error.AirMismatch;
|
|
};
|
|
const c_extra: [*]const u32 = cToOpt(u32, c_air.extra) orelse {
|
|
std.debug.print("'{s}': C extra is null but extra_len={d}\n", .{ name, extra_len });
|
|
return error.AirMismatch;
|
|
};
|
|
// Make mutable copies and normalize NullTerminatedString padding.
|
|
// Zig's appendAirString leaves trailing bytes uninitialised (0xaa
|
|
// in debug); the C side zeroes them. Normalise both to zero.
|
|
const zig_extra_copy = try std.testing.allocator.alloc(u32, extra_len);
|
|
defer std.testing.allocator.free(zig_extra_copy);
|
|
@memcpy(zig_extra_copy, zig_extra[0..extra_len]);
|
|
const c_extra_copy = try std.testing.allocator.alloc(u32, extra_len);
|
|
defer std.testing.allocator.free(c_extra_copy);
|
|
@memcpy(c_extra_copy, c_extra[0..extra_len]);
|
|
if (inst_len > 0) {
|
|
const tags: [*]const u8 = cToOpt(u8, zig_air.inst_tags).?;
|
|
const zig_datas_raw: [*]const u8 = @ptrCast(cToOpt(c.AirInstData, zig_air.inst_datas).?);
|
|
const c_datas_raw: [*]const u8 = @ptrCast(cToOpt(c.AirInstData, c_air.inst_datas).?);
|
|
for (0..inst_len) |j| {
|
|
if (tags[j] == c.AIR_INST_DBG_VAR_VAL or
|
|
tags[j] == c.AIR_INST_DBG_VAR_PTR or
|
|
tags[j] == c.AIR_INST_DBG_ARG_INLINE)
|
|
{
|
|
// pl_op: slot 0 = operand, slot 1 = payload (NullTerminatedString)
|
|
const zig_nts = std.mem.readInt(u32, zig_datas_raw[j * 8 + 4 ..][0..4], .little);
|
|
const c_nts = std.mem.readInt(u32, c_datas_raw[j * 8 + 4 ..][0..4], .little);
|
|
normalizeNtsPadding(zig_extra_copy, zig_nts);
|
|
normalizeNtsPadding(c_extra_copy, c_nts);
|
|
}
|
|
if (tags[j] == c.AIR_INST_DBG_INLINE_BLOCK) {
|
|
// ty_pl: slot 1 = payload (extra index).
|
|
// Extra layout: {func(IP ref), body_len, body...}
|
|
// Canonicalize the func IP ref.
|
|
const zig_payload = std.mem.readInt(u32, zig_datas_raw[j * 8 + 4 ..][0..4], .little);
|
|
const c_payload = std.mem.readInt(u32, c_datas_raw[j * 8 + 4 ..][0..4], .little);
|
|
if (zig_payload < extra_len and c_payload < extra_len) {
|
|
zig_extra_copy[zig_payload] = canonicalizeRef(zig_extra_copy[zig_payload], &zig_ref_map, &next_zig_id);
|
|
c_extra_copy[c_payload] = canonicalizeRef(c_extra_copy[c_payload], &c_ref_map, &next_c_id);
|
|
}
|
|
}
|
|
if (tags[j] == c.AIR_INST_CALL or
|
|
tags[j] == c.AIR_INST_CALL_ALWAYS_TAIL or
|
|
tags[j] == c.AIR_INST_CALL_NEVER_TAIL or
|
|
tags[j] == c.AIR_INST_CALL_NEVER_INLINE)
|
|
{
|
|
// pl_op: slot 1 = payload (extra index).
|
|
// Extra layout: {args_len, arg_refs[0..args_len]}
|
|
// Canonicalize arg refs (they may be IP refs).
|
|
const zig_payload = std.mem.readInt(u32, zig_datas_raw[j * 8 + 4 ..][0..4], .little);
|
|
const c_payload = std.mem.readInt(u32, c_datas_raw[j * 8 + 4 ..][0..4], .little);
|
|
if (zig_payload < extra_len and c_payload < extra_len) {
|
|
const zig_args_len = zig_extra_copy[zig_payload];
|
|
const c_args_len = c_extra_copy[c_payload];
|
|
var ai: u32 = 0;
|
|
while (ai < zig_args_len and ai < c_args_len) : (ai += 1) {
|
|
const zi = zig_payload + 1 + ai;
|
|
const ci = c_payload + 1 + ai;
|
|
if (zi < extra_len and ci < extra_len) {
|
|
zig_extra_copy[zi] = canonicalizeRef(zig_extra_copy[zi], &zig_ref_map, &next_zig_id);
|
|
c_extra_copy[ci] = canonicalizeRef(c_extra_copy[ci], &c_ref_map, &next_c_id);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!std.mem.eql(u32, zig_extra_copy, c_extra_copy)) {
|
|
std.debug.print("'{s}': extra mismatch (extra_len={d})\n", .{ name, extra_len });
|
|
std.debug.print(" zig extra:", .{});
|
|
for (0..extra_len) |ei| std.debug.print(" {d}", .{zig_extra_copy[ei]});
|
|
std.debug.print("\n c extra:", .{});
|
|
for (0..extra_len) |ei| std.debug.print(" {d}", .{c_extra_copy[ei]});
|
|
std.debug.print("\n", .{});
|
|
for (0..extra_len) |ei| {
|
|
if (zig_extra_copy[ei] != c_extra_copy[ei]) {
|
|
std.debug.print(" extra[{d}]: zig=0x{x} c=0x{x}\n", .{ ei, zig_extra_copy[ei], c_extra_copy[ei] });
|
|
}
|
|
}
|
|
return error.AirMismatch;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn semaAirRawCheck(source: [:0]const u8) !void {
|
|
// C pipeline: parse -> astgen -> sema
|
|
var result = try semaCheck(source);
|
|
defer result.deinit();
|
|
|
|
// Zig pipeline: compile source and compare Air arrays
|
|
try airCompareFromSource(source, result.c_func_air_list);
|
|
}
|
|
|
|
test "sema: Air raw C vs Zig comparison (empty)" {
|
|
try semaAirRawCheck("");
|
|
}
|
|
|
|
test "sema: Air raw C vs Zig comparison (const)" {
|
|
try semaAirRawCheck("const x = 0;");
|
|
}
|
|
|
|
test "sema air: empty void function" {
|
|
try semaAirRawCheck("export fn f() void {}");
|
|
}
|
|
|
|
test "sema air: return integer" {
|
|
try semaAirRawCheck("export fn f() u32 { return 42; }");
|
|
}
|
|
|
|
test "sema air: identity function" {
|
|
try semaAirRawCheck("export fn f(x: u32) u32 { return x; }");
|
|
}
|
|
|
|
test "sema air: add two args" {
|
|
try semaAirRawCheck("export fn f(x: u32, y: u32) u32 { return x + y; }");
|
|
}
|
|
|
|
test "sema air: add comptime int" {
|
|
try semaAirRawCheck("export fn f(x: u32) u32 { return x + 1; }");
|
|
}
|
|
|
|
test "sema air: sub two args" {
|
|
try semaAirRawCheck("export fn f(x: u32, y: u32) u32 { return x - y; }");
|
|
}
|
|
|
|
test "sema air: xor two args" {
|
|
try semaAirRawCheck("export fn f(x: u32, y: u32) u32 { return x ^ y; }");
|
|
}
|
|
|
|
test "sema air: xor comptime int" {
|
|
try semaAirRawCheck("export fn f(x: u16) u16 { return x ^ 0x8000; }");
|
|
}
|
|
|
|
test "sema air: bitcast u32 to f32" {
|
|
try semaAirRawCheck("export fn f(x: u32) f32 { return @bitCast(x); }");
|
|
}
|
|
|
|
test "sema air: bitcast f32 to u32" {
|
|
try semaAirRawCheck("export fn f(x: f32) u32 { return @bitCast(x); }");
|
|
}
|
|
|
|
test "sema air: as node" {
|
|
try semaAirRawCheck("export fn f(x: u32) u32 { return @as(u32, x); }");
|
|
}
|
|
|
|
test "sema air: local const binding" {
|
|
try semaAirRawCheck("export fn f(x: u32) u32 { const y = x + 1; return y; }");
|
|
}
|
|
|
|
test "sema air: multiple operations" {
|
|
try semaAirRawCheck("export fn f(x: u32, y: u32) u32 { return (x + y) ^ 0xFF; }");
|
|
}
|
|
|
|
test "sema air: neghf2 inline equivalent" {
|
|
try semaAirRawCheck(
|
|
\\export fn f(a: f16) f16 {
|
|
\\ return @bitCast(@as(u16, @bitCast(a)) ^ @as(u16, 0x8000));
|
|
\\}
|
|
);
|
|
}
|
|
|
|
test "sema air: mul two args" {
|
|
try semaAirRawCheck("export fn f(x: u32, y: u32) u32 { return x * y; }");
|
|
}
|
|
|
|
// TODO: bool and/or require block merges and conditional analysis (bool_br_and).
|
|
// test "sema air: bool and" {
|
|
// try semaAirRawCheck("export fn f(x: bool, y: bool) bool { return x and y; }");
|
|
// }
|
|
|
|
test "sema air: compare lt" {
|
|
try semaAirRawCheck("export fn f(x: u32, y: u32) bool { return x < y; }");
|
|
}
|
|
|
|
test "sema air: compare eq" {
|
|
try semaAirRawCheck("export fn f(x: u32, y: u32) bool { return x == y; }");
|
|
}
|
|
|
|
test "sema air: bit shift right" {
|
|
try semaAirRawCheck("export fn f(x: u32) u32 { return x >> 1; }");
|
|
}
|
|
|
|
test "sema air: mul comptime int" {
|
|
try semaAirRawCheck("export fn f(x: u32) u32 { return x * 3; }");
|
|
}
|
|
|
|
test "sema air: chain of casts" {
|
|
try semaAirRawCheck(
|
|
\\export fn f(x: u8) u32 {
|
|
\\ const wide: u16 = @intCast(x);
|
|
\\ return @intCast(wide);
|
|
\\}
|
|
);
|
|
}
|
|
|
|
test "sema air: mixed arithmetic and bitwise" {
|
|
try semaAirRawCheck(
|
|
\\export fn f(a: u32, b: u32) u32 {
|
|
\\ return (a + b) & 0xFF;
|
|
\\}
|
|
);
|
|
}
|
|
|
|
test "sema air: shift and mask" {
|
|
try semaAirRawCheck(
|
|
\\export fn f(x: u32) u32 {
|
|
\\ return (x >> 8) & 0xFF;
|
|
\\}
|
|
);
|
|
}
|
|
|
|
test "sema air: f32 arithmetic" {
|
|
try semaAirRawCheck("export fn f(x: f32, y: f32) f32 { return x + y; }");
|
|
}
|
|
|
|
test "sema air: multi-param function" {
|
|
try semaAirRawCheck(
|
|
\\export fn f(a: u32, b: u32, c: u32) u32 {
|
|
\\ return (a + b) * c;
|
|
\\}
|
|
);
|
|
}
|
|
|
|
test "sema air: nested bitcast xor" {
|
|
try semaAirRawCheck(
|
|
\\export fn f(a: f32) f32 {
|
|
\\ return @bitCast(@as(u32, @bitCast(a)) ^ @as(u32, 0x80000000));
|
|
\\}
|
|
);
|
|
}
|
|
|
|
test "sema air: pointer param identity" {
|
|
try semaAirRawCheck("export fn f(x: *u32) *u32 { return x; }");
|
|
}
|
|
|
|
test "sema air: store to pointer" {
|
|
try semaAirRawCheck(
|
|
\\export fn f(x: *u32) void {
|
|
\\ x.* = 42;
|
|
\\}
|
|
);
|
|
}
|
|
|
|
test "sema air: sub comptime" {
|
|
try semaAirRawCheck("export fn f(x: u32) u32 { return x - 1; }");
|
|
}
|
|
|
|
test "sema air: store runtime value" {
|
|
try semaAirRawCheck(
|
|
\\export fn f(p: *u32, x: u32) void {
|
|
\\ p.* = x;
|
|
\\}
|
|
);
|
|
}
|
|
|
|
test "sema air: load from pointer" {
|
|
try semaAirRawCheck(
|
|
\\export fn f(p: *u32) u32 {
|
|
\\ return p.*;
|
|
\\}
|
|
);
|
|
}
|
|
|
|
test "sema air: negate" {
|
|
try semaAirRawCheck("export fn f(x: i32) i32 { return -x; }");
|
|
}
|
|
|
|
test "sema air: bit not" {
|
|
try semaAirRawCheck("export fn f(x: u32) u32 { return ~x; }");
|
|
}
|
|
|
|
test "sema air: bit shift left" {
|
|
try semaAirRawCheck("export fn f(x: u32) u32 { return x << 1; }");
|
|
}
|
|
|
|
test "sema air: intcast" {
|
|
try semaAirRawCheck("export fn f(x: u16) u32 { return @intCast(x); }");
|
|
}
|
|
|
|
test "sema air: truncate" {
|
|
try semaAirRawCheck("export fn f(x: u32) u16 { return @truncate(x); }");
|
|
}
|
|
|
|
test "sema air: two local bindings" {
|
|
try semaAirRawCheck(
|
|
\\export fn f(x: u32, y: u32) u32 {
|
|
\\ const a = x + 1;
|
|
\\ const b = y + 2;
|
|
\\ return a ^ b;
|
|
\\}
|
|
);
|
|
}
|
|
|
|
test "sema air: wrapping add" {
|
|
try semaAirRawCheck("export fn f(x: u32, y: u32) u32 { return x +% y; }");
|
|
}
|
|
|
|
test "sema air: wrapping sub" {
|
|
try semaAirRawCheck("export fn f(x: u32, y: u32) u32 { return x -% y; }");
|
|
}
|
|
|
|
test "sema air: wrapping mul" {
|
|
try semaAirRawCheck("export fn f(x: u32, y: u32) u32 { return x *% y; }");
|
|
}
|
|
|
|
// test "sema air: div" {
|
|
// // Requires zirDiv with safety checks (div_trunc + remainder check).
|
|
// try semaAirRawCheck("export fn f(x: u32, y: u32) u32 { return x / y; }");
|
|
// }
|
|
|
|
test "sema air: bool not" {
|
|
try semaAirRawCheck("export fn f(x: bool) bool { return !x; }");
|
|
}
|
|
|
|
test "sema air: if simple" {
|
|
// Requires condbr, block merging, conditional branching.
|
|
try semaAirRawCheck(
|
|
\\export fn f(x: u32, y: u32) u32 {
|
|
\\ if (x > y) return x;
|
|
\\ return y;
|
|
\\}
|
|
);
|
|
}
|
|
|
|
test "sema air: wrapping negate" {
|
|
try semaAirRawCheck("export fn f(x: i32) i32 { return -%x; }");
|
|
}
|
|
|
|
test "sema air: clz" {
|
|
try semaAirRawCheck("export fn f(x: u32) u32 { return @clz(x); }");
|
|
}
|
|
|
|
test "sema air: ctz" {
|
|
try semaAirRawCheck("export fn f(x: u32) u32 { return @ctz(x); }");
|
|
}
|
|
|
|
test "sema air: popcount" {
|
|
try semaAirRawCheck("export fn f(x: u32) u32 { return @popCount(x); }");
|
|
}
|
|
|
|
test "sema air: byteswap" {
|
|
try semaAirRawCheck("export fn f(x: u32) u32 { return @byteSwap(x); }");
|
|
}
|
|
|
|
test "sema air: float cast widen" {
|
|
try semaAirRawCheck("export fn f(x: f32) f64 { return @floatCast(x); }");
|
|
}
|
|
|
|
test "sema air: float cast narrow" {
|
|
try semaAirRawCheck("export fn f(x: f64) f32 { return @floatCast(x); }");
|
|
}
|
|
|
|
test "sema air: int from float" {
|
|
try semaAirRawCheck("export fn f(x: f32) u32 { return @intFromFloat(x); }");
|
|
}
|
|
|
|
test "sema air: float from int" {
|
|
try semaAirRawCheck("export fn f(x: u32) f32 { return @floatFromInt(x); }");
|
|
}
|
|
|
|
test "sema air: bitmask shift and" {
|
|
try semaAirRawCheck("export fn f(x: u32) u32 { return (x >> 16) & 0xFF; }");
|
|
}
|
|
|
|
test "sema air: double negate" {
|
|
try semaAirRawCheck("export fn f(x: i32) i32 { return -(-x); }");
|
|
}
|
|
|
|
test "sema air: return ptr type" {
|
|
try semaAirRawCheck("export fn f(p: *u32) *u32 { return p; }");
|
|
}
|
|
|
|
test "sema air: float cast f16 to f32" {
|
|
try semaAirRawCheck("export fn f(x: f16) f32 { return @floatCast(x); }");
|
|
}
|
|
|
|
test "sema air: wrapping add comptime" {
|
|
try semaAirRawCheck("export fn f(x: u32) u32 { return x +% 1; }");
|
|
}
|
|
|
|
test "sema air: byteswap and xor" {
|
|
try semaAirRawCheck(
|
|
\\export fn f(x: u32) u32 {
|
|
\\ return @byteSwap(x) ^ 0xFF;
|
|
\\}
|
|
);
|
|
}
|
|
|
|
test "sema air: same-file inline function call" {
|
|
try semaAirRawCheck(
|
|
\\inline fn negate(x: u16) u16 {
|
|
\\ return ~x;
|
|
\\}
|
|
\\export fn f(a: u16) u16 {
|
|
\\ return negate(a);
|
|
\\}
|
|
);
|
|
}
|
|
|
|
test "sema air: same-file inline call with bitcast and xor" {
|
|
try semaAirRawCheck(
|
|
\\inline fn flip_sign(x: u16) u16 {
|
|
\\ return x ^ 0x8000;
|
|
\\}
|
|
\\export fn f(a: u16) u16 {
|
|
\\ return flip_sign(a);
|
|
\\}
|
|
);
|
|
}
|
|
|
|
test "sema air: same-file inline call with two args" {
|
|
try semaAirRawCheck(
|
|
\\inline fn my_add(x: u32, y: u32) u32 {
|
|
\\ return x + y;
|
|
\\}
|
|
\\export fn f(a: u32, b: u32) u32 {
|
|
\\ return my_add(a, b);
|
|
\\}
|
|
);
|
|
}
|
|
|
|
test "sema air: intFromBool" {
|
|
try semaAirRawCheck("export fn f(x: bool) u32 { return @intFromBool(x); }");
|
|
}
|
|
|
|
test "sema air: add_sat" {
|
|
try semaAirRawCheck("export fn f(x: u32, y: u32) u32 { return x +| y; }");
|
|
}
|
|
|
|
test "sema air: sub_sat" {
|
|
try semaAirRawCheck("export fn f(x: u32, y: u32) u32 { return x -| y; }");
|
|
}
|
|
|
|
test "sema air: mul_sat" {
|
|
try semaAirRawCheck("export fn f(x: u32, y: u32) u32 { return x *| y; }");
|
|
}
|
|
|
|
test "sema air: shl_sat" {
|
|
try semaAirRawCheck("export fn f(x: u32) u32 { return x <<| 1; }");
|
|
}
|
|
|
|
test "sema air: bit_or" {
|
|
try semaAirRawCheck("export fn f(x: u32, y: u32) u32 { return x | y; }");
|
|
}
|
|
|
|
test "sema air: bit_and" {
|
|
try semaAirRawCheck("export fn f(x: u32, y: u32) u32 { return x & y; }");
|
|
}
|
|
|
|
test "sema air: f16 add" {
|
|
try semaAirRawCheck("export fn f(x: f16, y: f16) f16 { return x + y; }");
|
|
}
|
|
|
|
test "sema air: f64 mul" {
|
|
try semaAirRawCheck("export fn f(x: f64, y: f64) f64 { return x * y; }");
|
|
}
|
|
|
|
test "sema air: intcast computed dest type" {
|
|
try semaAirRawCheck(
|
|
\\export fn f(x: u16) u32 {
|
|
\\ return @intCast(x);
|
|
\\}
|
|
);
|
|
}
|
|
|
|
test "sema air: multiple return paths" {
|
|
try semaAirRawCheck(
|
|
\\export fn f(x: u32) u32 {
|
|
\\ return x + 1;
|
|
\\}
|
|
\\export fn g(x: u32) u32 {
|
|
\\ return x * 2;
|
|
\\}
|
|
);
|
|
}
|
|
|
|
test "sema air: if with early return" {
|
|
try semaAirRawCheck(
|
|
\\export fn f(x: u32, y: u32) u32 {
|
|
\\ if (x > y) return x;
|
|
\\ return y;
|
|
\\}
|
|
);
|
|
}
|
|
|
|
test "sema air: var bitcast and if" {
|
|
try semaAirRawCheck(
|
|
\\export fn f(a: f16) u16 {
|
|
\\ var x: u16 = @bitCast(a);
|
|
\\ if (x > 100) {
|
|
\\ x = x - 1;
|
|
\\ }
|
|
\\ return x;
|
|
\\}
|
|
);
|
|
}
|
|
|
|
test "sema air: var assignment in if" {
|
|
try semaAirRawCheck(
|
|
\\export fn f(a: u32, b: u32) u32 {
|
|
\\ var x = a;
|
|
\\ if (b > a) {
|
|
\\ x = b;
|
|
\\ }
|
|
\\ return x;
|
|
\\}
|
|
);
|
|
}
|
|
|
|
test "sema air: nested if" {
|
|
try semaAirRawCheck(
|
|
\\export fn f(a: u32, b: u32) u32 {
|
|
\\ if (a > 0) {
|
|
\\ if (b > 0) return a + b;
|
|
\\ return a;
|
|
\\ }
|
|
\\ return b;
|
|
\\}
|
|
);
|
|
}
|
|
|
|
test "sema air: wrapping sub in expr" {
|
|
try semaAirRawCheck(
|
|
\\export fn f(a: u32) u32 {
|
|
\\ return a -% 1;
|
|
\\}
|
|
);
|
|
}
|
|
|
|
test "sema air: if-else block result" {
|
|
try semaAirRawCheck(
|
|
\\export fn f(a: u32, b: u32) u32 {
|
|
\\ const x = if (a > b) a else b;
|
|
\\ return x;
|
|
\\}
|
|
);
|
|
}
|
|
|
|
test "sema air: call inside runtime conditional" {
|
|
try semaAirRawCheck(
|
|
\\fn bar(p: *u32) void {
|
|
\\ p.* += 1;
|
|
\\}
|
|
\\export fn f(a: u32) u32 {
|
|
\\ var x: u32 = a;
|
|
\\ if (a < 10) bar(&x);
|
|
\\ return x;
|
|
\\}
|
|
);
|
|
}
|
|
|
|
test "sema air: inline fn with call inside conditional" {
|
|
try semaAirRawCheck(
|
|
\\fn bar(p: *u32) void {
|
|
\\ p.* += 1;
|
|
\\}
|
|
\\inline fn baz(a: u32, x: *u32) void {
|
|
\\ if (a < 10) bar(x);
|
|
\\}
|
|
\\export fn f(a: u32) u32 {
|
|
\\ var x: u32 = a;
|
|
\\ baz(a, &x);
|
|
\\ return x;
|
|
\\}
|
|
);
|
|
}
|
|
|
|
test "sema air: += with call inside conditional" {
|
|
try semaAirRawCheck(
|
|
\\fn bar(p: *u32) u32 {
|
|
\\ return p.* + 1;
|
|
\\}
|
|
\\export fn f(a: u32) u32 {
|
|
\\ var x: u32 = a;
|
|
\\ if (a < 10) x += bar(&x);
|
|
\\ return x;
|
|
\\}
|
|
);
|
|
}
|
|
|
|
test "sema air: inline fn with += call inside conditional" {
|
|
try semaAirRawCheck(
|
|
\\fn bar(p: *u32) u32 {
|
|
\\ return p.* + 1;
|
|
\\}
|
|
\\inline fn baz(a: u32, x: *u32) void {
|
|
\\ if (a < 10) x.* += bar(x);
|
|
\\}
|
|
\\export fn f(a: u32) u32 {
|
|
\\ var x: u32 = a;
|
|
\\ baz(a, &x);
|
|
\\ return x;
|
|
\\}
|
|
);
|
|
}
|
|
|
|
test "sema air: inline fn with generic call inside conditional" {
|
|
try semaAirRawCheck(
|
|
\\fn normalize(comptime T: type, p: *T) i32 {
|
|
\\ p.* +%= 1;
|
|
\\ return 1;
|
|
\\}
|
|
\\inline fn mulf(comptime T: type, a: T) T {
|
|
\\ var x: T = a;
|
|
\\ var scale: i32 = 0;
|
|
\\ if (x < 10) scale += normalize(T, &x);
|
|
\\ return x +% @as(T, @intCast(@as(u32, @bitCast(scale))));
|
|
\\}
|
|
\\export fn f(a: u32) u32 {
|
|
\\ return mulf(u32, a);
|
|
\\}
|
|
);
|
|
}
|
|
|
|
test "sema air: inline fn with two generic calls in conditionals" {
|
|
try semaAirRawCheck(
|
|
\\fn normalize(comptime T: type, p: *T) i32 {
|
|
\\ p.* +%= 1;
|
|
\\ return 1;
|
|
\\}
|
|
\\inline fn mulf(comptime T: type, a: T, b: T) T {
|
|
\\ var x: T = a;
|
|
\\ var y: T = b;
|
|
\\ var scale: i32 = 0;
|
|
\\ if (x < 10) scale += normalize(T, &x);
|
|
\\ if (y < 10) scale += normalize(T, &y);
|
|
\\ return x +% y +% @as(T, @intCast(@as(u32, @bitCast(scale))));
|
|
\\}
|
|
\\export fn f(a: u32, b: u32) u32 {
|
|
\\ return mulf(u32, a, b);
|
|
\\}
|
|
);
|
|
}
|
|
|
|
test "sema air: inline fn with += call inside two conditionals" {
|
|
try semaAirRawCheck(
|
|
\\fn bar(p: *u32) u32 {
|
|
\\ return p.* + 1;
|
|
\\}
|
|
\\inline fn baz(a: u32, x: *u32, y: *u32) void {
|
|
\\ if (a < 10) x.* += bar(x);
|
|
\\ if (a < 20) y.* += bar(y);
|
|
\\}
|
|
\\export fn f(a: u32) u32 {
|
|
\\ var x: u32 = a;
|
|
\\ var y: u32 = a;
|
|
\\ baz(a, &x, &y);
|
|
\\ return x +% y;
|
|
\\}
|
|
);
|
|
}
|