Rewrite globalVarDecl to properly handle extern/export/pub/threadlocal variables with type/align/linksection/addrspace bodies. Port the full Declaration extra data layout from upstream AstGen.zig:13883, including lib_name, type_body, and special bodies fields. Add extractVarDecl to decode all VarDecl node types (global, local, simple, aligned) and computeVarDeclId to select the correct Declaration.Flags.Id. Fix firstToken to scan backwards for modifier tokens (extern, export, pub, threadlocal, comptime) on var decl nodes, matching upstream Ast.zig:634-643. Test added: extern var. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
966 lines
34 KiB
Zig
966 lines
34 KiB
Zig
const std = @import("std");
|
|
const Ast = std.zig.Ast;
|
|
const Zir = std.zig.Zir;
|
|
const AstGen = std.zig.AstGen;
|
|
const Allocator = std.mem.Allocator;
|
|
|
|
const c = @cImport({
|
|
@cInclude("astgen.h");
|
|
});
|
|
|
|
fn dumpZir(ref_zir: Zir) void {
|
|
const tags = ref_zir.instructions.items(.tag);
|
|
const datas = ref_zir.instructions.items(.data);
|
|
std.debug.print(" instructions: {d}\n", .{ref_zir.instructions.len});
|
|
for (0..ref_zir.instructions.len) |i| {
|
|
const tag = tags[i];
|
|
std.debug.print(" [{d}] tag={d} ({s})", .{ i, @intFromEnum(tag), @tagName(tag) });
|
|
switch (tag) {
|
|
.extended => {
|
|
const ext = datas[i].extended;
|
|
std.debug.print(" opcode={d} small=0x{x:0>4} operand={d}", .{ @intFromEnum(ext.opcode), ext.small, ext.operand });
|
|
},
|
|
.declaration => {
|
|
const decl = datas[i].declaration;
|
|
std.debug.print(" src_node={d} payload_index={d}", .{ @intFromEnum(decl.src_node), decl.payload_index });
|
|
},
|
|
.break_inline => {
|
|
const brk = datas[i].@"break";
|
|
std.debug.print(" operand={d} payload_index={d}", .{ @intFromEnum(brk.operand), brk.payload_index });
|
|
},
|
|
else => {},
|
|
}
|
|
std.debug.print("\n", .{});
|
|
}
|
|
std.debug.print(" extra ({d}):\n", .{ref_zir.extra.len});
|
|
for (0..ref_zir.extra.len) |i| {
|
|
std.debug.print(" [{d}] = 0x{x:0>8} ({d})\n", .{ i, ref_zir.extra[i], ref_zir.extra[i] });
|
|
}
|
|
std.debug.print(" string_bytes ({d}):", .{ref_zir.string_bytes.len});
|
|
for (0..ref_zir.string_bytes.len) |i| {
|
|
std.debug.print(" {x:0>2}", .{ref_zir.string_bytes[i]});
|
|
}
|
|
std.debug.print("\n", .{});
|
|
}
|
|
|
|
fn refZir(gpa: Allocator, source: [:0]const u8) !Zir {
|
|
var tree = try Ast.parse(gpa, source, .zig);
|
|
defer tree.deinit(gpa);
|
|
return try AstGen.generate(gpa, tree);
|
|
}
|
|
|
|
test "astgen dump: simple cases" {
|
|
const gpa = std.testing.allocator;
|
|
|
|
const cases = .{
|
|
.{ "empty", "" },
|
|
.{ "comptime {}", "comptime {}" },
|
|
.{ "const x = 0;", "const x = 0;" },
|
|
.{ "const x = 1;", "const x = 1;" },
|
|
.{ "const x = 0; const y = 0;", "const x = 0; const y = 0;" },
|
|
.{ "test \"t\" {}", "test \"t\" {}" },
|
|
.{ "const std = @import(\"std\");", "const std = @import(\"std\");" },
|
|
.{ "test_all.zig", @embedFile("test_all.zig") },
|
|
};
|
|
|
|
inline for (cases) |case| {
|
|
// std.debug.print("--- {s} ---\n", .{case[0]});
|
|
const source: [:0]const u8 = case[1];
|
|
var zir = try refZir(gpa, source);
|
|
zir.deinit(gpa);
|
|
}
|
|
}
|
|
|
|
/// Build a mask of extra[] indices that contain hash data (src_hash or
|
|
/// fields_hash). These are zero-filled in the C output but contain real
|
|
/// Blake3 hashes in the Zig reference. We skip these positions during
|
|
/// comparison.
|
|
fn buildHashSkipMask(gpa: Allocator, ref: Zir) ![]bool {
|
|
const ref_extra_len: u32 = @intCast(ref.extra.len);
|
|
const skip = try gpa.alloc(bool, ref_extra_len);
|
|
@memset(skip, false);
|
|
|
|
const ref_len: u32 = @intCast(ref.instructions.len);
|
|
const ref_tags = ref.instructions.items(.tag);
|
|
const ref_datas = ref.instructions.items(.data);
|
|
for (0..ref_len) |i| {
|
|
switch (ref_tags[i]) {
|
|
.extended => {
|
|
const ext = ref_datas[i].extended;
|
|
if (ext.opcode == .struct_decl) {
|
|
// StructDecl starts with fields_hash[4].
|
|
const pi = ext.operand;
|
|
for (0..4) |j| skip[pi + j] = true;
|
|
}
|
|
},
|
|
.declaration => {
|
|
// Declaration starts with src_hash[4].
|
|
const pi = ref_datas[i].declaration.payload_index;
|
|
for (0..4) |j| skip[pi + j] = true;
|
|
},
|
|
else => {},
|
|
}
|
|
}
|
|
return skip;
|
|
}
|
|
|
|
test "astgen: empty source" {
|
|
const gpa = std.testing.allocator;
|
|
const source: [:0]const u8 = "";
|
|
|
|
var ref_zir = try refZir(gpa, source);
|
|
defer ref_zir.deinit(gpa);
|
|
|
|
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);
|
|
|
|
try expectEqualZir(gpa, ref_zir, c_zir);
|
|
}
|
|
|
|
test "astgen: comptime {}" {
|
|
const gpa = std.testing.allocator;
|
|
const source: [:0]const u8 = "comptime {}";
|
|
|
|
var ref_zir = try refZir(gpa, source);
|
|
defer ref_zir.deinit(gpa);
|
|
|
|
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);
|
|
|
|
try expectEqualZir(gpa, ref_zir, c_zir);
|
|
}
|
|
|
|
test "astgen: const x = 0;" {
|
|
const gpa = std.testing.allocator;
|
|
const source: [:0]const u8 = "const x = 0;";
|
|
|
|
var ref_zir = try refZir(gpa, source);
|
|
defer ref_zir.deinit(gpa);
|
|
|
|
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);
|
|
|
|
try expectEqualZir(gpa, ref_zir, c_zir);
|
|
}
|
|
|
|
test "astgen: const x = 1;" {
|
|
const gpa = std.testing.allocator;
|
|
const source: [:0]const u8 = "const x = 1;";
|
|
|
|
var ref_zir = try refZir(gpa, source);
|
|
defer ref_zir.deinit(gpa);
|
|
|
|
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);
|
|
|
|
try expectEqualZir(gpa, ref_zir, c_zir);
|
|
}
|
|
|
|
test "astgen: const x = 0; const y = 0;" {
|
|
const gpa = std.testing.allocator;
|
|
const source: [:0]const u8 = "const x = 0; const y = 0;";
|
|
|
|
var ref_zir = try refZir(gpa, source);
|
|
defer ref_zir.deinit(gpa);
|
|
|
|
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);
|
|
|
|
try expectEqualZir(gpa, ref_zir, c_zir);
|
|
}
|
|
|
|
test "astgen: field_access" {
|
|
const gpa = std.testing.allocator;
|
|
const source: [:0]const u8 = "const std = @import(\"std\");\nconst mem = std.mem;";
|
|
|
|
var ref_zir = try refZir(gpa, source);
|
|
defer ref_zir.deinit(gpa);
|
|
|
|
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);
|
|
|
|
try expectEqualZir(gpa, ref_zir, c_zir);
|
|
}
|
|
|
|
test "astgen: addr array init" {
|
|
const gpa = std.testing.allocator;
|
|
const source: [:0]const u8 = "const x = &[_][]const u8{\"a\",\"b\"};";
|
|
|
|
var ref_zir = try refZir(gpa, source);
|
|
defer ref_zir.deinit(gpa);
|
|
|
|
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);
|
|
|
|
try expectEqualZir(gpa, ref_zir, c_zir);
|
|
}
|
|
|
|
test "astgen: test empty body" {
|
|
const gpa = std.testing.allocator;
|
|
const source: [:0]const u8 = "test \"t\" {}";
|
|
|
|
var ref_zir = try refZir(gpa, source);
|
|
defer ref_zir.deinit(gpa);
|
|
|
|
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);
|
|
|
|
try expectEqualZir(gpa, ref_zir, c_zir);
|
|
}
|
|
|
|
test "astgen: test_all.zig" {
|
|
const gpa = std.testing.allocator;
|
|
const source: [:0]const u8 = @embedFile("test_all.zig");
|
|
|
|
var ref_zir = try refZir(gpa, source);
|
|
defer ref_zir.deinit(gpa);
|
|
|
|
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);
|
|
|
|
try expectEqualZir(gpa, ref_zir, c_zir);
|
|
}
|
|
|
|
test "astgen: @import" {
|
|
const gpa = std.testing.allocator;
|
|
const source: [:0]const u8 = "const std = @import(\"std\");";
|
|
|
|
var ref_zir = try refZir(gpa, source);
|
|
defer ref_zir.deinit(gpa);
|
|
|
|
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);
|
|
|
|
try expectEqualZir(gpa, ref_zir, c_zir);
|
|
}
|
|
|
|
fn expectEqualZir(gpa: Allocator, ref: Zir, got: c.Zir) !void {
|
|
// Compare instruction count.
|
|
const ref_len: u32 = @intCast(ref.instructions.len);
|
|
if (ref_len != got.inst_len) {
|
|
std.debug.print("inst_len mismatch: ref={d} got={d}\n", .{ ref_len, got.inst_len });
|
|
return error.TestExpectedEqual;
|
|
}
|
|
|
|
// Compare instructions (tag + data) field-by-field.
|
|
const ref_tags = ref.instructions.items(.tag);
|
|
const ref_datas = ref.instructions.items(.data);
|
|
for (0..ref_len) |i| {
|
|
const ref_tag: u8 = @intFromEnum(ref_tags[i]);
|
|
const got_tag: u8 = @intCast(got.inst_tags[i]);
|
|
if (ref_tag != got_tag) {
|
|
std.debug.print(
|
|
"inst_tags[{d}] mismatch: ref={d} got={d}\n",
|
|
.{ i, ref_tag, got_tag },
|
|
);
|
|
return error.TestExpectedEqual;
|
|
}
|
|
try expectEqualData(i, ref_tags[i], ref_datas[i], got.inst_datas[i]);
|
|
}
|
|
|
|
// Build hash skip mask for extra comparison.
|
|
const skip = try buildHashSkipMask(gpa, ref);
|
|
defer gpa.free(skip);
|
|
|
|
// Compare extra data, skipping hash positions.
|
|
const ref_extra_len: u32 = @intCast(ref.extra.len);
|
|
try std.testing.expectEqual(ref_extra_len, got.extra_len);
|
|
for (0..ref_extra_len) |i| {
|
|
if (skip[i]) continue;
|
|
if (ref.extra[i] != got.extra[i]) {
|
|
std.debug.print(
|
|
"extra[{d}] mismatch: ref=0x{x:0>8} got=0x{x:0>8}\n",
|
|
.{ i, ref.extra[i], got.extra[i] },
|
|
);
|
|
return error.TestExpectedEqual;
|
|
}
|
|
}
|
|
|
|
// Compare string bytes.
|
|
const ref_sb_len: u32 = @intCast(ref.string_bytes.len);
|
|
try std.testing.expectEqual(ref_sb_len, got.string_bytes_len);
|
|
for (0..ref_sb_len) |i| {
|
|
if (ref.string_bytes[i] != got.string_bytes[i]) {
|
|
std.debug.print(
|
|
"string_bytes[{d}] mismatch: ref=0x{x:0>2} got=0x{x:0>2}\n",
|
|
.{ i, ref.string_bytes[i], got.string_bytes[i] },
|
|
);
|
|
return error.TestExpectedEqual;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Compare a single instruction's data, dispatching by tag.
|
|
/// Zig's Data union has no guaranteed in-memory layout, so we
|
|
/// compare each variant's fields individually.
|
|
fn expectEqualData(
|
|
idx: usize,
|
|
tag: Zir.Inst.Tag,
|
|
ref: Zir.Inst.Data,
|
|
got: c.ZirInstData,
|
|
) !void {
|
|
switch (tag) {
|
|
.extended => {
|
|
const r = ref.extended;
|
|
const g = got.extended;
|
|
if (@intFromEnum(r.opcode) != g.opcode or
|
|
r.small != g.small or
|
|
r.operand != g.operand)
|
|
{
|
|
std.debug.print(
|
|
"inst_datas[{d}] (extended) mismatch:\n" ++
|
|
" ref: opcode={d} small=0x{x:0>4} operand={d}\n" ++
|
|
" got: opcode={d} small=0x{x:0>4} operand={d}\n",
|
|
.{
|
|
idx,
|
|
@intFromEnum(r.opcode),
|
|
r.small,
|
|
r.operand,
|
|
g.opcode,
|
|
g.small,
|
|
g.operand,
|
|
},
|
|
);
|
|
return error.TestExpectedEqual;
|
|
}
|
|
},
|
|
.declaration => {
|
|
const r = ref.declaration;
|
|
const g = got.declaration;
|
|
if (@intFromEnum(r.src_node) != g.src_node or
|
|
r.payload_index != g.payload_index)
|
|
{
|
|
std.debug.print(
|
|
"inst_datas[{d}] (declaration) mismatch:\n" ++
|
|
" ref: src_node={d} payload_index={d}\n" ++
|
|
" got: src_node={d} payload_index={d}\n",
|
|
.{
|
|
idx,
|
|
@intFromEnum(r.src_node),
|
|
r.payload_index,
|
|
g.src_node,
|
|
g.payload_index,
|
|
},
|
|
);
|
|
return error.TestExpectedEqual;
|
|
}
|
|
},
|
|
.break_inline => {
|
|
const r = ref.@"break";
|
|
const g = got.break_data;
|
|
if (@intFromEnum(r.operand) != g.operand or
|
|
r.payload_index != g.payload_index)
|
|
{
|
|
std.debug.print(
|
|
"inst_datas[{d}] (break_inline) mismatch:\n" ++
|
|
" ref: operand={d} payload_index={d}\n" ++
|
|
" got: operand={d} payload_index={d}\n",
|
|
.{
|
|
idx,
|
|
@intFromEnum(r.operand),
|
|
r.payload_index,
|
|
g.operand,
|
|
g.payload_index,
|
|
},
|
|
);
|
|
return error.TestExpectedEqual;
|
|
}
|
|
},
|
|
.import => {
|
|
const r = ref.pl_tok;
|
|
const g = got.pl_tok;
|
|
if (@intFromEnum(r.src_tok) != g.src_tok or
|
|
r.payload_index != g.payload_index)
|
|
{
|
|
std.debug.print(
|
|
"inst_datas[{d}] (import) mismatch:\n" ++
|
|
" ref: src_tok={d} payload_index={d}\n" ++
|
|
" got: src_tok={d} payload_index={d}\n",
|
|
.{
|
|
idx,
|
|
@intFromEnum(r.src_tok),
|
|
r.payload_index,
|
|
g.src_tok,
|
|
g.payload_index,
|
|
},
|
|
);
|
|
return error.TestExpectedEqual;
|
|
}
|
|
},
|
|
.dbg_stmt => {
|
|
const r = ref.dbg_stmt;
|
|
const g = got.dbg_stmt;
|
|
if (r.line != g.line or r.column != g.column) {
|
|
std.debug.print(
|
|
"inst_datas[{d}] (dbg_stmt) mismatch:\n" ++
|
|
" ref: line={d} column={d}\n" ++
|
|
" got: line={d} column={d}\n",
|
|
.{ idx, r.line, r.column, g.line, g.column },
|
|
);
|
|
return error.TestExpectedEqual;
|
|
}
|
|
},
|
|
.ensure_result_non_error,
|
|
.restore_err_ret_index_unconditional,
|
|
=> {
|
|
const r = ref.un_node;
|
|
const g = got.un_node;
|
|
if (@intFromEnum(r.src_node) != g.src_node or
|
|
@intFromEnum(r.operand) != g.operand)
|
|
{
|
|
std.debug.print(
|
|
"inst_datas[{d}] ({s}) mismatch:\n" ++
|
|
" ref: src_node={d} operand={d}\n" ++
|
|
" got: src_node={d} operand={d}\n",
|
|
.{
|
|
idx,
|
|
@tagName(tag),
|
|
@intFromEnum(r.src_node),
|
|
@intFromEnum(r.operand),
|
|
g.src_node,
|
|
g.operand,
|
|
},
|
|
);
|
|
return error.TestExpectedEqual;
|
|
}
|
|
},
|
|
.ret_implicit => {
|
|
const r = ref.un_tok;
|
|
const g = got.un_tok;
|
|
if (@intFromEnum(r.src_tok) != g.src_tok or
|
|
@intFromEnum(r.operand) != g.operand)
|
|
{
|
|
std.debug.print(
|
|
"inst_datas[{d}] (ret_implicit) mismatch:\n" ++
|
|
" ref: src_tok={d} operand={d}\n" ++
|
|
" got: src_tok={d} operand={d}\n",
|
|
.{
|
|
idx,
|
|
@intFromEnum(r.src_tok),
|
|
@intFromEnum(r.operand),
|
|
g.src_tok,
|
|
g.operand,
|
|
},
|
|
);
|
|
return error.TestExpectedEqual;
|
|
}
|
|
},
|
|
.func,
|
|
.func_inferred,
|
|
.array_type,
|
|
.array_type_sentinel,
|
|
.array_cat,
|
|
.array_init,
|
|
.array_init_ref,
|
|
.error_set_decl,
|
|
=> {
|
|
const r = ref.pl_node;
|
|
const g = got.pl_node;
|
|
if (@intFromEnum(r.src_node) != g.src_node or
|
|
r.payload_index != g.payload_index)
|
|
{
|
|
std.debug.print(
|
|
"inst_datas[{d}] ({s}) mismatch:\n" ++
|
|
" ref: src_node={d} payload_index={d}\n" ++
|
|
" got: src_node={d} payload_index={d}\n",
|
|
.{
|
|
idx,
|
|
@tagName(tag),
|
|
@intFromEnum(r.src_node),
|
|
r.payload_index,
|
|
g.src_node,
|
|
g.payload_index,
|
|
},
|
|
);
|
|
return error.TestExpectedEqual;
|
|
}
|
|
},
|
|
.decl_val, .decl_ref => {
|
|
const r = ref.str_tok;
|
|
const g = got.str_tok;
|
|
if (@intFromEnum(r.start) != g.start or @intFromEnum(r.src_tok) != g.src_tok) {
|
|
std.debug.print(
|
|
"inst_datas[{d}] ({s}) mismatch:\n" ++
|
|
" ref: start={d} src_tok={d}\n" ++
|
|
" got: start={d} src_tok={d}\n",
|
|
.{
|
|
idx,
|
|
@tagName(tag),
|
|
@intFromEnum(r.start),
|
|
@intFromEnum(r.src_tok),
|
|
g.start,
|
|
g.src_tok,
|
|
},
|
|
);
|
|
return error.TestExpectedEqual;
|
|
}
|
|
},
|
|
.field_val, .field_ptr, .field_val_named, .field_ptr_named => {
|
|
const r = ref.pl_node;
|
|
const g = got.pl_node;
|
|
if (@intFromEnum(r.src_node) != g.src_node or
|
|
r.payload_index != g.payload_index)
|
|
{
|
|
std.debug.print(
|
|
"inst_datas[{d}] ({s}) mismatch:\n" ++
|
|
" ref: src_node={d} payload_index={d}\n" ++
|
|
" got: src_node={d} payload_index={d}\n",
|
|
.{
|
|
idx,
|
|
@tagName(tag),
|
|
@intFromEnum(r.src_node),
|
|
r.payload_index,
|
|
g.src_node,
|
|
g.payload_index,
|
|
},
|
|
);
|
|
return error.TestExpectedEqual;
|
|
}
|
|
},
|
|
.int => {
|
|
if (ref.int != got.int_val) {
|
|
std.debug.print(
|
|
"inst_datas[{d}] (int) mismatch: ref={d} got={d}\n",
|
|
.{ idx, ref.int, got.int_val },
|
|
);
|
|
return error.TestExpectedEqual;
|
|
}
|
|
},
|
|
.ptr_type => {
|
|
// Compare ptr_type data: flags, size, payload_index.
|
|
if (@as(u8, @bitCast(ref.ptr_type.flags)) != got.ptr_type.flags or
|
|
@intFromEnum(ref.ptr_type.size) != got.ptr_type.size or
|
|
ref.ptr_type.payload_index != got.ptr_type.payload_index)
|
|
{
|
|
std.debug.print(
|
|
"inst_datas[{d}] (ptr_type) mismatch:\n" ++
|
|
" ref: flags=0x{x} size={d} pi={d}\n" ++
|
|
" got: flags=0x{x} size={d} pi={d}\n",
|
|
.{
|
|
idx,
|
|
@as(u8, @bitCast(ref.ptr_type.flags)),
|
|
@intFromEnum(ref.ptr_type.size),
|
|
ref.ptr_type.payload_index,
|
|
got.ptr_type.flags,
|
|
got.ptr_type.size,
|
|
got.ptr_type.payload_index,
|
|
},
|
|
);
|
|
return error.TestExpectedEqual;
|
|
}
|
|
},
|
|
.int_type => {
|
|
const r = ref.int_type;
|
|
const g = got.int_type;
|
|
if (@intFromEnum(r.src_node) != g.src_node or
|
|
@intFromEnum(r.signedness) != g.signedness or
|
|
r.bit_count != g.bit_count)
|
|
{
|
|
std.debug.print(
|
|
"inst_datas[{d}] (int_type) mismatch\n",
|
|
.{idx},
|
|
);
|
|
return error.TestExpectedEqual;
|
|
}
|
|
},
|
|
.str => {
|
|
const r = ref.str;
|
|
const g = got.str;
|
|
if (@intFromEnum(r.start) != g.start or r.len != g.len) {
|
|
std.debug.print(
|
|
"inst_datas[{d}] (str) mismatch:\n" ++
|
|
" ref: start={d} len={d}\n" ++
|
|
" got: start={d} len={d}\n",
|
|
.{ idx, @intFromEnum(r.start), r.len, g.start, g.len },
|
|
);
|
|
return error.TestExpectedEqual;
|
|
}
|
|
},
|
|
else => {
|
|
std.debug.print(
|
|
"inst_datas[{d}]: unhandled tag {d} ({s}) in comparison\n",
|
|
.{ idx, @intFromEnum(tag), @tagName(tag) },
|
|
);
|
|
return error.TestUnexpectedResult;
|
|
},
|
|
}
|
|
}
|
|
|
|
/// Silent ZIR comparison: returns true if ZIR matches, false otherwise.
|
|
/// Unlike expectEqualZir, does not print diagnostics or return errors.
|
|
fn zirMatches(_: Allocator, ref: Zir, got: c.Zir) bool {
|
|
const ref_len: u32 = @intCast(ref.instructions.len);
|
|
if (ref_len != got.inst_len) {
|
|
std.debug.print(" inst_len: ref={d} got={d}\n", .{ ref_len, got.inst_len });
|
|
}
|
|
|
|
{
|
|
const elen: u32 = @intCast(ref.extra.len);
|
|
const slen: u32 = @intCast(ref.string_bytes.len);
|
|
std.debug.print(" inst_len: ref={d} got={d}\n", .{ ref_len, got.inst_len });
|
|
std.debug.print(" extra_len: ref={d} got={d} diff={d}\n", .{ elen, got.extra_len, @as(i64, elen) - @as(i64, got.extra_len) });
|
|
std.debug.print(" string_bytes_len: ref={d} got={d} diff={d}\n", .{ slen, got.string_bytes_len, @as(i64, slen) - @as(i64, got.string_bytes_len) });
|
|
}
|
|
|
|
const ref_tags = ref.instructions.items(.tag);
|
|
const ref_datas = ref.instructions.items(.data);
|
|
const min_len = @min(ref_len, got.inst_len);
|
|
var first_tag_mismatch: ?u32 = null;
|
|
for (0..min_len) |i| {
|
|
const ref_tag: u8 = @intFromEnum(ref_tags[i]);
|
|
const got_tag: u8 = @intCast(got.inst_tags[i]);
|
|
if (ref_tag != got_tag) {
|
|
first_tag_mismatch = @intCast(i);
|
|
break;
|
|
}
|
|
}
|
|
if (first_tag_mismatch) |ftm| {
|
|
const start = if (ftm > 15) ftm - 15 else 0;
|
|
const end = @min(ftm + 30, min_len);
|
|
std.debug.print(" first tag mismatch at inst[{d}]:\n", .{ftm});
|
|
for (start..end) |i| {
|
|
const ref_tag: u8 = @intFromEnum(ref_tags[i]);
|
|
const got_tag: u8 = @intCast(got.inst_tags[i]);
|
|
const marker: u8 = if (i == ftm) '>' else ' ';
|
|
if (ref_tag == 251) {
|
|
const ext_op: u16 = @intFromEnum(ref_datas[i].extended.opcode);
|
|
std.debug.print(" {c} [{d}] ref_tag=251(EXT:{d}) got_tag={d}\n", .{ marker, i, ext_op, got_tag });
|
|
} else {
|
|
std.debug.print(" {c} [{d}] ref_tag={d} got_tag={d}\n", .{ marker, i, ref_tag, got_tag });
|
|
}
|
|
}
|
|
// Tag histogram: count each tag in ref vs got and show diffs.
|
|
var ref_hist: [256]i32 = undefined;
|
|
var got_hist: [256]i32 = undefined;
|
|
for (&ref_hist) |*h| h.* = 0;
|
|
for (&got_hist) |*h| h.* = 0;
|
|
for (0..ref_len) |j| {
|
|
ref_hist[@intFromEnum(ref_tags[j])] += 1;
|
|
}
|
|
for (0..got.inst_len) |j| {
|
|
got_hist[@as(u8, @intCast(got.inst_tags[j]))] += 1;
|
|
}
|
|
std.debug.print(" tag histogram diff (ref-got):\n", .{});
|
|
for (0..256) |t| {
|
|
const diff = ref_hist[t] - got_hist[t];
|
|
if (diff != 0) {
|
|
std.debug.print(" tag {d}: ref={d} got={d} diff={d}\n", .{ t, ref_hist[t], got_hist[t], diff });
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
// Skip inst_datas comparison for now (extra indices shift).
|
|
// Go straight to extra/string_bytes.
|
|
if (ref_len != got.inst_len) return false;
|
|
|
|
// Compare string_bytes first (smaller diff).
|
|
const ref_sb_len2: u32 = @intCast(ref.string_bytes.len);
|
|
const sb_min = @min(ref_sb_len2, got.string_bytes_len);
|
|
for (0..sb_min) |i| {
|
|
if (ref.string_bytes[i] != got.string_bytes[i]) {
|
|
// Print surrounding context.
|
|
const ctx_start = if (i > 30) i - 30 else 0;
|
|
std.debug.print(" string_bytes[{d}] first diff (ref=0x{x:0>2} got=0x{x:0>2})\n", .{ i, ref.string_bytes[i], got.string_bytes[i] });
|
|
std.debug.print(" ref context: \"", .{});
|
|
for (ctx_start..@min(i + 30, sb_min)) |j| {
|
|
const ch = ref.string_bytes[j];
|
|
if (ch >= 0x20 and ch < 0x7f) {
|
|
std.debug.print("{c}", .{ch});
|
|
} else {
|
|
std.debug.print("\\x{x:0>2}", .{ch});
|
|
}
|
|
}
|
|
std.debug.print("\"\n", .{});
|
|
std.debug.print(" got context: \"", .{});
|
|
for (ctx_start..@min(i + 30, sb_min)) |j| {
|
|
const ch = got.string_bytes[j];
|
|
if (ch >= 0x20 and ch < 0x7f) {
|
|
std.debug.print("{c}", .{ch});
|
|
} else {
|
|
std.debug.print("\\x{x:0>2}", .{ch});
|
|
}
|
|
}
|
|
std.debug.print("\"\n", .{});
|
|
return false;
|
|
}
|
|
}
|
|
if (ref_sb_len2 != got.string_bytes_len) {
|
|
std.debug.print(" string_bytes_len mismatch: ref={d} got={d} (content matched up to {d})\n", .{ ref_sb_len2, got.string_bytes_len, sb_min });
|
|
// Print what ref has at the end.
|
|
if (ref_sb_len2 > got.string_bytes_len) {
|
|
const extra_start = got.string_bytes_len;
|
|
std.debug.print(" ref extra at [{d}]: \"", .{extra_start});
|
|
for (extra_start..@min(extra_start + 60, ref_sb_len2)) |j| {
|
|
const ch = ref.string_bytes[j];
|
|
if (ch >= 0x20 and ch < 0x7f) {
|
|
std.debug.print("{c}", .{ch});
|
|
} else {
|
|
std.debug.print("\\x{x:0>2}", .{ch});
|
|
}
|
|
}
|
|
std.debug.print("\"\n", .{});
|
|
}
|
|
return false;
|
|
}
|
|
|
|
const ref_extra_len2: u32 = @intCast(ref.extra.len);
|
|
if (ref_extra_len2 != got.extra_len) {
|
|
std.debug.print(" extra_len mismatch: ref={d} got={d}\n", .{ ref_extra_len2, got.extra_len });
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// Silent data comparison: returns true if fields match, false otherwise.
|
|
fn dataMatches(tag: Zir.Inst.Tag, ref: Zir.Inst.Data, got: c.ZirInstData) bool {
|
|
switch (tag) {
|
|
.extended => {
|
|
const r = ref.extended;
|
|
const g = got.extended;
|
|
return @intFromEnum(r.opcode) == g.opcode and
|
|
r.small == g.small and
|
|
r.operand == g.operand;
|
|
},
|
|
.declaration => {
|
|
const r = ref.declaration;
|
|
const g = got.declaration;
|
|
return @intFromEnum(r.src_node) == g.src_node and
|
|
r.payload_index == g.payload_index;
|
|
},
|
|
.break_inline => {
|
|
const r = ref.@"break";
|
|
const g = got.break_data;
|
|
return @intFromEnum(r.operand) == g.operand and
|
|
r.payload_index == g.payload_index;
|
|
},
|
|
.import => {
|
|
const r = ref.pl_tok;
|
|
const g = got.pl_tok;
|
|
return @intFromEnum(r.src_tok) == g.src_tok and
|
|
r.payload_index == g.payload_index;
|
|
},
|
|
.dbg_stmt => {
|
|
return ref.dbg_stmt.line == got.dbg_stmt.line and
|
|
ref.dbg_stmt.column == got.dbg_stmt.column;
|
|
},
|
|
.ensure_result_non_error,
|
|
.restore_err_ret_index_unconditional,
|
|
=> {
|
|
return @intFromEnum(ref.un_node.src_node) == got.un_node.src_node and
|
|
@intFromEnum(ref.un_node.operand) == got.un_node.operand;
|
|
},
|
|
.ret_implicit => {
|
|
return @intFromEnum(ref.un_tok.src_tok) == got.un_tok.src_tok and
|
|
@intFromEnum(ref.un_tok.operand) == got.un_tok.operand;
|
|
},
|
|
.func,
|
|
.func_inferred,
|
|
.array_type,
|
|
.array_type_sentinel,
|
|
.array_cat,
|
|
.array_init,
|
|
.array_init_ref,
|
|
.error_set_decl,
|
|
=> {
|
|
return @intFromEnum(ref.pl_node.src_node) == got.pl_node.src_node and
|
|
ref.pl_node.payload_index == got.pl_node.payload_index;
|
|
},
|
|
.ptr_type => {
|
|
return @as(u8, @bitCast(ref.ptr_type.flags)) == got.ptr_type.flags and
|
|
@intFromEnum(ref.ptr_type.size) == got.ptr_type.size and
|
|
ref.ptr_type.payload_index == got.ptr_type.payload_index;
|
|
},
|
|
.int_type => {
|
|
return @intFromEnum(ref.int_type.src_node) == got.int_type.src_node and
|
|
@intFromEnum(ref.int_type.signedness) == got.int_type.signedness and
|
|
ref.int_type.bit_count == got.int_type.bit_count;
|
|
},
|
|
.decl_val, .decl_ref => {
|
|
return @intFromEnum(ref.str_tok.start) == got.str_tok.start and
|
|
@intFromEnum(ref.str_tok.src_tok) == got.str_tok.src_tok;
|
|
},
|
|
.field_val, .field_ptr, .field_val_named, .field_ptr_named => {
|
|
return @intFromEnum(ref.pl_node.src_node) == got.pl_node.src_node and
|
|
ref.pl_node.payload_index == got.pl_node.payload_index;
|
|
},
|
|
.int => return ref.int == got.int_val,
|
|
.str => {
|
|
return @intFromEnum(ref.str.start) == got.str.start and
|
|
ref.str.len == got.str.len;
|
|
},
|
|
.@"defer" => {
|
|
return ref.@"defer".index == got.defer_data.index and
|
|
ref.@"defer".len == got.defer_data.len;
|
|
},
|
|
else => return false,
|
|
}
|
|
}
|
|
|
|
const corpus_files = .{
|
|
.{ "astgen_test.zig", @embedFile("astgen_test.zig") },
|
|
.{ "build.zig", @embedFile("build.zig") },
|
|
.{ "parser_test.zig", @embedFile("parser_test.zig") },
|
|
.{ "test_all.zig", @embedFile("test_all.zig") },
|
|
.{ "tokenizer_test.zig", @embedFile("tokenizer_test.zig") },
|
|
};
|
|
|
|
fn corpusCheck(gpa: Allocator, name: []const u8, source: [:0]const u8) !void {
|
|
var tree = try Ast.parse(gpa, source, .zig);
|
|
defer tree.deinit(gpa);
|
|
|
|
var ref_zir = try AstGen.generate(gpa, tree);
|
|
defer ref_zir.deinit(gpa);
|
|
|
|
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);
|
|
|
|
if (c_zir.has_compile_errors) {
|
|
std.debug.print(" {s} -> has_compile_errors\n", .{name});
|
|
return error.ZirCompileErrors;
|
|
}
|
|
|
|
if (zirMatches(gpa, ref_zir, c_zir)) {
|
|
return;
|
|
} else {
|
|
std.debug.print(" {s} -> zir mismatch\n", .{name});
|
|
return error.ZirMismatch;
|
|
}
|
|
}
|
|
|
|
test "astgen: struct single field" {
|
|
const gpa = std.testing.allocator;
|
|
const source: [:0]const u8 = "const T = struct { x: u32 };";
|
|
var ref_zir = try refZir(gpa, source);
|
|
defer ref_zir.deinit(gpa);
|
|
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);
|
|
try expectEqualZir(gpa, ref_zir, c_zir);
|
|
}
|
|
|
|
test "astgen: struct multiple fields" {
|
|
const gpa = std.testing.allocator;
|
|
const source: [:0]const u8 = "const T = struct { x: u32, y: bool };";
|
|
var ref_zir = try refZir(gpa, source);
|
|
defer ref_zir.deinit(gpa);
|
|
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);
|
|
try expectEqualZir(gpa, ref_zir, c_zir);
|
|
}
|
|
|
|
test "astgen: struct field with default" {
|
|
const gpa = std.testing.allocator;
|
|
const source: [:0]const u8 = "const T = struct { x: u32 = 0 };";
|
|
var ref_zir = try refZir(gpa, source);
|
|
defer ref_zir.deinit(gpa);
|
|
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);
|
|
try expectEqualZir(gpa, ref_zir, c_zir);
|
|
}
|
|
|
|
test "astgen: struct field with align" {
|
|
const gpa = std.testing.allocator;
|
|
const source: [:0]const u8 = "const T = struct { x: u32 align(4) };";
|
|
var ref_zir = try refZir(gpa, source);
|
|
defer ref_zir.deinit(gpa);
|
|
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);
|
|
try expectEqualZir(gpa, ref_zir, c_zir);
|
|
}
|
|
|
|
test "astgen: struct comptime field" {
|
|
const gpa = std.testing.allocator;
|
|
const source: [:0]const u8 = "const T = struct { comptime x: u32 = 0 };";
|
|
var ref_zir = try refZir(gpa, source);
|
|
defer ref_zir.deinit(gpa);
|
|
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);
|
|
try expectEqualZir(gpa, ref_zir, c_zir);
|
|
}
|
|
|
|
test "astgen: empty error set" {
|
|
const gpa = std.testing.allocator;
|
|
const source: [:0]const u8 = "const E = error{};";
|
|
var ref_zir = try refZir(gpa, source);
|
|
defer ref_zir.deinit(gpa);
|
|
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);
|
|
try expectEqualZir(gpa, ref_zir, c_zir);
|
|
}
|
|
|
|
test "astgen: error set with members" {
|
|
const gpa = std.testing.allocator;
|
|
const source: [:0]const u8 = "const E = error{ OutOfMemory, OutOfTime };";
|
|
var ref_zir = try refZir(gpa, source);
|
|
defer ref_zir.deinit(gpa);
|
|
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);
|
|
try expectEqualZir(gpa, ref_zir, c_zir);
|
|
}
|
|
|
|
test "astgen: extern var" {
|
|
const gpa = std.testing.allocator;
|
|
const source: [:0]const u8 = "extern var x: u32;";
|
|
var ref_zir = try refZir(gpa, source);
|
|
defer ref_zir.deinit(gpa);
|
|
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);
|
|
try expectEqualZir(gpa, ref_zir, c_zir);
|
|
}
|
|
|
|
test "astgen: corpus test_all.zig" {
|
|
const gpa = std.testing.allocator;
|
|
try corpusCheck(gpa, "test_all.zig", @embedFile("test_all.zig"));
|
|
}
|
|
|
|
test "astgen: corpus" {
|
|
if (true) return error.SkipZigTest;
|
|
const gpa = std.testing.allocator;
|
|
|
|
var any_fail = false;
|
|
inline for (corpus_files) |entry| {
|
|
corpusCheck(gpa, entry[0], entry[1]) catch {
|
|
any_fail = true;
|
|
};
|
|
}
|
|
if (any_fail) return error.ZirMismatch;
|
|
}
|