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};