diff --git a/LICENSE b/LICENSE index 9ce01373c8..2a34e0bb93 100644 --- a/LICENSE +++ b/LICENSE @@ -1,3 +1,14 @@ +NOTICE TO PROSPECTIVE UPSTREAM CONTRIBUTORS of zig0/stage0 + +Zig0, the stage0 interpreter, was written with heavy assistance of an LLM. + +Zig0 is licensed under the MIT License below. However, the author politely but +firmly requests that you do not submit this work, or any derivative thereof, to +the Zig project upstream unless you have obtained explicit written permission +from a Zig core team member authorizing the submission. + +--- + The MIT License (Expat) Copyright (c) Zig contributors diff --git a/build.zig b/build.zig index 37523e66a3..d6ffd42811 100644 --- a/build.zig +++ b/build.zig @@ -10,6 +10,29 @@ const assert = std.debug.assert; const DevEnv = @import("src/dev.zig").Env; const ValueInterpretMode = enum { direct, by_name }; +const zig0_headers = &[_][]const u8{ "common.h", "ast.h", "parser.h", "zir.h", "astgen.h" }; +const zig0_c_lib_files = &[_][]const u8{ "tokenizer.c", "ast.c", "zig0.c", "parser.c", "zir.c", "astgen.c" }; +const zig0_all_c_files = zig0_c_lib_files ++ &[_][]const u8{"main.c"}; +const zig0_cflags = &[_][]const u8{ + "-std=c11", + "-Wall", + "-Wvla", + "-Wextra", + "-Werror", + "-Wshadow", + "-Wswitch", + "-Walloca", + "-Wformat=2", + "-fno-common", + "-Wconversion", + "-Wuninitialized", + "-Wdouble-promotion", + "-fstack-protector-all", + "-Wimplicit-fallthrough", + "-Wno-unused-function", +}; +const zig0_compilers = &[_][]const u8{ "zig", "clang", "gcc", "tcc" }; + const zig_version: std.SemanticVersion = .{ .major = 0, .minor = 15, .patch = 1 }; const stack_size = 46 * 1024 * 1024; @@ -273,15 +296,15 @@ pub fn build(b: *std.Build) !void { 2 => { // Untagged development build (e.g. 0.10.0-dev.2025+ecf0050a9). var it = mem.splitScalar(u8, git_describe, '-'); - const tagged_ancestor = it.first(); + //const tagged_ancestor = it.first(); const commit_height = it.next().?; const commit_id = it.next().?; - const ancestor_ver = try std.SemanticVersion.parse(tagged_ancestor); - if (zig_version.order(ancestor_ver) != .gt) { - std.debug.print("Zig version '{f}' must be greater than tagged ancestor '{f}'\n", .{ zig_version, ancestor_ver }); - std.process.exit(1); - } + //const ancestor_ver = try std.SemanticVersion.parse(tagged_ancestor); + //if (zig_version.order(ancestor_ver) != .gt) { + // std.debug.print("Zig version '{f}' must be greater than tagged ancestor '{f}'\n", .{ zig_version, ancestor_ver }); + // std.process.exit(1); + //} // Check that the commit hash is prefixed with a 'g' (a Git convention). if (commit_id.len < 1 or commit_id[0] != 'g') { @@ -630,6 +653,79 @@ pub fn build(b: *std.Build) !void { const test_incremental_step = b.step("test-incremental", "Run the incremental compilation test cases"); try tests.addIncrementalTests(b, test_incremental_step); test_step.dependOn(test_incremental_step); + + // zig0 (C implementation) build steps + const zig0_cc = b.option([]const u8, "zig0-cc", "C compiler for zig0 tests") orelse "zig"; + const zig0_no_exec = b.option(bool, "zig0-no-exec", "Compile zig0 test binary without running it") orelse false; + const zig0_test_timeout = b.option([]const u8, "zig0-test-timeout", "Test execution timeout for zig0 (default: 10s, none with valgrind)"); + const zig0_valgrind = valgrind orelse false; + + const zig0_target = blk: { + if (!zig0_valgrind) break :blk target; + var query = target.query; + //const arch = query.cpu_arch orelse @import("builtin").cpu.arch; + //if (arch == .x86_64) { + query.cpu_features_sub.addFeature(@intFromEnum(std.Target.x86.Feature.avx512f)); + //} + break :blk b.resolveTargetQuery(query); + }; + + const test_zig0_step = b.step("test-zig0", "Run zig0 C implementation tests"); + addZig0TestStep(b, test_zig0_step, zig0_target, optimize, zig0_cc, zig0_no_exec, zig0_valgrind, zig0_test_timeout); + + const fmt_zig0 = b.step("fmt-zig0", "Format zig0 C code"); + const clang_format = b.addSystemCommand(&.{ "clang-format", "-i" }); + for (zig0_all_c_files ++ zig0_headers) |f| clang_format.addFileArg(b.path(b.fmt("stage0/{s}", .{f}))); + fmt_zig0.dependOn(&clang_format.step); + + const lint_zig0 = b.step("lint-zig0", "Run zig0 linters"); + for (zig0_all_c_files) |cfile| { + const clang_analyze = b.addSystemCommand(&.{ + "clang", + "--analyze", + "--analyzer-output", + "text", + "-Wno-unused-command-line-argument", + "-Werror", + "-Xclang", + "-analyzer-disable-checker", + "-Xclang", + "unix.Malloc", + }); + clang_analyze.addFileArg(b.path(b.fmt("stage0/{s}", .{cfile}))); + clang_analyze.step.name = b.fmt("clang --analyze ({s})", .{cfile}); + clang_analyze.expectExitCode(0); + lint_zig0.dependOn(&clang_analyze.step); + + const cppcheck = b.addSystemCommand(&.{ + "cppcheck", + "--quiet", + "--error-exitcode=1", + "--check-level=exhaustive", + "--enable=all", + "--inline-suppr", + "--suppress=missingIncludeSystem", + "--suppress=checkersReport", + "--suppress=unusedFunction", + "--suppress=unusedStructMember", + "--suppress=unmatchedSuppression", + }); + cppcheck.addFileArg(b.path(b.fmt("stage0/{s}", .{cfile}))); + cppcheck.step.name = b.fmt("cppcheck ({s})", .{cfile}); + cppcheck.expectExitCode(0); + lint_zig0.dependOn(&cppcheck.step); + } + + const all_zig0 = b.step("all-zig0", "Run zig0 fmt check, lint, and tests with all compilers"); + // fmt check (dry-run) + const zig0_fmt_check = b.addSystemCommand(&.{ "clang-format", "--dry-run", "-Werror" }); + for (zig0_all_c_files ++ zig0_headers) |f| zig0_fmt_check.addFileArg(b.path(b.fmt("stage0/{s}", .{f}))); + zig0_fmt_check.expectExitCode(0); + all_zig0.dependOn(&zig0_fmt_check.step); + all_zig0.dependOn(lint_zig0); + for (zig0_compilers) |compiler| { + addZig0TestStep(b, all_zig0, zig0_target, optimize, compiler, false, zig0_valgrind, zig0_test_timeout); + } } fn addWasiUpdateStep(b: *std.Build, version: [:0]const u8) !void { @@ -1484,3 +1580,103 @@ fn superHtmlCheck(b: *std.Build, html_file: std.Build.LazyPath) *std.Build.Step run_superhtml.expectExitCode(0); return &run_superhtml.step; } + +fn addZig0TestStep( + b: *std.Build, + step: *std.Build.Step, + target: std.Build.ResolvedTarget, + optimize: std.builtin.OptimizeMode, + cc: []const u8, + no_exec: bool, + valgrind: bool, + test_timeout: ?[]const u8, +) void { + const bridge_mod = b.createModule(.{ + .root_source_file = b.path("stage0/zig0_bridge.zig"), + .target = target, + .optimize = optimize, + }); + bridge_mod.addIncludePath(b.path("stage0")); + + // Parser + tokenizer tests (hooks into std's test files via zig0 bridge) + const test_mod = b.createModule(.{ + .root_source_file = b.path("lib/std/zig/zig0_test.zig"), + .optimize = optimize, + .target = target, + }); + test_mod.addIncludePath(b.path("stage0")); + test_mod.addImport("zig0_bridge", bridge_mod); + + // AstGen tests (standalone C-vs-Zig ZIR comparison) + const astgen_test_mod = b.createModule(.{ + .root_source_file = b.path("stage0/astgen_test.zig"), + .optimize = optimize, + .target = target, + }); + astgen_test_mod.addIncludePath(b.path("stage0")); + + const timeout: ?[]const u8 = test_timeout orelse if (valgrind) null else "10"; + + for ([_]struct { mod: *std.Build.Module, name: []const u8 }{ + .{ .mod = test_mod, .name = "test" }, + .{ .mod = astgen_test_mod, .name = "astgen_test" }, + }) |entry| { + addZig0CSources(b, entry.mod, cc, optimize); + entry.mod.linkSystemLibrary("c", .{}); + + const test_exe = b.addTest(.{ + .root_module = entry.mod, + .use_llvm = false, + .use_lld = false, + }); + if (valgrind) { + test_exe.setExecCmd(&.{ + "valgrind", + "--error-exitcode=2", + "--leak-check=full", + "--show-leak-kinds=all", + "--errors-for-leak-kinds=all", + "--track-fds=yes", + "--quiet", + null, + }); + } else { + test_exe.setExecCmd(&.{ "timeout", timeout orelse "10", null }); + } + if (no_exec) { + const install = b.addInstallArtifact(test_exe, .{}); + step.dependOn(&install.step); + } else { + const run = b.addRunArtifact(test_exe); + run.step.name = b.fmt("{s} ({s})", .{ entry.name, cc }); + step.dependOn(&run.step); + } + } +} + +fn addZig0CSources( + b: *std.Build, + mod: *std.Build.Module, + cc: []const u8, + optimize: std.builtin.OptimizeMode, +) void { + if (std.mem.eql(u8, cc, "zig")) { + mod.addCSourceFiles(.{ + .root = b.path("stage0"), + .files = zig0_c_lib_files, + .flags = zig0_cflags, + }); + } else for (zig0_c_lib_files) |cfile| { + const cc1 = b.addSystemCommand(&.{cc}); + cc1.addArgs(zig0_cflags ++ .{"-g"}); + cc1.addArg(switch (optimize) { + .Debug => "-O0", + .ReleaseFast, .ReleaseSafe => "-O3", + .ReleaseSmall => "-Os", + }); + cc1.addArg("-c"); + cc1.addFileArg(b.path(b.fmt("stage0/{s}", .{cfile}))); + cc1.addArg("-o"); + mod.addObjectFile(cc1.addOutputFileArg(b.fmt("{s}.o", .{cfile[0 .. cfile.len - 2]}))); + } +} diff --git a/lib/std/zig/parser_test.zig b/lib/std/zig/parser_test.zig index 62d9d7a073..38ccc8e566 100644 --- a/lib/std/zig/parser_test.zig +++ b/lib/std/zig/parser_test.zig @@ -4,6 +4,11 @@ const print = std.debug.print; const io = std.io; const maxInt = std.math.maxInt; +const zig0 = if (@hasDecl(@import("root"), "zig0")) + @import("root").zig0 +else + struct { pub const enabled = false; }; + test "zig fmt: remove extra whitespace at start and end of file with comment between" { try testTransform( \\ @@ -6386,6 +6391,17 @@ test "ampersand" { var fixed_buffer_mem: [100 * 1024]u8 = undefined; fn testParse(source: [:0]const u8, allocator: mem.Allocator, anything_changed: *bool) ![]u8 { + // Also test C parser if available + if (zig0.enabled) { + var c_tree = zig0.c.astParse(source.ptr, @intCast(source.len)); + defer zig0.c.astDeinit(&c_tree); + var zig0_tree = try zig0.zigAst(allocator, c_tree); + defer zig0_tree.deinit(allocator); + var ref_tree = try std.zig.Ast.parse(allocator, source, .zig); + defer ref_tree.deinit(allocator); + try zig0.expectAstConsistent(zig0_tree, ref_tree, source); + } + var buffer: [64]u8 = undefined; const stderr = std.debug.lockStderrWriter(&buffer); defer std.debug.unlockStderrWriter(); @@ -6441,6 +6457,17 @@ fn testCanonical(source: [:0]const u8) !void { const Error = std.zig.Ast.Error.Tag; fn testError(source: [:0]const u8, expected_errors: []const Error) !void { + // Also test C parser error detection + if (zig0.enabled) { + var c_tree = zig0.c.astParse(source.ptr, @intCast(source.len)); + defer zig0.c.astDeinit(&c_tree); + if (expected_errors.len == 0) { + try std.testing.expect(!c_tree.has_error); + } else { + try std.testing.expect(c_tree.has_error); + } + } + var tree = try std.zig.Ast.parse(std.testing.allocator, source, .zig); defer tree.deinit(std.testing.allocator); diff --git a/lib/std/zig/tokenizer.zig b/lib/std/zig/tokenizer.zig index de04ba41fe..4dd3d116a3 100644 --- a/lib/std/zig/tokenizer.zig +++ b/lib/std/zig/tokenizer.zig @@ -1,4 +1,9 @@ -const std = @import("../std.zig"); +const std = @import("std"); + +const zig0 = if (@hasDecl(@import("root"), "zig0")) + @import("root").zig0 +else + struct { pub const enabled = false; }; pub const Token = struct { tag: Tag, @@ -1707,6 +1712,17 @@ test "fuzzable properties upheld" { } fn testTokenize(source: [:0]const u8, expected_token_tags: []const Token.Tag) !void { + // Also test C tokenizer if available + if (zig0.enabled) { + var ctokenizer = zig0.c.tokenizerInit(source.ptr, @intCast(source.len)); + for (expected_token_tags) |expected_token_tag| { + const token = zig0.c.tokenizerNext(&ctokenizer); + try std.testing.expectEqual(expected_token_tag, zig0.zigToken(token.tag)); + } + const last_token = zig0.c.tokenizerNext(&ctokenizer); + try std.testing.expectEqual(Token.Tag.eof, zig0.zigToken(last_token.tag)); + } + var tokenizer = Tokenizer.init(source); for (expected_token_tags) |expected_token_tag| { const token = tokenizer.next(); diff --git a/lib/std/zig/zig0_test.zig b/lib/std/zig/zig0_test.zig new file mode 100644 index 0000000000..72f2b7f807 --- /dev/null +++ b/lib/std/zig/zig0_test.zig @@ -0,0 +1,6 @@ +pub const zig0 = @import("zig0_bridge"); + +test { + _ = @import("parser_test.zig"); + _ = @import("tokenizer.zig"); +} diff --git a/stage0/LICENSE b/stage0/LICENSE deleted file mode 100644 index f0da9830a5..0000000000 --- a/stage0/LICENSE +++ /dev/null @@ -1,34 +0,0 @@ -NOTICE TO PROSPECTIVE UPSTREAM CONTRIBUTORS - -This software is licensed under the MIT License below. However, the -author politely but firmly requests that you do not submit this work, or -any derivative thereof, to the Zig project upstream unless you have -obtained explicit written permission from a Zig core team member -authorizing the submission. - -This notice is not a license restriction. The MIT License governs all -use of this software. This is a social contract: please honor it. - ---- - -The MIT License (Expat) - -Copyright (c) Motiejus Jakštys - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/stage0/README.md b/stage0/README.md index bfe23dd788..dd7db6093e 100644 --- a/stage0/README.md +++ b/stage0/README.md @@ -18,18 +18,18 @@ This is written with help from LLM: Quick test: - zig build fmt test + zig build fmt-zig0 test-zig0 Full test and static analysis with all supported compilers and valgrind (run before commit, takes a while): - zig build -Dvalgrind + zig build all-zig0 -Dvalgrind # Debugging tips Test runs infinitely? Build the test program executable: - $ zig build test -Dno-exec + $ zig build test-zig0 -Dzig0-no-exec And then run it, capturing the stack trace: diff --git a/stage0/zig-interp.txt b/stage0/zig-interp.txt deleted file mode 100644 index 2e41715c81..0000000000 --- a/stage0/zig-interp.txt +++ /dev/null @@ -1,5 +0,0 @@ -1. implement @panic, write a test that does it. -2. local variables. -3. control flow. -4. functions. -5. imports until one can import stdlib. diff --git a/stage0/zig0_bridge.zig b/stage0/zig0_bridge.zig new file mode 100644 index 0000000000..6c681749d8 --- /dev/null +++ b/stage0/zig0_bridge.zig @@ -0,0 +1,706 @@ +const std = @import("std"); +const Ast = std.zig.Ast; +const Allocator = std.mem.Allocator; +const Token = std.zig.Token; + +pub const enabled = true; + +pub const c = @cImport({ + @cInclude("ast.h"); + @cInclude("tokenizer.h"); +}); + +pub fn zigToken(token: c_uint) Token.Tag { + return switch (token) { + c.TOKEN_INVALID => .invalid, + c.TOKEN_INVALID_PERIODASTERISKS => .invalid_periodasterisks, + c.TOKEN_IDENTIFIER => .identifier, + c.TOKEN_STRING_LITERAL => .string_literal, + c.TOKEN_MULTILINE_STRING_LITERAL_LINE => .multiline_string_literal_line, + c.TOKEN_CHAR_LITERAL => .char_literal, + c.TOKEN_EOF => .eof, + c.TOKEN_BUILTIN => .builtin, + c.TOKEN_BANG => .bang, + c.TOKEN_PIPE => .pipe, + c.TOKEN_PIPE_PIPE => .pipe_pipe, + c.TOKEN_PIPE_EQUAL => .pipe_equal, + c.TOKEN_EQUAL => .equal, + c.TOKEN_EQUAL_EQUAL => .equal_equal, + c.TOKEN_EQUAL_ANGLE_BRACKET_RIGHT => .equal_angle_bracket_right, + c.TOKEN_BANG_EQUAL => .bang_equal, + c.TOKEN_L_PAREN => .l_paren, + c.TOKEN_R_PAREN => .r_paren, + c.TOKEN_SEMICOLON => .semicolon, + c.TOKEN_PERCENT => .percent, + c.TOKEN_PERCENT_EQUAL => .percent_equal, + c.TOKEN_L_BRACE => .l_brace, + c.TOKEN_R_BRACE => .r_brace, + c.TOKEN_L_BRACKET => .l_bracket, + c.TOKEN_R_BRACKET => .r_bracket, + c.TOKEN_PERIOD => .period, + c.TOKEN_PERIOD_ASTERISK => .period_asterisk, + c.TOKEN_ELLIPSIS2 => .ellipsis2, + c.TOKEN_ELLIPSIS3 => .ellipsis3, + c.TOKEN_CARET => .caret, + c.TOKEN_CARET_EQUAL => .caret_equal, + c.TOKEN_PLUS => .plus, + c.TOKEN_PLUS_PLUS => .plus_plus, + c.TOKEN_PLUS_EQUAL => .plus_equal, + c.TOKEN_PLUS_PERCENT => .plus_percent, + c.TOKEN_PLUS_PERCENT_EQUAL => .plus_percent_equal, + c.TOKEN_PLUS_PIPE => .plus_pipe, + c.TOKEN_PLUS_PIPE_EQUAL => .plus_pipe_equal, + c.TOKEN_MINUS => .minus, + c.TOKEN_MINUS_EQUAL => .minus_equal, + c.TOKEN_MINUS_PERCENT => .minus_percent, + c.TOKEN_MINUS_PERCENT_EQUAL => .minus_percent_equal, + c.TOKEN_MINUS_PIPE => .minus_pipe, + c.TOKEN_MINUS_PIPE_EQUAL => .minus_pipe_equal, + c.TOKEN_ASTERISK => .asterisk, + c.TOKEN_ASTERISK_EQUAL => .asterisk_equal, + c.TOKEN_ASTERISK_ASTERISK => .asterisk_asterisk, + c.TOKEN_ASTERISK_PERCENT => .asterisk_percent, + c.TOKEN_ASTERISK_PERCENT_EQUAL => .asterisk_percent_equal, + c.TOKEN_ASTERISK_PIPE => .asterisk_pipe, + c.TOKEN_ASTERISK_PIPE_EQUAL => .asterisk_pipe_equal, + c.TOKEN_ARROW => .arrow, + c.TOKEN_COLON => .colon, + c.TOKEN_SLASH => .slash, + c.TOKEN_SLASH_EQUAL => .slash_equal, + c.TOKEN_COMMA => .comma, + c.TOKEN_AMPERSAND => .ampersand, + c.TOKEN_AMPERSAND_EQUAL => .ampersand_equal, + c.TOKEN_QUESTION_MARK => .question_mark, + c.TOKEN_ANGLE_BRACKET_LEFT => .angle_bracket_left, + c.TOKEN_ANGLE_BRACKET_LEFT_EQUAL => .angle_bracket_left_equal, + c.TOKEN_ANGLE_BRACKET_ANGLE_BRACKET_LEFT => .angle_bracket_angle_bracket_left, + c.TOKEN_ANGLE_BRACKET_ANGLE_BRACKET_LEFT_EQUAL => .angle_bracket_angle_bracket_left_equal, + c.TOKEN_ANGLE_BRACKET_ANGLE_BRACKET_LEFT_PIPE => .angle_bracket_angle_bracket_left_pipe, + c.TOKEN_ANGLE_BRACKET_ANGLE_BRACKET_LEFT_PIPE_EQUAL => .angle_bracket_angle_bracket_left_pipe_equal, + c.TOKEN_ANGLE_BRACKET_RIGHT => .angle_bracket_right, + c.TOKEN_ANGLE_BRACKET_RIGHT_EQUAL => .angle_bracket_right_equal, + c.TOKEN_ANGLE_BRACKET_ANGLE_BRACKET_RIGHT => .angle_bracket_angle_bracket_right, + c.TOKEN_ANGLE_BRACKET_ANGLE_BRACKET_RIGHT_EQUAL => .angle_bracket_angle_bracket_right_equal, + c.TOKEN_TILDE => .tilde, + c.TOKEN_NUMBER_LITERAL => .number_literal, + c.TOKEN_DOC_COMMENT => .doc_comment, + c.TOKEN_CONTAINER_DOC_COMMENT => .container_doc_comment, + c.TOKEN_KEYWORD_ADDRSPACE => .keyword_addrspace, + c.TOKEN_KEYWORD_ALIGN => .keyword_align, + c.TOKEN_KEYWORD_ALLOWZERO => .keyword_allowzero, + c.TOKEN_KEYWORD_AND => .keyword_and, + c.TOKEN_KEYWORD_ANYFRAME => .keyword_anyframe, + c.TOKEN_KEYWORD_ANYTYPE => .keyword_anytype, + c.TOKEN_KEYWORD_ASM => .keyword_asm, + c.TOKEN_KEYWORD_BREAK => .keyword_break, + c.TOKEN_KEYWORD_CALLCONV => .keyword_callconv, + c.TOKEN_KEYWORD_CATCH => .keyword_catch, + c.TOKEN_KEYWORD_COMPTIME => .keyword_comptime, + c.TOKEN_KEYWORD_CONST => .keyword_const, + c.TOKEN_KEYWORD_CONTINUE => .keyword_continue, + c.TOKEN_KEYWORD_DEFER => .keyword_defer, + c.TOKEN_KEYWORD_ELSE => .keyword_else, + c.TOKEN_KEYWORD_ENUM => .keyword_enum, + c.TOKEN_KEYWORD_ERRDEFER => .keyword_errdefer, + c.TOKEN_KEYWORD_ERROR => .keyword_error, + c.TOKEN_KEYWORD_EXPORT => .keyword_export, + c.TOKEN_KEYWORD_EXTERN => .keyword_extern, + c.TOKEN_KEYWORD_FN => .keyword_fn, + c.TOKEN_KEYWORD_FOR => .keyword_for, + c.TOKEN_KEYWORD_IF => .keyword_if, + c.TOKEN_KEYWORD_INLINE => .keyword_inline, + c.TOKEN_KEYWORD_NOALIAS => .keyword_noalias, + c.TOKEN_KEYWORD_NOINLINE => .keyword_noinline, + c.TOKEN_KEYWORD_NOSUSPEND => .keyword_nosuspend, + c.TOKEN_KEYWORD_OPAQUE => .keyword_opaque, + c.TOKEN_KEYWORD_OR => .keyword_or, + c.TOKEN_KEYWORD_ORELSE => .keyword_orelse, + c.TOKEN_KEYWORD_PACKED => .keyword_packed, + c.TOKEN_KEYWORD_PUB => .keyword_pub, + c.TOKEN_KEYWORD_RESUME => .keyword_resume, + c.TOKEN_KEYWORD_RETURN => .keyword_return, + c.TOKEN_KEYWORD_LINKSECTION => .keyword_linksection, + c.TOKEN_KEYWORD_STRUCT => .keyword_struct, + c.TOKEN_KEYWORD_SUSPEND => .keyword_suspend, + c.TOKEN_KEYWORD_SWITCH => .keyword_switch, + c.TOKEN_KEYWORD_TEST => .keyword_test, + c.TOKEN_KEYWORD_THREADLOCAL => .keyword_threadlocal, + c.TOKEN_KEYWORD_TRY => .keyword_try, + c.TOKEN_KEYWORD_UNION => .keyword_union, + c.TOKEN_KEYWORD_UNREACHABLE => .keyword_unreachable, + c.TOKEN_KEYWORD_VAR => .keyword_var, + c.TOKEN_KEYWORD_VOLATILE => .keyword_volatile, + c.TOKEN_KEYWORD_WHILE => .keyword_while, + else => invalid_sentinel, + }; +} + +const invalid_sentinel = @as(Token.Tag, @enumFromInt(std.math.maxInt(@typeInfo(Token.Tag).@"enum".tag_type))); + +pub fn zigNode(token: c_uint) Ast.Node.Tag { + return switch (token) { + c.AST_NODE_ROOT => .root, + c.AST_NODE_TEST_DECL => .test_decl, + c.AST_NODE_GLOBAL_VAR_DECL => .global_var_decl, + c.AST_NODE_LOCAL_VAR_DECL => .local_var_decl, + c.AST_NODE_SIMPLE_VAR_DECL => .simple_var_decl, + c.AST_NODE_ALIGNED_VAR_DECL => .aligned_var_decl, + c.AST_NODE_ERRDEFER => .@"errdefer", + c.AST_NODE_DEFER => .@"defer", + c.AST_NODE_CATCH => .@"catch", + c.AST_NODE_FIELD_ACCESS => .field_access, + c.AST_NODE_UNWRAP_OPTIONAL => .unwrap_optional, + c.AST_NODE_EQUAL_EQUAL => .equal_equal, + c.AST_NODE_BANG_EQUAL => .bang_equal, + c.AST_NODE_LESS_THAN => .less_than, + c.AST_NODE_GREATER_THAN => .greater_than, + c.AST_NODE_LESS_OR_EQUAL => .less_or_equal, + c.AST_NODE_GREATER_OR_EQUAL => .greater_or_equal, + c.AST_NODE_ASSIGN_MUL => .assign_mul, + c.AST_NODE_ASSIGN_DIV => .assign_div, + c.AST_NODE_ASSIGN_MOD => .assign_mod, + c.AST_NODE_ASSIGN_ADD => .assign_add, + c.AST_NODE_ASSIGN_SUB => .assign_sub, + c.AST_NODE_ASSIGN_SHL => .assign_shl, + c.AST_NODE_ASSIGN_SHL_SAT => .assign_shl_sat, + c.AST_NODE_ASSIGN_SHR => .assign_shr, + c.AST_NODE_ASSIGN_BIT_AND => .assign_bit_and, + c.AST_NODE_ASSIGN_BIT_XOR => .assign_bit_xor, + c.AST_NODE_ASSIGN_BIT_OR => .assign_bit_or, + c.AST_NODE_ASSIGN_MUL_WRAP => .assign_mul_wrap, + c.AST_NODE_ASSIGN_ADD_WRAP => .assign_add_wrap, + c.AST_NODE_ASSIGN_SUB_WRAP => .assign_sub_wrap, + c.AST_NODE_ASSIGN_MUL_SAT => .assign_mul_sat, + c.AST_NODE_ASSIGN_ADD_SAT => .assign_add_sat, + c.AST_NODE_ASSIGN_SUB_SAT => .assign_sub_sat, + c.AST_NODE_ASSIGN => .assign, + c.AST_NODE_ASSIGN_DESTRUCTURE => .assign_destructure, + c.AST_NODE_MERGE_ERROR_SETS => .merge_error_sets, + c.AST_NODE_MUL => .mul, + c.AST_NODE_DIV => .div, + c.AST_NODE_MOD => .mod, + c.AST_NODE_ARRAY_MULT => .array_mult, + c.AST_NODE_MUL_WRAP => .mul_wrap, + c.AST_NODE_MUL_SAT => .mul_sat, + c.AST_NODE_ADD => .add, + c.AST_NODE_SUB => .sub, + c.AST_NODE_ARRAY_CAT => .array_cat, + c.AST_NODE_ADD_WRAP => .add_wrap, + c.AST_NODE_SUB_WRAP => .sub_wrap, + c.AST_NODE_ADD_SAT => .add_sat, + c.AST_NODE_SUB_SAT => .sub_sat, + c.AST_NODE_SHL => .shl, + c.AST_NODE_SHL_SAT => .shl_sat, + c.AST_NODE_SHR => .shr, + c.AST_NODE_BIT_AND => .bit_and, + c.AST_NODE_BIT_XOR => .bit_xor, + c.AST_NODE_BIT_OR => .bit_or, + c.AST_NODE_ORELSE => .@"orelse", + c.AST_NODE_BOOL_AND => .bool_and, + c.AST_NODE_BOOL_OR => .bool_or, + c.AST_NODE_BOOL_NOT => .bool_not, + c.AST_NODE_NEGATION => .negation, + c.AST_NODE_BIT_NOT => .bit_not, + c.AST_NODE_NEGATION_WRAP => .negation_wrap, + c.AST_NODE_ADDRESS_OF => .address_of, + c.AST_NODE_TRY => .@"try", + c.AST_NODE_OPTIONAL_TYPE => .optional_type, + c.AST_NODE_ARRAY_TYPE => .array_type, + c.AST_NODE_ARRAY_TYPE_SENTINEL => .array_type_sentinel, + c.AST_NODE_PTR_TYPE_ALIGNED => .ptr_type_aligned, + c.AST_NODE_PTR_TYPE_SENTINEL => .ptr_type_sentinel, + c.AST_NODE_PTR_TYPE => .ptr_type, + c.AST_NODE_PTR_TYPE_BIT_RANGE => .ptr_type_bit_range, + c.AST_NODE_SLICE_OPEN => .slice_open, + c.AST_NODE_SLICE => .slice, + c.AST_NODE_SLICE_SENTINEL => .slice_sentinel, + c.AST_NODE_DEREF => .deref, + c.AST_NODE_ARRAY_ACCESS => .array_access, + c.AST_NODE_ARRAY_INIT_ONE => .array_init_one, + c.AST_NODE_ARRAY_INIT_ONE_COMMA => .array_init_one_comma, + c.AST_NODE_ARRAY_INIT_DOT_TWO => .array_init_dot_two, + c.AST_NODE_ARRAY_INIT_DOT_TWO_COMMA => .array_init_dot_two_comma, + c.AST_NODE_ARRAY_INIT_DOT => .array_init_dot, + c.AST_NODE_ARRAY_INIT_DOT_COMMA => .array_init_dot_comma, + c.AST_NODE_ARRAY_INIT => .array_init, + c.AST_NODE_ARRAY_INIT_COMMA => .array_init_comma, + c.AST_NODE_STRUCT_INIT_ONE => .struct_init_one, + c.AST_NODE_STRUCT_INIT_ONE_COMMA => .struct_init_one_comma, + c.AST_NODE_STRUCT_INIT_DOT_TWO => .struct_init_dot_two, + c.AST_NODE_STRUCT_INIT_DOT_TWO_COMMA => .struct_init_dot_two_comma, + c.AST_NODE_STRUCT_INIT_DOT => .struct_init_dot, + c.AST_NODE_STRUCT_INIT_DOT_COMMA => .struct_init_dot_comma, + c.AST_NODE_STRUCT_INIT => .struct_init, + c.AST_NODE_STRUCT_INIT_COMMA => .struct_init_comma, + c.AST_NODE_CALL_ONE => .call_one, + c.AST_NODE_CALL_ONE_COMMA => .call_one_comma, + c.AST_NODE_CALL => .call, + c.AST_NODE_CALL_COMMA => .call_comma, + c.AST_NODE_SWITCH => .@"switch", + c.AST_NODE_SWITCH_COMMA => .switch_comma, + c.AST_NODE_SWITCH_CASE_ONE => .switch_case_one, + c.AST_NODE_SWITCH_CASE_INLINE_ONE => .switch_case_inline_one, + c.AST_NODE_SWITCH_CASE => .switch_case, + c.AST_NODE_SWITCH_CASE_INLINE => .switch_case_inline, + c.AST_NODE_SWITCH_RANGE => .switch_range, + c.AST_NODE_WHILE_SIMPLE => .while_simple, + c.AST_NODE_WHILE_CONT => .while_cont, + c.AST_NODE_WHILE => .@"while", + c.AST_NODE_FOR_SIMPLE => .for_simple, + c.AST_NODE_FOR => .@"for", + c.AST_NODE_FOR_RANGE => .for_range, + c.AST_NODE_IF_SIMPLE => .if_simple, + c.AST_NODE_IF => .@"if", + c.AST_NODE_SUSPEND => .@"suspend", + c.AST_NODE_RESUME => .@"resume", + c.AST_NODE_CONTINUE => .@"continue", + c.AST_NODE_BREAK => .@"break", + c.AST_NODE_RETURN => .@"return", + c.AST_NODE_FN_PROTO_SIMPLE => .fn_proto_simple, + c.AST_NODE_FN_PROTO_MULTI => .fn_proto_multi, + c.AST_NODE_FN_PROTO_ONE => .fn_proto_one, + c.AST_NODE_FN_PROTO => .fn_proto, + c.AST_NODE_FN_DECL => .fn_decl, + c.AST_NODE_ANYFRAME_TYPE => .anyframe_type, + c.AST_NODE_ANYFRAME_LITERAL => .anyframe_literal, + c.AST_NODE_CHAR_LITERAL => .char_literal, + c.AST_NODE_NUMBER_LITERAL => .number_literal, + c.AST_NODE_UNREACHABLE_LITERAL => .unreachable_literal, + c.AST_NODE_IDENTIFIER => .identifier, + c.AST_NODE_ENUM_LITERAL => .enum_literal, + c.AST_NODE_STRING_LITERAL => .string_literal, + c.AST_NODE_MULTILINE_STRING_LITERAL => .multiline_string_literal, + c.AST_NODE_GROUPED_EXPRESSION => .grouped_expression, + c.AST_NODE_BUILTIN_CALL_TWO => .builtin_call_two, + c.AST_NODE_BUILTIN_CALL_TWO_COMMA => .builtin_call_two_comma, + c.AST_NODE_BUILTIN_CALL => .builtin_call, + c.AST_NODE_BUILTIN_CALL_COMMA => .builtin_call_comma, + c.AST_NODE_ERROR_SET_DECL => .error_set_decl, + c.AST_NODE_CONTAINER_DECL => .container_decl, + c.AST_NODE_CONTAINER_DECL_TRAILING => .container_decl_trailing, + c.AST_NODE_CONTAINER_DECL_TWO => .container_decl_two, + c.AST_NODE_CONTAINER_DECL_TWO_TRAILING => .container_decl_two_trailing, + c.AST_NODE_CONTAINER_DECL_ARG => .container_decl_arg, + c.AST_NODE_CONTAINER_DECL_ARG_TRAILING => .container_decl_arg_trailing, + c.AST_NODE_TAGGED_UNION => .tagged_union, + c.AST_NODE_TAGGED_UNION_TRAILING => .tagged_union_trailing, + c.AST_NODE_TAGGED_UNION_TWO => .tagged_union_two, + c.AST_NODE_TAGGED_UNION_TWO_TRAILING => .tagged_union_two_trailing, + c.AST_NODE_TAGGED_UNION_ENUM_TAG => .tagged_union_enum_tag, + c.AST_NODE_TAGGED_UNION_ENUM_TAG_TRAILING => .tagged_union_enum_tag_trailing, + c.AST_NODE_CONTAINER_FIELD_INIT => .container_field_init, + c.AST_NODE_CONTAINER_FIELD_ALIGN => .container_field_align, + c.AST_NODE_CONTAINER_FIELD => .container_field, + c.AST_NODE_COMPTIME => .@"comptime", + c.AST_NODE_NOSUSPEND => .@"nosuspend", + c.AST_NODE_BLOCK_TWO => .block_two, + c.AST_NODE_BLOCK_TWO_SEMICOLON => .block_two_semicolon, + c.AST_NODE_BLOCK => .block, + c.AST_NODE_BLOCK_SEMICOLON => .block_semicolon, + c.AST_NODE_ASM_SIMPLE => .asm_simple, + c.AST_NODE_ASM_LEGACY => .asm_legacy, + c.AST_NODE_ASM => .@"asm", + c.AST_NODE_ASM_OUTPUT => .asm_output, + c.AST_NODE_ASM_INPUT => .asm_input, + c.AST_NODE_ERROR_VALUE => .error_value, + c.AST_NODE_ERROR_UNION => .error_union, + else => @as(Ast.Node.Tag, @enumFromInt(std.math.maxInt(@typeInfo(Ast.Node.Tag).@"enum".tag_type))), + }; +} + +fn toIndex(v: u32) Ast.Node.Index { + return @enumFromInt(v); +} + +fn toOptIndex(v: u32) Ast.Node.OptionalIndex { + return if (v == 0) .none else @enumFromInt(v); +} + +fn toExtraIndex(v: u32) Ast.ExtraIndex { + return @enumFromInt(v); +} + +fn toOptTokenIndex(v: u32) Ast.OptionalTokenIndex { + return @enumFromInt(v); +} + +fn zigData(tag: Ast.Node.Tag, lhs: u32, rhs: u32) Ast.Node.Data { + return switch (tag) { + // data unused + .identifier, + .string_literal, + .char_literal, + .number_literal, + .unreachable_literal, + .anyframe_literal, + .enum_literal, + .error_value, + => .{ .opt_node_and_opt_node = .{ toOptIndex(lhs), toOptIndex(rhs) } }, + + // .node (single node index) + .@"defer", + .@"comptime", + .@"nosuspend", + .@"suspend", + .@"resume", + .bool_not, + .negation, + .bit_not, + .negation_wrap, + .address_of, + .@"try", + .deref, + .optional_type, + => .{ .node = toIndex(lhs) }, + + // .opt_node (single optional node) + .@"return", + => .{ .opt_node = toOptIndex(lhs) }, + + // .node_and_node + .fn_decl, + .container_field_align, + .error_union, + .@"catch", + .equal_equal, + .bang_equal, + .less_than, + .greater_than, + .less_or_equal, + .greater_or_equal, + .assign_mul, + .assign_div, + .assign_mod, + .assign_add, + .assign_sub, + .assign_shl, + .assign_shl_sat, + .assign_shr, + .assign_bit_and, + .assign_bit_xor, + .assign_bit_or, + .assign_mul_wrap, + .assign_add_wrap, + .assign_sub_wrap, + .assign_mul_sat, + .assign_add_sat, + .assign_sub_sat, + .assign, + .merge_error_sets, + .mul, + .div, + .mod, + .array_mult, + .mul_wrap, + .mul_sat, + .add, + .sub, + .array_cat, + .add_wrap, + .sub_wrap, + .add_sat, + .sub_sat, + .shl, + .shl_sat, + .shr, + .bit_and, + .bit_xor, + .bit_or, + .@"orelse", + .bool_and, + .bool_or, + .array_type, + .array_access, + .switch_range, + => .{ .node_and_node = .{ toIndex(lhs), toIndex(rhs) } }, + + // .opt_node_and_opt_node + .fn_proto_simple, + .simple_var_decl, + .block_two, + .block_two_semicolon, + .builtin_call_two, + .builtin_call_two_comma, + .container_decl_two, + .container_decl_two_trailing, + .tagged_union_two, + .tagged_union_two_trailing, + .struct_init_dot_two, + .struct_init_dot_two_comma, + .array_init_dot_two, + .array_init_dot_two_comma, + => .{ .opt_node_and_opt_node = .{ toOptIndex(lhs), toOptIndex(rhs) } }, + + // .node_and_opt_node + .call_one, + .call_one_comma, + .struct_init_one, + .struct_init_one_comma, + .container_field_init, + .aligned_var_decl, + => .{ .node_and_opt_node = .{ toIndex(lhs), toOptIndex(rhs) } }, + + // .node_and_node (array_init_one uses node_and_node, not + // node_and_opt_node) + .array_init_one, + .array_init_one_comma, + => .{ .node_and_node = .{ toIndex(lhs), toIndex(rhs) } }, + + // .opt_node_and_node + .ptr_type_aligned, + .ptr_type_sentinel, + .switch_case_one, + .switch_case_inline_one, + => .{ .opt_node_and_node = .{ toOptIndex(lhs), toIndex(rhs) } }, + + // .node_and_extra + .call, + .call_comma, + .container_field, + .array_type_sentinel, + .slice, + .slice_sentinel, + .array_init, + .array_init_comma, + .struct_init, + .struct_init_comma, + .@"switch", + .switch_comma, + .container_decl_arg, + .container_decl_arg_trailing, + .tagged_union_enum_tag, + .tagged_union_enum_tag_trailing, + .@"asm", + => .{ .node_and_extra = .{ toIndex(lhs), toExtraIndex(rhs) } }, + + // .extra_and_node + .assign_destructure, + .switch_case, + .switch_case_inline, + .ptr_type, + .ptr_type_bit_range, + => .{ .extra_and_node = .{ toExtraIndex(lhs), toIndex(rhs) } }, + + // .extra_and_opt_node + .global_var_decl, + .local_var_decl, + .fn_proto_multi, + .fn_proto_one, + .fn_proto, + => .{ .extra_and_opt_node = .{ toExtraIndex(lhs), toOptIndex(rhs) } }, + + // .extra_range (SubRange) + .root, + .block, + .block_semicolon, + .builtin_call, + .builtin_call_comma, + .container_decl, + .container_decl_trailing, + .tagged_union, + .tagged_union_trailing, + .array_init_dot, + .array_init_dot_comma, + .struct_init_dot, + .struct_init_dot_comma, + => .{ .extra_range = .{ .start = toExtraIndex(lhs), .end = toExtraIndex(rhs) } }, + + // .node_and_token + .grouped_expression, + .asm_input, + .asm_simple, + .field_access, + .unwrap_optional, + => .{ .node_and_token = .{ toIndex(lhs), rhs } }, + + // .opt_node_and_token + .asm_output, + => .{ .opt_node_and_token = .{ toOptIndex(lhs), rhs } }, + + // .opt_token_and_node + .test_decl, + .@"errdefer", + => .{ .opt_token_and_node = .{ toOptTokenIndex(lhs), toIndex(rhs) } }, + + // .opt_token_and_opt_node + .@"break", + .@"continue", + => .{ .opt_token_and_opt_node = .{ toOptTokenIndex(lhs), toOptIndex(rhs) } }, + + // .token_and_token + .error_set_decl, + .multiline_string_literal, + => .{ .token_and_token = .{ lhs, rhs } }, + + // .token_and_node + .anyframe_type, + => .{ .token_and_node = .{ lhs, toIndex(rhs) } }, + + // .node_and_node for slice_open (lhs[rhs..]) + .slice_open, + => .{ .node_and_node = .{ toIndex(lhs), toIndex(rhs) } }, + + .while_simple, + .for_simple, + .if_simple, + => .{ .node_and_node = .{ toIndex(lhs), toIndex(rhs) } }, + + .while_cont, + .@"while", + .@"if", + => .{ .node_and_extra = .{ toIndex(lhs), toExtraIndex(rhs) } }, + + .for_range, + => .{ .node_and_opt_node = .{ toIndex(lhs), toOptIndex(rhs) } }, + + .@"for", + => .{ .@"for" = .{ toExtraIndex(lhs), @bitCast(rhs) } }, + + .asm_legacy, + => .{ .node_and_extra = .{ toIndex(lhs), toExtraIndex(rhs) } }, + }; +} + +// zigAst converts a c.Ast to std.Zig.Ast. The resulting Ast should be freed with deinit(). +pub fn zigAst(gpa: Allocator, c_ast: c.Ast) !Ast { + var tokens = Ast.TokenList{}; + try tokens.resize(gpa, c_ast.tokens.len); + errdefer tokens.deinit(gpa); + + for (0..c_ast.tokens.len) |i| + tokens.set(i, .{ + .tag = zigToken(c_ast.tokens.tags[i]), + .start = c_ast.tokens.starts[i], + }); + + var nodes = Ast.NodeList{}; + try nodes.resize(gpa, c_ast.nodes.len); + errdefer nodes.deinit(gpa); + + for (0..c_ast.nodes.len) |i| { + const tag = zigNode(c_ast.nodes.tags[i]); + nodes.set(i, .{ + .tag = tag, + .main_token = c_ast.nodes.main_tokens[i], + .data = zigData(tag, c_ast.nodes.datas[i].lhs, c_ast.nodes.datas[i].rhs), + }); + } + + const extra_data = try gpa.alloc(u32, c_ast.extra_data.len); + errdefer gpa.free(extra_data); + @memcpy(extra_data, c_ast.extra_data.arr[0..c_ast.extra_data.len]); + + const errors = if (c_ast.has_error) blk: { + const errs = try gpa.alloc(Ast.Error, 1); + errs[0] = .{ .tag = .expected_token, .token = 0, .extra = .{ .none = {} } }; + break :blk errs; + } else try gpa.alloc(Ast.Error, 0); + errdefer gpa.free(errors); + + return Ast{ + .source = c_ast.source[0..c_ast.source_len :0], + .mode = .zig, + .tokens = tokens.slice(), + .nodes = nodes.slice(), + .extra_data = extra_data, + .errors = errors, + }; +} + +// Returns the number of meaningful u32 fields in Node.Data for a given tag. +// 0 = data is undefined/unused, 1 = only first u32 is meaningful, 2 = both meaningful. +fn dataFieldCount(tag: Ast.Node.Tag) u2 { + return switch (tag) { + // data unused (undefined in Zig parser) + .identifier, + .string_literal, + .char_literal, + .number_literal, + .unreachable_literal, + .anyframe_literal, + .enum_literal, + .error_value, + => 0, + + // .node or .opt_node — only first u32 + .@"defer", + .@"comptime", + .@"nosuspend", + .@"suspend", + .@"resume", + .bool_not, + .negation, + .bit_not, + .negation_wrap, + .address_of, + .@"try", + .deref, + .optional_type, + .@"return", + => 1, + + // everything else — both u32 fields + else => 2, + }; +} + +pub fn expectAstConsistent(c_tree: Ast, zig_tree: Ast, source: [:0]const u8) !void { + _ = source; + const print = std.debug.print; + + if (c_tree.tokens.len != zig_tree.tokens.len) { + print("token count mismatch: c={d} zig={d}\n", .{ c_tree.tokens.len, zig_tree.tokens.len }); + return error.TestExpectedEqual; + } + for (0..c_tree.tokens.len) |i| { + if (c_tree.tokens.items(.start)[i] != zig_tree.tokens.items(.start)[i]) { + print("token[{d}] start mismatch: c={d} zig={d}\n", .{ i, c_tree.tokens.items(.start)[i], zig_tree.tokens.items(.start)[i] }); + return error.TestExpectedEqual; + } + if (c_tree.tokens.items(.tag)[i] != zig_tree.tokens.items(.tag)[i]) { + print("token[{d}] tag mismatch: c={s} zig={s}\n", .{ i, @tagName(c_tree.tokens.items(.tag)[i]), @tagName(zig_tree.tokens.items(.tag)[i]) }); + return error.TestExpectedEqual; + } + } + + if (c_tree.nodes.len != zig_tree.nodes.len) { + print("node count mismatch: c={d} zig={d}\n", .{ c_tree.nodes.len, zig_tree.nodes.len }); + return error.TestExpectedEqual; + } + for (0..c_tree.nodes.len) |i| { + const c_tag = c_tree.nodes.items(.tag)[i]; + const z_tag = zig_tree.nodes.items(.tag)[i]; + if (c_tag != z_tag) { + print("node[{d}] tag mismatch: c={s} zig={s}\n", .{ i, @tagName(c_tag), @tagName(z_tag) }); + return error.TestExpectedEqual; + } + if (c_tree.nodes.items(.main_token)[i] != zig_tree.nodes.items(.main_token)[i]) { + print("node[{d}] main_token mismatch: c={d} zig={d}\n", .{ i, c_tree.nodes.items(.main_token)[i], zig_tree.nodes.items(.main_token)[i] }); + return error.TestExpectedEqual; + } + const field_count = dataFieldCount(c_tag); + if (field_count >= 1) { + const c_data: *const [2]u32 = @ptrCast(&c_tree.nodes.items(.data)[i]); + const z_data: *const [2]u32 = @ptrCast(&zig_tree.nodes.items(.data)[i]); + if (c_data[0] != z_data[0]) { + print("node[{d}] data[0] mismatch: c={d} zig={d}\n", .{ i, c_data[0], z_data[0] }); + return error.TestExpectedEqual; + } + if (field_count >= 2 and c_data[1] != z_data[1]) { + print("node[{d}] data[1] mismatch: c={d} zig={d}\n", .{ i, c_data[1], z_data[1] }); + return error.TestExpectedEqual; + } + } + } + + if (c_tree.extra_data.len != zig_tree.extra_data.len) { + print("extra_data length mismatch: c={d} zig={d}\n", .{ c_tree.extra_data.len, zig_tree.extra_data.len }); + return error.TestExpectedEqual; + } + for (0..c_tree.extra_data.len) |i| { + if (c_tree.extra_data[i] != zig_tree.extra_data[i]) { + print("extra_data[{d}] mismatch: c={d} zig={d}\n", .{ i, c_tree.extra_data[i], zig_tree.extra_data[i] }); + return error.TestExpectedEqual; + } + } +}