master
   1const std = @import("std");
   2const Allocator = std.mem.Allocator;
   3const Token = @import("lex.zig").Token;
   4const SupportedCodePage = @import("code_pages.zig").SupportedCodePage;
   5
   6pub const Tree = struct {
   7    node: *Node,
   8    input_code_pages: CodePageLookup,
   9    output_code_pages: CodePageLookup,
  10
  11    /// not owned by the tree
  12    source: []const u8,
  13
  14    arena: std.heap.ArenaAllocator.State,
  15    allocator: Allocator,
  16
  17    pub fn deinit(self: *Tree) void {
  18        self.arena.promote(self.allocator).deinit();
  19    }
  20
  21    pub fn root(self: *Tree) *Node.Root {
  22        return @alignCast(@fieldParentPtr("base", self.node));
  23    }
  24
  25    pub fn dump(self: *Tree, writer: *std.Io.Writer) !void {
  26        try self.node.dump(self, writer, 0);
  27    }
  28};
  29
  30pub const CodePageLookup = struct {
  31    lookup: std.ArrayList(SupportedCodePage) = .empty,
  32    allocator: Allocator,
  33    default_code_page: SupportedCodePage,
  34
  35    pub fn init(allocator: Allocator, default_code_page: SupportedCodePage) CodePageLookup {
  36        return .{
  37            .allocator = allocator,
  38            .default_code_page = default_code_page,
  39        };
  40    }
  41
  42    pub fn deinit(self: *CodePageLookup) void {
  43        self.lookup.deinit(self.allocator);
  44    }
  45
  46    /// line_num is 1-indexed
  47    pub fn setForLineNum(self: *CodePageLookup, line_num: usize, code_page: SupportedCodePage) !void {
  48        const index = line_num - 1;
  49        if (index >= self.lookup.items.len) {
  50            const new_size = line_num;
  51            const missing_lines_start_index = self.lookup.items.len;
  52            try self.lookup.resize(self.allocator, new_size);
  53
  54            // If there are any gaps created, we need to fill them in with the value of the
  55            // last line before the gap. This can happen for e.g. string literals that
  56            // span multiple lines, or if the start of a file has multiple empty lines.
  57            const fill_value = if (missing_lines_start_index > 0)
  58                self.lookup.items[missing_lines_start_index - 1]
  59            else
  60                self.default_code_page;
  61            var i: usize = missing_lines_start_index;
  62            while (i < new_size - 1) : (i += 1) {
  63                self.lookup.items[i] = fill_value;
  64            }
  65        }
  66        self.lookup.items[index] = code_page;
  67    }
  68
  69    pub fn setForToken(self: *CodePageLookup, token: Token, code_page: SupportedCodePage) !void {
  70        return self.setForLineNum(token.line_number, code_page);
  71    }
  72
  73    /// line_num is 1-indexed
  74    pub fn getForLineNum(self: CodePageLookup, line_num: usize) SupportedCodePage {
  75        return self.lookup.items[line_num - 1];
  76    }
  77
  78    pub fn getForToken(self: CodePageLookup, token: Token) SupportedCodePage {
  79        return self.getForLineNum(token.line_number);
  80    }
  81};
  82
  83test "CodePageLookup" {
  84    var lookup = CodePageLookup.init(std.testing.allocator, .windows1252);
  85    defer lookup.deinit();
  86
  87    try lookup.setForLineNum(5, .utf8);
  88    try std.testing.expectEqual(SupportedCodePage.windows1252, lookup.getForLineNum(1));
  89    try std.testing.expectEqual(SupportedCodePage.windows1252, lookup.getForLineNum(2));
  90    try std.testing.expectEqual(SupportedCodePage.windows1252, lookup.getForLineNum(3));
  91    try std.testing.expectEqual(SupportedCodePage.windows1252, lookup.getForLineNum(4));
  92    try std.testing.expectEqual(SupportedCodePage.utf8, lookup.getForLineNum(5));
  93    try std.testing.expectEqual(@as(usize, 5), lookup.lookup.items.len);
  94
  95    try lookup.setForLineNum(7, .windows1252);
  96    try std.testing.expectEqual(SupportedCodePage.windows1252, lookup.getForLineNum(1));
  97    try std.testing.expectEqual(SupportedCodePage.windows1252, lookup.getForLineNum(2));
  98    try std.testing.expectEqual(SupportedCodePage.windows1252, lookup.getForLineNum(3));
  99    try std.testing.expectEqual(SupportedCodePage.windows1252, lookup.getForLineNum(4));
 100    try std.testing.expectEqual(SupportedCodePage.utf8, lookup.getForLineNum(5));
 101    try std.testing.expectEqual(SupportedCodePage.utf8, lookup.getForLineNum(6));
 102    try std.testing.expectEqual(SupportedCodePage.windows1252, lookup.getForLineNum(7));
 103    try std.testing.expectEqual(@as(usize, 7), lookup.lookup.items.len);
 104}
 105
 106pub const Node = struct {
 107    id: Id,
 108
 109    pub const Id = enum {
 110        root,
 111        resource_external,
 112        resource_raw_data,
 113        literal,
 114        binary_expression,
 115        grouped_expression,
 116        not_expression,
 117        accelerators,
 118        accelerator,
 119        dialog,
 120        control_statement,
 121        toolbar,
 122        menu,
 123        menu_item,
 124        menu_item_separator,
 125        menu_item_ex,
 126        popup,
 127        popup_ex,
 128        version_info,
 129        version_statement,
 130        block,
 131        block_value,
 132        block_value_value,
 133        string_table,
 134        string_table_string,
 135        language_statement,
 136        font_statement,
 137        simple_statement,
 138        invalid,
 139
 140        pub fn Type(comptime id: Id) type {
 141            return switch (id) {
 142                .root => Root,
 143                .resource_external => ResourceExternal,
 144                .resource_raw_data => ResourceRawData,
 145                .literal => Literal,
 146                .binary_expression => BinaryExpression,
 147                .grouped_expression => GroupedExpression,
 148                .not_expression => NotExpression,
 149                .accelerators => Accelerators,
 150                .accelerator => Accelerator,
 151                .dialog => Dialog,
 152                .control_statement => ControlStatement,
 153                .toolbar => Toolbar,
 154                .menu => Menu,
 155                .menu_item => MenuItem,
 156                .menu_item_separator => MenuItemSeparator,
 157                .menu_item_ex => MenuItemEx,
 158                .popup => Popup,
 159                .popup_ex => PopupEx,
 160                .version_info => VersionInfo,
 161                .version_statement => VersionStatement,
 162                .block => Block,
 163                .block_value => BlockValue,
 164                .block_value_value => BlockValueValue,
 165                .string_table => StringTable,
 166                .string_table_string => StringTableString,
 167                .language_statement => LanguageStatement,
 168                .font_statement => FontStatement,
 169                .simple_statement => SimpleStatement,
 170                .invalid => Invalid,
 171            };
 172        }
 173    };
 174
 175    pub fn cast(base: *Node, comptime id: Id) ?*id.Type() {
 176        if (base.id == id) {
 177            return @alignCast(@fieldParentPtr("base", base));
 178        }
 179        return null;
 180    }
 181
 182    pub const Root = struct {
 183        base: Node = .{ .id = .root },
 184        body: []*Node,
 185    };
 186
 187    pub const ResourceExternal = struct {
 188        base: Node = .{ .id = .resource_external },
 189        id: Token,
 190        type: Token,
 191        common_resource_attributes: []Token,
 192        filename: *Node,
 193    };
 194
 195    pub const ResourceRawData = struct {
 196        base: Node = .{ .id = .resource_raw_data },
 197        id: Token,
 198        type: Token,
 199        common_resource_attributes: []Token,
 200        begin_token: Token,
 201        raw_data: []*Node,
 202        end_token: Token,
 203    };
 204
 205    pub const Literal = struct {
 206        base: Node = .{ .id = .literal },
 207        token: Token,
 208    };
 209
 210    pub const BinaryExpression = struct {
 211        base: Node = .{ .id = .binary_expression },
 212        operator: Token,
 213        left: *Node,
 214        right: *Node,
 215    };
 216
 217    pub const GroupedExpression = struct {
 218        base: Node = .{ .id = .grouped_expression },
 219        open_token: Token,
 220        expression: *Node,
 221        close_token: Token,
 222    };
 223
 224    pub const NotExpression = struct {
 225        base: Node = .{ .id = .not_expression },
 226        not_token: Token,
 227        number_token: Token,
 228    };
 229
 230    pub const Accelerators = struct {
 231        base: Node = .{ .id = .accelerators },
 232        id: Token,
 233        type: Token,
 234        common_resource_attributes: []Token,
 235        optional_statements: []*Node,
 236        begin_token: Token,
 237        accelerators: []*Node,
 238        end_token: Token,
 239    };
 240
 241    pub const Accelerator = struct {
 242        base: Node = .{ .id = .accelerator },
 243        event: *Node,
 244        idvalue: *Node,
 245        type_and_options: []Token,
 246    };
 247
 248    pub const Dialog = struct {
 249        base: Node = .{ .id = .dialog },
 250        id: Token,
 251        type: Token,
 252        common_resource_attributes: []Token,
 253        x: *Node,
 254        y: *Node,
 255        width: *Node,
 256        height: *Node,
 257        help_id: ?*Node,
 258        optional_statements: []*Node,
 259        begin_token: Token,
 260        controls: []*Node,
 261        end_token: Token,
 262    };
 263
 264    pub const ControlStatement = struct {
 265        base: Node = .{ .id = .control_statement },
 266        type: Token,
 267        text: ?Token,
 268        /// Only relevant for the user-defined CONTROL control
 269        class: ?*Node,
 270        id: *Node,
 271        x: *Node,
 272        y: *Node,
 273        width: *Node,
 274        height: *Node,
 275        style: ?*Node,
 276        exstyle: ?*Node,
 277        help_id: ?*Node,
 278        extra_data_begin: ?Token,
 279        extra_data: []*Node,
 280        extra_data_end: ?Token,
 281
 282        /// Returns true if this node describes a user-defined CONTROL control
 283        /// https://learn.microsoft.com/en-us/windows/win32/menurc/control-control
 284        pub fn isUserDefined(self: *const ControlStatement) bool {
 285            return self.class != null;
 286        }
 287    };
 288
 289    pub const Toolbar = struct {
 290        base: Node = .{ .id = .toolbar },
 291        id: Token,
 292        type: Token,
 293        common_resource_attributes: []Token,
 294        button_width: *Node,
 295        button_height: *Node,
 296        begin_token: Token,
 297        /// Will contain Literal and SimpleStatement nodes
 298        buttons: []*Node,
 299        end_token: Token,
 300    };
 301
 302    pub const Menu = struct {
 303        base: Node = .{ .id = .menu },
 304        id: Token,
 305        type: Token,
 306        common_resource_attributes: []Token,
 307        optional_statements: []*Node,
 308        /// `help_id` will never be non-null if `type` is MENU
 309        help_id: ?*Node,
 310        begin_token: Token,
 311        items: []*Node,
 312        end_token: Token,
 313    };
 314
 315    pub const MenuItem = struct {
 316        base: Node = .{ .id = .menu_item },
 317        menuitem: Token,
 318        text: Token,
 319        result: *Node,
 320        option_list: []Token,
 321    };
 322
 323    pub const MenuItemSeparator = struct {
 324        base: Node = .{ .id = .menu_item_separator },
 325        menuitem: Token,
 326        separator: Token,
 327    };
 328
 329    pub const MenuItemEx = struct {
 330        base: Node = .{ .id = .menu_item_ex },
 331        menuitem: Token,
 332        text: Token,
 333        id: ?*Node,
 334        type: ?*Node,
 335        state: ?*Node,
 336    };
 337
 338    pub const Popup = struct {
 339        base: Node = .{ .id = .popup },
 340        popup: Token,
 341        text: Token,
 342        option_list: []Token,
 343        begin_token: Token,
 344        items: []*Node,
 345        end_token: Token,
 346    };
 347
 348    pub const PopupEx = struct {
 349        base: Node = .{ .id = .popup_ex },
 350        popup: Token,
 351        text: Token,
 352        id: ?*Node,
 353        type: ?*Node,
 354        state: ?*Node,
 355        help_id: ?*Node,
 356        begin_token: Token,
 357        items: []*Node,
 358        end_token: Token,
 359    };
 360
 361    pub const VersionInfo = struct {
 362        base: Node = .{ .id = .version_info },
 363        id: Token,
 364        versioninfo: Token,
 365        common_resource_attributes: []Token,
 366        /// Will contain VersionStatement and/or SimpleStatement nodes
 367        fixed_info: []*Node,
 368        begin_token: Token,
 369        block_statements: []*Node,
 370        end_token: Token,
 371    };
 372
 373    /// Used for FILEVERSION and PRODUCTVERSION statements
 374    pub const VersionStatement = struct {
 375        base: Node = .{ .id = .version_statement },
 376        type: Token,
 377        /// Between 1-4 parts
 378        parts: []*Node,
 379    };
 380
 381    pub const Block = struct {
 382        base: Node = .{ .id = .block },
 383        /// The BLOCK token itself
 384        identifier: Token,
 385        key: Token,
 386        /// This is undocumented but BLOCK statements support values after
 387        /// the key just like VALUE statements.
 388        values: []*Node,
 389        begin_token: Token,
 390        children: []*Node,
 391        end_token: Token,
 392    };
 393
 394    pub const BlockValue = struct {
 395        base: Node = .{ .id = .block_value },
 396        /// The VALUE token itself
 397        identifier: Token,
 398        key: Token,
 399        /// These will be BlockValueValue nodes
 400        values: []*Node,
 401    };
 402
 403    pub const BlockValueValue = struct {
 404        base: Node = .{ .id = .block_value_value },
 405        expression: *Node,
 406        /// Whether or not the value has a trailing comma is relevant
 407        trailing_comma: bool,
 408    };
 409
 410    pub const StringTable = struct {
 411        base: Node = .{ .id = .string_table },
 412        type: Token,
 413        common_resource_attributes: []Token,
 414        optional_statements: []*Node,
 415        begin_token: Token,
 416        strings: []*Node,
 417        end_token: Token,
 418    };
 419
 420    pub const StringTableString = struct {
 421        base: Node = .{ .id = .string_table_string },
 422        id: *Node,
 423        maybe_comma: ?Token,
 424        string: Token,
 425    };
 426
 427    pub const LanguageStatement = struct {
 428        base: Node = .{ .id = .language_statement },
 429        /// The LANGUAGE token itself
 430        language_token: Token,
 431        primary_language_id: *Node,
 432        sublanguage_id: *Node,
 433    };
 434
 435    pub const FontStatement = struct {
 436        base: Node = .{ .id = .font_statement },
 437        /// The FONT token itself
 438        identifier: Token,
 439        point_size: *Node,
 440        typeface: Token,
 441        weight: ?*Node,
 442        italic: ?*Node,
 443        char_set: ?*Node,
 444    };
 445
 446    /// A statement with one value associated with it.
 447    /// Used for CAPTION, CHARACTERISTICS, CLASS, EXSTYLE, MENU, STYLE, VERSION,
 448    /// as well as VERSIONINFO-specific statements FILEFLAGSMASK, FILEFLAGS, FILEOS,
 449    /// FILETYPE, FILESUBTYPE
 450    pub const SimpleStatement = struct {
 451        base: Node = .{ .id = .simple_statement },
 452        identifier: Token,
 453        value: *Node,
 454    };
 455
 456    pub const Invalid = struct {
 457        base: Node = .{ .id = .invalid },
 458        context: []Token,
 459    };
 460
 461    pub fn isNumberExpression(node: *const Node) bool {
 462        switch (node.id) {
 463            .literal => {
 464                const literal: *const Node.Literal = @alignCast(@fieldParentPtr("base", node));
 465                return switch (literal.token.id) {
 466                    .number => true,
 467                    else => false,
 468                };
 469            },
 470            .binary_expression, .grouped_expression, .not_expression => return true,
 471            else => return false,
 472        }
 473    }
 474
 475    pub fn isStringLiteral(node: *const Node) bool {
 476        switch (node.id) {
 477            .literal => {
 478                const literal: *const Node.Literal = @alignCast(@fieldParentPtr("base", node));
 479                return switch (literal.token.id) {
 480                    .quoted_ascii_string, .quoted_wide_string => true,
 481                    else => false,
 482                };
 483            },
 484            else => return false,
 485        }
 486    }
 487
 488    pub fn getFirstToken(node: *const Node) Token {
 489        switch (node.id) {
 490            .root => unreachable,
 491            .resource_external => {
 492                const casted: *const Node.ResourceExternal = @alignCast(@fieldParentPtr("base", node));
 493                return casted.id;
 494            },
 495            .resource_raw_data => {
 496                const casted: *const Node.ResourceRawData = @alignCast(@fieldParentPtr("base", node));
 497                return casted.id;
 498            },
 499            .literal => {
 500                const casted: *const Node.Literal = @alignCast(@fieldParentPtr("base", node));
 501                return casted.token;
 502            },
 503            .binary_expression => {
 504                const casted: *const Node.BinaryExpression = @alignCast(@fieldParentPtr("base", node));
 505                return casted.left.getFirstToken();
 506            },
 507            .grouped_expression => {
 508                const casted: *const Node.GroupedExpression = @alignCast(@fieldParentPtr("base", node));
 509                return casted.open_token;
 510            },
 511            .not_expression => {
 512                const casted: *const Node.NotExpression = @alignCast(@fieldParentPtr("base", node));
 513                return casted.not_token;
 514            },
 515            .accelerators => {
 516                const casted: *const Node.Accelerators = @alignCast(@fieldParentPtr("base", node));
 517                return casted.id;
 518            },
 519            .accelerator => {
 520                const casted: *const Node.Accelerator = @alignCast(@fieldParentPtr("base", node));
 521                return casted.event.getFirstToken();
 522            },
 523            .dialog => {
 524                const casted: *const Node.Dialog = @alignCast(@fieldParentPtr("base", node));
 525                return casted.id;
 526            },
 527            .control_statement => {
 528                const casted: *const Node.ControlStatement = @alignCast(@fieldParentPtr("base", node));
 529                return casted.type;
 530            },
 531            .toolbar => {
 532                const casted: *const Node.Toolbar = @alignCast(@fieldParentPtr("base", node));
 533                return casted.id;
 534            },
 535            .menu => {
 536                const casted: *const Node.Menu = @alignCast(@fieldParentPtr("base", node));
 537                return casted.id;
 538            },
 539            inline .menu_item, .menu_item_separator, .menu_item_ex => |menu_item_type| {
 540                const casted: *const menu_item_type.Type() = @alignCast(@fieldParentPtr("base", node));
 541                return casted.menuitem;
 542            },
 543            inline .popup, .popup_ex => |popup_type| {
 544                const casted: *const popup_type.Type() = @alignCast(@fieldParentPtr("base", node));
 545                return casted.popup;
 546            },
 547            .version_info => {
 548                const casted: *const Node.VersionInfo = @alignCast(@fieldParentPtr("base", node));
 549                return casted.id;
 550            },
 551            .version_statement => {
 552                const casted: *const Node.VersionStatement = @alignCast(@fieldParentPtr("base", node));
 553                return casted.type;
 554            },
 555            .block => {
 556                const casted: *const Node.Block = @alignCast(@fieldParentPtr("base", node));
 557                return casted.identifier;
 558            },
 559            .block_value => {
 560                const casted: *const Node.BlockValue = @alignCast(@fieldParentPtr("base", node));
 561                return casted.identifier;
 562            },
 563            .block_value_value => {
 564                const casted: *const Node.BlockValueValue = @alignCast(@fieldParentPtr("base", node));
 565                return casted.expression.getFirstToken();
 566            },
 567            .string_table => {
 568                const casted: *const Node.StringTable = @alignCast(@fieldParentPtr("base", node));
 569                return casted.type;
 570            },
 571            .string_table_string => {
 572                const casted: *const Node.StringTableString = @alignCast(@fieldParentPtr("base", node));
 573                return casted.id.getFirstToken();
 574            },
 575            .language_statement => {
 576                const casted: *const Node.LanguageStatement = @alignCast(@fieldParentPtr("base", node));
 577                return casted.language_token;
 578            },
 579            .font_statement => {
 580                const casted: *const Node.FontStatement = @alignCast(@fieldParentPtr("base", node));
 581                return casted.identifier;
 582            },
 583            .simple_statement => {
 584                const casted: *const Node.SimpleStatement = @alignCast(@fieldParentPtr("base", node));
 585                return casted.identifier;
 586            },
 587            .invalid => {
 588                const casted: *const Node.Invalid = @alignCast(@fieldParentPtr("base", node));
 589                return casted.context[0];
 590            },
 591        }
 592    }
 593
 594    pub fn getLastToken(node: *const Node) Token {
 595        switch (node.id) {
 596            .root => unreachable,
 597            .resource_external => {
 598                const casted: *const Node.ResourceExternal = @alignCast(@fieldParentPtr("base", node));
 599                return casted.filename.getLastToken();
 600            },
 601            .resource_raw_data => {
 602                const casted: *const Node.ResourceRawData = @alignCast(@fieldParentPtr("base", node));
 603                return casted.end_token;
 604            },
 605            .literal => {
 606                const casted: *const Node.Literal = @alignCast(@fieldParentPtr("base", node));
 607                return casted.token;
 608            },
 609            .binary_expression => {
 610                const casted: *const Node.BinaryExpression = @alignCast(@fieldParentPtr("base", node));
 611                return casted.right.getLastToken();
 612            },
 613            .grouped_expression => {
 614                const casted: *const Node.GroupedExpression = @alignCast(@fieldParentPtr("base", node));
 615                return casted.close_token;
 616            },
 617            .not_expression => {
 618                const casted: *const Node.NotExpression = @alignCast(@fieldParentPtr("base", node));
 619                return casted.number_token;
 620            },
 621            .accelerators => {
 622                const casted: *const Node.Accelerators = @alignCast(@fieldParentPtr("base", node));
 623                return casted.end_token;
 624            },
 625            .accelerator => {
 626                const casted: *const Node.Accelerator = @alignCast(@fieldParentPtr("base", node));
 627                if (casted.type_and_options.len > 0) return casted.type_and_options[casted.type_and_options.len - 1];
 628                return casted.idvalue.getLastToken();
 629            },
 630            .dialog => {
 631                const casted: *const Node.Dialog = @alignCast(@fieldParentPtr("base", node));
 632                return casted.end_token;
 633            },
 634            .control_statement => {
 635                const casted: *const Node.ControlStatement = @alignCast(@fieldParentPtr("base", node));
 636                if (casted.extra_data_end) |token| return token;
 637                if (casted.help_id) |help_id_node| return help_id_node.getLastToken();
 638                if (casted.exstyle) |exstyle_node| return exstyle_node.getLastToken();
 639                // For user-defined CONTROL controls, the style comes before 'x', but
 640                // otherwise it comes after 'height' so it could be the last token if
 641                // it's present.
 642                if (!casted.isUserDefined()) {
 643                    if (casted.style) |style_node| return style_node.getLastToken();
 644                }
 645                return casted.height.getLastToken();
 646            },
 647            .toolbar => {
 648                const casted: *const Node.Toolbar = @alignCast(@fieldParentPtr("base", node));
 649                return casted.end_token;
 650            },
 651            .menu => {
 652                const casted: *const Node.Menu = @alignCast(@fieldParentPtr("base", node));
 653                return casted.end_token;
 654            },
 655            .menu_item => {
 656                const casted: *const Node.MenuItem = @alignCast(@fieldParentPtr("base", node));
 657                if (casted.option_list.len > 0) return casted.option_list[casted.option_list.len - 1];
 658                return casted.result.getLastToken();
 659            },
 660            .menu_item_separator => {
 661                const casted: *const Node.MenuItemSeparator = @alignCast(@fieldParentPtr("base", node));
 662                return casted.separator;
 663            },
 664            .menu_item_ex => {
 665                const casted: *const Node.MenuItemEx = @alignCast(@fieldParentPtr("base", node));
 666                if (casted.state) |state_node| return state_node.getLastToken();
 667                if (casted.type) |type_node| return type_node.getLastToken();
 668                if (casted.id) |id_node| return id_node.getLastToken();
 669                return casted.text;
 670            },
 671            inline .popup, .popup_ex => |popup_type| {
 672                const casted: *const popup_type.Type() = @alignCast(@fieldParentPtr("base", node));
 673                return casted.end_token;
 674            },
 675            .version_info => {
 676                const casted: *const Node.VersionInfo = @alignCast(@fieldParentPtr("base", node));
 677                return casted.end_token;
 678            },
 679            .version_statement => {
 680                const casted: *const Node.VersionStatement = @alignCast(@fieldParentPtr("base", node));
 681                return casted.parts[casted.parts.len - 1].getLastToken();
 682            },
 683            .block => {
 684                const casted: *const Node.Block = @alignCast(@fieldParentPtr("base", node));
 685                return casted.end_token;
 686            },
 687            .block_value => {
 688                const casted: *const Node.BlockValue = @alignCast(@fieldParentPtr("base", node));
 689                if (casted.values.len > 0) return casted.values[casted.values.len - 1].getLastToken();
 690                return casted.key;
 691            },
 692            .block_value_value => {
 693                const casted: *const Node.BlockValueValue = @alignCast(@fieldParentPtr("base", node));
 694                return casted.expression.getLastToken();
 695            },
 696            .string_table => {
 697                const casted: *const Node.StringTable = @alignCast(@fieldParentPtr("base", node));
 698                return casted.end_token;
 699            },
 700            .string_table_string => {
 701                const casted: *const Node.StringTableString = @alignCast(@fieldParentPtr("base", node));
 702                return casted.string;
 703            },
 704            .language_statement => {
 705                const casted: *const Node.LanguageStatement = @alignCast(@fieldParentPtr("base", node));
 706                return casted.sublanguage_id.getLastToken();
 707            },
 708            .font_statement => {
 709                const casted: *const Node.FontStatement = @alignCast(@fieldParentPtr("base", node));
 710                if (casted.char_set) |char_set_node| return char_set_node.getLastToken();
 711                if (casted.italic) |italic_node| return italic_node.getLastToken();
 712                if (casted.weight) |weight_node| return weight_node.getLastToken();
 713                return casted.typeface;
 714            },
 715            .simple_statement => {
 716                const casted: *const Node.SimpleStatement = @alignCast(@fieldParentPtr("base", node));
 717                return casted.value.getLastToken();
 718            },
 719            .invalid => {
 720                const casted: *const Node.Invalid = @alignCast(@fieldParentPtr("base", node));
 721                return casted.context[casted.context.len - 1];
 722            },
 723        }
 724    }
 725
 726    pub fn dump(
 727        node: *const Node,
 728        tree: *const Tree,
 729        writer: *std.Io.Writer,
 730        indent: usize,
 731    ) std.Io.Writer.Error!void {
 732        try writer.splatByteAll(' ', indent);
 733        try writer.writeAll(@tagName(node.id));
 734        switch (node.id) {
 735            .root => {
 736                try writer.writeAll("\n");
 737                const root: *const Node.Root = @alignCast(@fieldParentPtr("base", node));
 738                for (root.body) |body_node| {
 739                    try body_node.dump(tree, writer, indent + 1);
 740                }
 741            },
 742            .resource_external => {
 743                const resource: *const Node.ResourceExternal = @alignCast(@fieldParentPtr("base", node));
 744                try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ resource.id.slice(tree.source), resource.type.slice(tree.source), resource.common_resource_attributes.len });
 745                try resource.filename.dump(tree, writer, indent + 1);
 746            },
 747            .resource_raw_data => {
 748                const resource: *const Node.ResourceRawData = @alignCast(@fieldParentPtr("base", node));
 749                try writer.print(" {s} {s} [{d} common_resource_attributes] raw data: {}\n", .{ resource.id.slice(tree.source), resource.type.slice(tree.source), resource.common_resource_attributes.len, resource.raw_data.len });
 750                for (resource.raw_data) |data_expression| {
 751                    try data_expression.dump(tree, writer, indent + 1);
 752                }
 753            },
 754            .literal => {
 755                const literal: *const Node.Literal = @alignCast(@fieldParentPtr("base", node));
 756                try writer.writeAll(" ");
 757                try writer.writeAll(literal.token.slice(tree.source));
 758                try writer.writeAll("\n");
 759            },
 760            .binary_expression => {
 761                const binary: *const Node.BinaryExpression = @alignCast(@fieldParentPtr("base", node));
 762                try writer.writeAll(" ");
 763                try writer.writeAll(binary.operator.slice(tree.source));
 764                try writer.writeAll("\n");
 765                try binary.left.dump(tree, writer, indent + 1);
 766                try binary.right.dump(tree, writer, indent + 1);
 767            },
 768            .grouped_expression => {
 769                const grouped: *const Node.GroupedExpression = @alignCast(@fieldParentPtr("base", node));
 770                try writer.writeAll("\n");
 771                try writer.splatByteAll(' ', indent);
 772                try writer.writeAll(grouped.open_token.slice(tree.source));
 773                try writer.writeAll("\n");
 774                try grouped.expression.dump(tree, writer, indent + 1);
 775                try writer.splatByteAll(' ', indent);
 776                try writer.writeAll(grouped.close_token.slice(tree.source));
 777                try writer.writeAll("\n");
 778            },
 779            .not_expression => {
 780                const not: *const Node.NotExpression = @alignCast(@fieldParentPtr("base", node));
 781                try writer.writeAll(" ");
 782                try writer.writeAll(not.not_token.slice(tree.source));
 783                try writer.writeAll(" ");
 784                try writer.writeAll(not.number_token.slice(tree.source));
 785                try writer.writeAll("\n");
 786            },
 787            .accelerators => {
 788                const accelerators: *const Node.Accelerators = @alignCast(@fieldParentPtr("base", node));
 789                try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ accelerators.id.slice(tree.source), accelerators.type.slice(tree.source), accelerators.common_resource_attributes.len });
 790                for (accelerators.optional_statements) |statement| {
 791                    try statement.dump(tree, writer, indent + 1);
 792                }
 793                try writer.splatByteAll(' ', indent);
 794                try writer.writeAll(accelerators.begin_token.slice(tree.source));
 795                try writer.writeAll("\n");
 796                for (accelerators.accelerators) |accelerator| {
 797                    try accelerator.dump(tree, writer, indent + 1);
 798                }
 799                try writer.splatByteAll(' ', indent);
 800                try writer.writeAll(accelerators.end_token.slice(tree.source));
 801                try writer.writeAll("\n");
 802            },
 803            .accelerator => {
 804                const accelerator: *const Node.Accelerator = @alignCast(@fieldParentPtr("base", node));
 805                for (accelerator.type_and_options, 0..) |option, i| {
 806                    if (i != 0) try writer.writeAll(",");
 807                    try writer.writeByte(' ');
 808                    try writer.writeAll(option.slice(tree.source));
 809                }
 810                try writer.writeAll("\n");
 811                try accelerator.event.dump(tree, writer, indent + 1);
 812                try accelerator.idvalue.dump(tree, writer, indent + 1);
 813            },
 814            .dialog => {
 815                const dialog: *const Node.Dialog = @alignCast(@fieldParentPtr("base", node));
 816                try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ dialog.id.slice(tree.source), dialog.type.slice(tree.source), dialog.common_resource_attributes.len });
 817                inline for (.{ "x", "y", "width", "height" }) |arg| {
 818                    try writer.splatByteAll(' ', indent + 1);
 819                    try writer.writeAll(arg ++ ":\n");
 820                    try @field(dialog, arg).dump(tree, writer, indent + 2);
 821                }
 822                if (dialog.help_id) |help_id| {
 823                    try writer.splatByteAll(' ', indent + 1);
 824                    try writer.writeAll("help_id:\n");
 825                    try help_id.dump(tree, writer, indent + 2);
 826                }
 827                for (dialog.optional_statements) |statement| {
 828                    try statement.dump(tree, writer, indent + 1);
 829                }
 830                try writer.splatByteAll(' ', indent);
 831                try writer.writeAll(dialog.begin_token.slice(tree.source));
 832                try writer.writeAll("\n");
 833                for (dialog.controls) |control| {
 834                    try control.dump(tree, writer, indent + 1);
 835                }
 836                try writer.splatByteAll(' ', indent);
 837                try writer.writeAll(dialog.end_token.slice(tree.source));
 838                try writer.writeAll("\n");
 839            },
 840            .control_statement => {
 841                const control: *const Node.ControlStatement = @alignCast(@fieldParentPtr("base", node));
 842                try writer.print(" {s}", .{control.type.slice(tree.source)});
 843                if (control.text) |text| {
 844                    try writer.print(" text: {s}", .{text.slice(tree.source)});
 845                }
 846                try writer.writeByte('\n');
 847                if (control.class) |class| {
 848                    try writer.splatByteAll(' ', indent + 1);
 849                    try writer.writeAll("class:\n");
 850                    try class.dump(tree, writer, indent + 2);
 851                }
 852                inline for (.{ "id", "x", "y", "width", "height" }) |arg| {
 853                    try writer.splatByteAll(' ', indent + 1);
 854                    try writer.writeAll(arg ++ ":\n");
 855                    try @field(control, arg).dump(tree, writer, indent + 2);
 856                }
 857                inline for (.{ "style", "exstyle", "help_id" }) |arg| {
 858                    if (@field(control, arg)) |val_node| {
 859                        try writer.splatByteAll(' ', indent + 1);
 860                        try writer.writeAll(arg ++ ":\n");
 861                        try val_node.dump(tree, writer, indent + 2);
 862                    }
 863                }
 864                if (control.extra_data_begin != null) {
 865                    try writer.splatByteAll(' ', indent);
 866                    try writer.writeAll(control.extra_data_begin.?.slice(tree.source));
 867                    try writer.writeAll("\n");
 868                    for (control.extra_data) |data_node| {
 869                        try data_node.dump(tree, writer, indent + 1);
 870                    }
 871                    try writer.splatByteAll(' ', indent);
 872                    try writer.writeAll(control.extra_data_end.?.slice(tree.source));
 873                    try writer.writeAll("\n");
 874                }
 875            },
 876            .toolbar => {
 877                const toolbar: *const Node.Toolbar = @alignCast(@fieldParentPtr("base", node));
 878                try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ toolbar.id.slice(tree.source), toolbar.type.slice(tree.source), toolbar.common_resource_attributes.len });
 879                inline for (.{ "button_width", "button_height" }) |arg| {
 880                    try writer.splatByteAll(' ', indent + 1);
 881                    try writer.writeAll(arg ++ ":\n");
 882                    try @field(toolbar, arg).dump(tree, writer, indent + 2);
 883                }
 884                try writer.splatByteAll(' ', indent);
 885                try writer.writeAll(toolbar.begin_token.slice(tree.source));
 886                try writer.writeAll("\n");
 887                for (toolbar.buttons) |button_or_sep| {
 888                    try button_or_sep.dump(tree, writer, indent + 1);
 889                }
 890                try writer.splatByteAll(' ', indent);
 891                try writer.writeAll(toolbar.end_token.slice(tree.source));
 892                try writer.writeAll("\n");
 893            },
 894            .menu => {
 895                const menu: *const Node.Menu = @alignCast(@fieldParentPtr("base", node));
 896                try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ menu.id.slice(tree.source), menu.type.slice(tree.source), menu.common_resource_attributes.len });
 897                for (menu.optional_statements) |statement| {
 898                    try statement.dump(tree, writer, indent + 1);
 899                }
 900                if (menu.help_id) |help_id| {
 901                    try writer.splatByteAll(' ', indent + 1);
 902                    try writer.writeAll("help_id:\n");
 903                    try help_id.dump(tree, writer, indent + 2);
 904                }
 905                try writer.splatByteAll(' ', indent);
 906                try writer.writeAll(menu.begin_token.slice(tree.source));
 907                try writer.writeAll("\n");
 908                for (menu.items) |item| {
 909                    try item.dump(tree, writer, indent + 1);
 910                }
 911                try writer.splatByteAll(' ', indent);
 912                try writer.writeAll(menu.end_token.slice(tree.source));
 913                try writer.writeAll("\n");
 914            },
 915            .menu_item => {
 916                const menu_item: *const Node.MenuItem = @alignCast(@fieldParentPtr("base", node));
 917                try writer.print(" {s} {s} [{d} options]\n", .{ menu_item.menuitem.slice(tree.source), menu_item.text.slice(tree.source), menu_item.option_list.len });
 918                try menu_item.result.dump(tree, writer, indent + 1);
 919            },
 920            .menu_item_separator => {
 921                const menu_item: *const Node.MenuItemSeparator = @alignCast(@fieldParentPtr("base", node));
 922                try writer.print(" {s} {s}\n", .{ menu_item.menuitem.slice(tree.source), menu_item.separator.slice(tree.source) });
 923            },
 924            .menu_item_ex => {
 925                const menu_item: *const Node.MenuItemEx = @alignCast(@fieldParentPtr("base", node));
 926                try writer.print(" {s} {s}\n", .{ menu_item.menuitem.slice(tree.source), menu_item.text.slice(tree.source) });
 927                inline for (.{ "id", "type", "state" }) |arg| {
 928                    if (@field(menu_item, arg)) |val_node| {
 929                        try writer.splatByteAll(' ', indent + 1);
 930                        try writer.writeAll(arg ++ ":\n");
 931                        try val_node.dump(tree, writer, indent + 2);
 932                    }
 933                }
 934            },
 935            .popup => {
 936                const popup: *const Node.Popup = @alignCast(@fieldParentPtr("base", node));
 937                try writer.print(" {s} {s} [{d} options]\n", .{ popup.popup.slice(tree.source), popup.text.slice(tree.source), popup.option_list.len });
 938                try writer.splatByteAll(' ', indent);
 939                try writer.writeAll(popup.begin_token.slice(tree.source));
 940                try writer.writeAll("\n");
 941                for (popup.items) |item| {
 942                    try item.dump(tree, writer, indent + 1);
 943                }
 944                try writer.splatByteAll(' ', indent);
 945                try writer.writeAll(popup.end_token.slice(tree.source));
 946                try writer.writeAll("\n");
 947            },
 948            .popup_ex => {
 949                const popup: *const Node.PopupEx = @alignCast(@fieldParentPtr("base", node));
 950                try writer.print(" {s} {s}\n", .{ popup.popup.slice(tree.source), popup.text.slice(tree.source) });
 951                inline for (.{ "id", "type", "state", "help_id" }) |arg| {
 952                    if (@field(popup, arg)) |val_node| {
 953                        try writer.splatByteAll(' ', indent + 1);
 954                        try writer.writeAll(arg ++ ":\n");
 955                        try val_node.dump(tree, writer, indent + 2);
 956                    }
 957                }
 958                try writer.splatByteAll(' ', indent);
 959                try writer.writeAll(popup.begin_token.slice(tree.source));
 960                try writer.writeAll("\n");
 961                for (popup.items) |item| {
 962                    try item.dump(tree, writer, indent + 1);
 963                }
 964                try writer.splatByteAll(' ', indent);
 965                try writer.writeAll(popup.end_token.slice(tree.source));
 966                try writer.writeAll("\n");
 967            },
 968            .version_info => {
 969                const version_info: *const Node.VersionInfo = @alignCast(@fieldParentPtr("base", node));
 970                try writer.print(" {s} {s} [{d} common_resource_attributes]\n", .{ version_info.id.slice(tree.source), version_info.versioninfo.slice(tree.source), version_info.common_resource_attributes.len });
 971                for (version_info.fixed_info) |fixed_info| {
 972                    try fixed_info.dump(tree, writer, indent + 1);
 973                }
 974                try writer.splatByteAll(' ', indent);
 975                try writer.writeAll(version_info.begin_token.slice(tree.source));
 976                try writer.writeAll("\n");
 977                for (version_info.block_statements) |block| {
 978                    try block.dump(tree, writer, indent + 1);
 979                }
 980                try writer.splatByteAll(' ', indent);
 981                try writer.writeAll(version_info.end_token.slice(tree.source));
 982                try writer.writeAll("\n");
 983            },
 984            .version_statement => {
 985                const version_statement: *const Node.VersionStatement = @alignCast(@fieldParentPtr("base", node));
 986                try writer.print(" {s}\n", .{version_statement.type.slice(tree.source)});
 987                for (version_statement.parts) |part| {
 988                    try part.dump(tree, writer, indent + 1);
 989                }
 990            },
 991            .block => {
 992                const block: *const Node.Block = @alignCast(@fieldParentPtr("base", node));
 993                try writer.print(" {s} {s}\n", .{ block.identifier.slice(tree.source), block.key.slice(tree.source) });
 994                for (block.values) |value| {
 995                    try value.dump(tree, writer, indent + 1);
 996                }
 997                try writer.splatByteAll(' ', indent);
 998                try writer.writeAll(block.begin_token.slice(tree.source));
 999                try writer.writeAll("\n");
1000                for (block.children) |child| {
1001                    try child.dump(tree, writer, indent + 1);
1002                }
1003                try writer.splatByteAll(' ', indent);
1004                try writer.writeAll(block.end_token.slice(tree.source));
1005                try writer.writeAll("\n");
1006            },
1007            .block_value => {
1008                const block_value: *const Node.BlockValue = @alignCast(@fieldParentPtr("base", node));
1009                try writer.print(" {s} {s}\n", .{ block_value.identifier.slice(tree.source), block_value.key.slice(tree.source) });
1010                for (block_value.values) |value| {
1011                    try value.dump(tree, writer, indent + 1);
1012                }
1013            },
1014            .block_value_value => {
1015                const block_value: *const Node.BlockValueValue = @alignCast(@fieldParentPtr("base", node));
1016                if (block_value.trailing_comma) {
1017                    try writer.writeAll(" ,");
1018                }
1019                try writer.writeAll("\n");
1020                try block_value.expression.dump(tree, writer, indent + 1);
1021            },
1022            .string_table => {
1023                const string_table: *const Node.StringTable = @alignCast(@fieldParentPtr("base", node));
1024                try writer.print(" {s} [{d} common_resource_attributes]\n", .{ string_table.type.slice(tree.source), string_table.common_resource_attributes.len });
1025                for (string_table.optional_statements) |statement| {
1026                    try statement.dump(tree, writer, indent + 1);
1027                }
1028                try writer.splatByteAll(' ', indent);
1029                try writer.writeAll(string_table.begin_token.slice(tree.source));
1030                try writer.writeAll("\n");
1031                for (string_table.strings) |string| {
1032                    try string.dump(tree, writer, indent + 1);
1033                }
1034                try writer.splatByteAll(' ', indent);
1035                try writer.writeAll(string_table.end_token.slice(tree.source));
1036                try writer.writeAll("\n");
1037            },
1038            .string_table_string => {
1039                try writer.writeAll("\n");
1040                const string: *const Node.StringTableString = @alignCast(@fieldParentPtr("base", node));
1041                try string.id.dump(tree, writer, indent + 1);
1042                try writer.splatByteAll(' ', indent + 1);
1043                try writer.print("{s}\n", .{string.string.slice(tree.source)});
1044            },
1045            .language_statement => {
1046                const language: *const Node.LanguageStatement = @alignCast(@fieldParentPtr("base", node));
1047                try writer.print(" {s}\n", .{language.language_token.slice(tree.source)});
1048                try language.primary_language_id.dump(tree, writer, indent + 1);
1049                try language.sublanguage_id.dump(tree, writer, indent + 1);
1050            },
1051            .font_statement => {
1052                const font: *const Node.FontStatement = @alignCast(@fieldParentPtr("base", node));
1053                try writer.print(" {s} typeface: {s}\n", .{ font.identifier.slice(tree.source), font.typeface.slice(tree.source) });
1054                try writer.splatByteAll(' ', indent + 1);
1055                try writer.writeAll("point_size:\n");
1056                try font.point_size.dump(tree, writer, indent + 2);
1057                inline for (.{ "weight", "italic", "char_set" }) |arg| {
1058                    if (@field(font, arg)) |arg_node| {
1059                        try writer.splatByteAll(' ', indent + 1);
1060                        try writer.writeAll(arg ++ ":\n");
1061                        try arg_node.dump(tree, writer, indent + 2);
1062                    }
1063                }
1064            },
1065            .simple_statement => {
1066                const statement: *const Node.SimpleStatement = @alignCast(@fieldParentPtr("base", node));
1067                try writer.print(" {s}\n", .{statement.identifier.slice(tree.source)});
1068                try statement.value.dump(tree, writer, indent + 1);
1069            },
1070            .invalid => {
1071                const invalid: *const Node.Invalid = @alignCast(@fieldParentPtr("base", node));
1072                try writer.print(" context.len: {}\n", .{invalid.context.len});
1073                for (invalid.context) |context_token| {
1074                    try writer.splatByteAll(' ', indent + 1);
1075                    try writer.print("{s}:{s}", .{ @tagName(context_token.id), context_token.slice(tree.source) });
1076                    try writer.writeByte('\n');
1077                }
1078            },
1079        }
1080    }
1081};