zig

fork of https://codeberg.org/ziglang/zig
Log | Files | Refs | README | LICENSE

compile.zig (173912B) - Raw


      1 const std = @import("std");
      2 const builtin = @import("builtin");
      3 const Allocator = std.mem.Allocator;
      4 const Node = @import("ast.zig").Node;
      5 const lex = @import("lex.zig");
      6 const Parser = @import("parse.zig").Parser;
      7 const ResourceType = @import("rc.zig").ResourceType;
      8 const Token = @import("lex.zig").Token;
      9 const literals = @import("literals.zig");
     10 const Number = literals.Number;
     11 const SourceBytes = literals.SourceBytes;
     12 const Diagnostics = @import("errors.zig").Diagnostics;
     13 const ErrorDetails = @import("errors.zig").ErrorDetails;
     14 const MemoryFlags = @import("res.zig").MemoryFlags;
     15 const rc = @import("rc.zig");
     16 const res = @import("res.zig");
     17 const ico = @import("ico.zig");
     18 const ani = @import("ani.zig");
     19 const bmp = @import("bmp.zig");
     20 const WORD = std.os.windows.WORD;
     21 const DWORD = std.os.windows.DWORD;
     22 const utils = @import("utils.zig");
     23 const NameOrOrdinal = res.NameOrOrdinal;
     24 const SupportedCodePage = @import("code_pages.zig").SupportedCodePage;
     25 const CodePageLookup = @import("ast.zig").CodePageLookup;
     26 const SourceMappings = @import("source_mapping.zig").SourceMappings;
     27 const windows1252 = @import("windows1252.zig");
     28 const lang = @import("lang.zig");
     29 const code_pages = @import("code_pages.zig");
     30 const errors = @import("errors.zig");
     31 const native_endian = builtin.cpu.arch.endian();
     32 
     33 pub const CompileOptions = struct {
     34     cwd: std.fs.Dir,
     35     diagnostics: *Diagnostics,
     36     source_mappings: ?*SourceMappings = null,
     37     /// List of paths (absolute or relative to `cwd`) for every file that the resources within the .rc file depend on.
     38     /// Items within the list will be allocated using the allocator of the ArrayList and must be
     39     /// freed by the caller.
     40     /// TODO: Maybe a dedicated struct for this purpose so that it's a bit nicer to work with.
     41     dependencies_list: ?*std.array_list.Managed([]const u8) = null,
     42     default_code_page: SupportedCodePage = .windows1252,
     43     /// If true, the first #pragma code_page directive only sets the input code page, but not the output code page.
     44     /// This check must be done before comments are removed from the file.
     45     disjoint_code_page: bool = false,
     46     ignore_include_env_var: bool = false,
     47     extra_include_paths: []const []const u8 = &.{},
     48     /// This is just an API convenience to allow separately passing 'system' (i.e. those
     49     /// that would normally be gotten from the INCLUDE env var) include paths. This is mostly
     50     /// intended for use when setting `ignore_include_env_var = true`. When `ignore_include_env_var`
     51     /// is false, `system_include_paths` will be searched before the paths in the INCLUDE env var.
     52     system_include_paths: []const []const u8 = &.{},
     53     default_language_id: ?u16 = null,
     54     // TODO: Implement verbose output
     55     verbose: bool = false,
     56     null_terminate_string_table_strings: bool = false,
     57     /// Note: This is a u15 to ensure that the maximum number of UTF-16 code units
     58     ///       plus a null-terminator can always fit into a u16.
     59     max_string_literal_codepoints: u15 = lex.default_max_string_literal_codepoints,
     60     silent_duplicate_control_ids: bool = false,
     61     warn_instead_of_error_on_invalid_code_page: bool = false,
     62 };
     63 
     64 pub fn compile(allocator: Allocator, source: []const u8, writer: anytype, options: CompileOptions) !void {
     65     var lexer = lex.Lexer.init(source, .{
     66         .default_code_page = options.default_code_page,
     67         .source_mappings = options.source_mappings,
     68         .max_string_literal_codepoints = options.max_string_literal_codepoints,
     69     });
     70     var parser = Parser.init(&lexer, .{
     71         .warn_instead_of_error_on_invalid_code_page = options.warn_instead_of_error_on_invalid_code_page,
     72         .disjoint_code_page = options.disjoint_code_page,
     73     });
     74     var tree = try parser.parse(allocator, options.diagnostics);
     75     defer tree.deinit();
     76 
     77     var search_dirs = std.array_list.Managed(SearchDir).init(allocator);
     78     defer {
     79         for (search_dirs.items) |*search_dir| {
     80             search_dir.deinit(allocator);
     81         }
     82         search_dirs.deinit();
     83     }
     84 
     85     if (options.source_mappings) |source_mappings| {
     86         const root_path = source_mappings.files.get(source_mappings.root_filename_offset);
     87         // If dirname returns null, then the root path will be the same as
     88         // the cwd so we don't need to add it as a distinct search path.
     89         if (std.fs.path.dirname(root_path)) |root_dir_path| {
     90             var root_dir = try options.cwd.openDir(root_dir_path, .{});
     91             errdefer root_dir.close();
     92             try search_dirs.append(.{ .dir = root_dir, .path = try allocator.dupe(u8, root_dir_path) });
     93         }
     94     }
     95     // Re-open the passed in cwd since we want to be able to close it (std.fs.cwd() shouldn't be closed)
     96     const cwd_dir = options.cwd.openDir(".", .{}) catch |err| {
     97         try options.diagnostics.append(.{
     98             .err = .failed_to_open_cwd,
     99             .token = .{
    100                 .id = .invalid,
    101                 .start = 0,
    102                 .end = 0,
    103                 .line_number = 1,
    104             },
    105             .code_page = .utf8,
    106             .print_source_line = false,
    107             .extra = .{ .file_open_error = .{
    108                 .err = ErrorDetails.FileOpenError.enumFromError(err),
    109                 .filename_string_index = undefined,
    110             } },
    111         });
    112         return error.CompileError;
    113     };
    114     try search_dirs.append(.{ .dir = cwd_dir, .path = null });
    115     for (options.extra_include_paths) |extra_include_path| {
    116         var dir = openSearchPathDir(options.cwd, extra_include_path) catch {
    117             // TODO: maybe a warning that the search path is skipped?
    118             continue;
    119         };
    120         errdefer dir.close();
    121         try search_dirs.append(.{ .dir = dir, .path = try allocator.dupe(u8, extra_include_path) });
    122     }
    123     for (options.system_include_paths) |system_include_path| {
    124         var dir = openSearchPathDir(options.cwd, system_include_path) catch {
    125             // TODO: maybe a warning that the search path is skipped?
    126             continue;
    127         };
    128         errdefer dir.close();
    129         try search_dirs.append(.{ .dir = dir, .path = try allocator.dupe(u8, system_include_path) });
    130     }
    131     if (!options.ignore_include_env_var) {
    132         const INCLUDE = std.process.getEnvVarOwned(allocator, "INCLUDE") catch "";
    133         defer allocator.free(INCLUDE);
    134 
    135         // The only precedence here is llvm-rc which also uses the platform-specific
    136         // delimiter. There's no precedence set by `rc.exe` since it's Windows-only.
    137         const delimiter = switch (builtin.os.tag) {
    138             .windows => ';',
    139             else => ':',
    140         };
    141         var it = std.mem.tokenizeScalar(u8, INCLUDE, delimiter);
    142         while (it.next()) |search_path| {
    143             var dir = openSearchPathDir(options.cwd, search_path) catch continue;
    144             errdefer dir.close();
    145             try search_dirs.append(.{ .dir = dir, .path = try allocator.dupe(u8, search_path) });
    146         }
    147     }
    148 
    149     var arena_allocator = std.heap.ArenaAllocator.init(allocator);
    150     defer arena_allocator.deinit();
    151     const arena = arena_allocator.allocator();
    152 
    153     var compiler = Compiler{
    154         .source = source,
    155         .arena = arena,
    156         .allocator = allocator,
    157         .cwd = options.cwd,
    158         .diagnostics = options.diagnostics,
    159         .dependencies_list = options.dependencies_list,
    160         .input_code_pages = &tree.input_code_pages,
    161         .output_code_pages = &tree.output_code_pages,
    162         // This is only safe because we know search_dirs won't be modified past this point
    163         .search_dirs = search_dirs.items,
    164         .null_terminate_string_table_strings = options.null_terminate_string_table_strings,
    165         .silent_duplicate_control_ids = options.silent_duplicate_control_ids,
    166     };
    167     if (options.default_language_id) |default_language_id| {
    168         compiler.state.language = res.Language.fromInt(default_language_id);
    169     }
    170 
    171     try compiler.writeRoot(tree.root(), writer);
    172 }
    173 
    174 pub const Compiler = struct {
    175     source: []const u8,
    176     arena: Allocator,
    177     allocator: Allocator,
    178     cwd: std.fs.Dir,
    179     state: State = .{},
    180     diagnostics: *Diagnostics,
    181     dependencies_list: ?*std.array_list.Managed([]const u8),
    182     input_code_pages: *const CodePageLookup,
    183     output_code_pages: *const CodePageLookup,
    184     search_dirs: []SearchDir,
    185     null_terminate_string_table_strings: bool,
    186     silent_duplicate_control_ids: bool,
    187 
    188     pub const State = struct {
    189         icon_id: u16 = 1,
    190         string_tables: StringTablesByLanguage = .{},
    191         language: res.Language = .{},
    192         font_dir: FontDir = .{},
    193         version: u32 = 0,
    194         characteristics: u32 = 0,
    195     };
    196 
    197     pub fn writeRoot(self: *Compiler, root: *Node.Root, writer: anytype) !void {
    198         try writeEmptyResource(writer);
    199         for (root.body) |node| {
    200             try self.writeNode(node, writer);
    201         }
    202 
    203         // now write the FONTDIR (if it has anything in it)
    204         try self.state.font_dir.writeResData(self, writer);
    205         if (self.state.font_dir.fonts.items.len != 0) {
    206             // The Win32 RC compiler may write a different FONTDIR resource than us,
    207             // due to it sometimes writing a non-zero-length device name/face name
    208             // whereas we *always* write them both as zero-length.
    209             //
    210             // In practical terms, this doesn't matter, since for various reasons the format
    211             // of the FONTDIR cannot be relied on and is seemingly not actually used by anything
    212             // anymore. We still want to emit some sort of diagnostic for the purposes of being able
    213             // to know that our .RES is intentionally not meant to be byte-for-byte identical with
    214             // the rc.exe output.
    215             //
    216             // By using the hint type here, we allow this diagnostic to be detected in code,
    217             // but it will not be printed since the end-user doesn't need to care.
    218             try self.addErrorDetails(.{
    219                 .err = .result_contains_fontdir,
    220                 .type = .hint,
    221                 .token = .{
    222                     .id = .invalid,
    223                     .start = 0,
    224                     .end = 0,
    225                     .line_number = 1,
    226                 },
    227             });
    228         }
    229         // once we've written every else out, we can write out the finalized STRINGTABLE resources
    230         var string_tables_it = self.state.string_tables.tables.iterator();
    231         while (string_tables_it.next()) |string_table_entry| {
    232             var string_table_it = string_table_entry.value_ptr.blocks.iterator();
    233             while (string_table_it.next()) |entry| {
    234                 try entry.value_ptr.writeResData(self, string_table_entry.key_ptr.*, entry.key_ptr.*, writer);
    235             }
    236         }
    237     }
    238 
    239     pub fn writeNode(self: *Compiler, node: *Node, writer: anytype) !void {
    240         switch (node.id) {
    241             .root => unreachable, // writeRoot should be called directly instead
    242             .resource_external => try self.writeResourceExternal(@alignCast(@fieldParentPtr("base", node)), writer),
    243             .resource_raw_data => try self.writeResourceRawData(@alignCast(@fieldParentPtr("base", node)), writer),
    244             .literal => unreachable, // this is context dependent and should be handled by its parent
    245             .binary_expression => unreachable,
    246             .grouped_expression => unreachable,
    247             .not_expression => unreachable,
    248             .invalid => {}, // no-op, currently only used for dangling literals at EOF
    249             .accelerators => try self.writeAccelerators(@alignCast(@fieldParentPtr("base", node)), writer),
    250             .accelerator => unreachable, // handled by writeAccelerators
    251             .dialog => try self.writeDialog(@alignCast(@fieldParentPtr("base", node)), writer),
    252             .control_statement => unreachable,
    253             .toolbar => try self.writeToolbar(@alignCast(@fieldParentPtr("base", node)), writer),
    254             .menu => try self.writeMenu(@alignCast(@fieldParentPtr("base", node)), writer),
    255             .menu_item => unreachable,
    256             .menu_item_separator => unreachable,
    257             .menu_item_ex => unreachable,
    258             .popup => unreachable,
    259             .popup_ex => unreachable,
    260             .version_info => try self.writeVersionInfo(@alignCast(@fieldParentPtr("base", node)), writer),
    261             .version_statement => unreachable,
    262             .block => unreachable,
    263             .block_value => unreachable,
    264             .block_value_value => unreachable,
    265             .string_table => try self.writeStringTable(@alignCast(@fieldParentPtr("base", node))),
    266             .string_table_string => unreachable, // handled by writeStringTable
    267             .language_statement => self.writeLanguageStatement(@alignCast(@fieldParentPtr("base", node))),
    268             .font_statement => unreachable,
    269             .simple_statement => self.writeTopLevelSimpleStatement(@alignCast(@fieldParentPtr("base", node))),
    270         }
    271     }
    272 
    273     /// Returns the filename encoded as UTF-8 (allocated by self.allocator)
    274     pub fn evaluateFilenameExpression(self: *Compiler, expression_node: *Node) ![]u8 {
    275         switch (expression_node.id) {
    276             .literal => {
    277                 const literal_node = expression_node.cast(.literal).?;
    278                 switch (literal_node.token.id) {
    279                     .literal, .number => {
    280                         const slice = literal_node.token.slice(self.source);
    281                         const code_page = self.input_code_pages.getForToken(literal_node.token);
    282                         var buf = try std.array_list.Managed(u8).initCapacity(self.allocator, slice.len);
    283                         errdefer buf.deinit();
    284 
    285                         var index: usize = 0;
    286                         while (code_page.codepointAt(index, slice)) |codepoint| : (index += codepoint.byte_len) {
    287                             const c = codepoint.value;
    288                             if (c == code_pages.Codepoint.invalid) {
    289                                 try buf.appendSlice("�");
    290                             } else {
    291                                 // Anything that is not returned as an invalid codepoint must be encodable as UTF-8.
    292                                 const utf8_len = std.unicode.utf8CodepointSequenceLength(c) catch unreachable;
    293                                 try buf.ensureUnusedCapacity(utf8_len);
    294                                 _ = std.unicode.utf8Encode(c, buf.unusedCapacitySlice()) catch unreachable;
    295                                 buf.items.len += utf8_len;
    296                             }
    297                         }
    298 
    299                         return buf.toOwnedSlice();
    300                     },
    301                     .quoted_ascii_string, .quoted_wide_string => {
    302                         const slice = literal_node.token.slice(self.source);
    303                         const column = literal_node.token.calculateColumn(self.source, 8, null);
    304                         const bytes = SourceBytes{ .slice = slice, .code_page = self.input_code_pages.getForToken(literal_node.token) };
    305 
    306                         var buf = std.array_list.Managed(u8).init(self.allocator);
    307                         errdefer buf.deinit();
    308 
    309                         // Filenames are sort-of parsed as if they were wide strings, but the max escape width of
    310                         // hex/octal escapes is still determined by the L prefix. Since we want to end up with
    311                         // UTF-8, we can parse either string type directly to UTF-8.
    312                         var parser = literals.IterativeStringParser.init(bytes, .{
    313                             .start_column = column,
    314                             .diagnostics = self.errContext(literal_node.token),
    315                             // TODO: Re-evaluate this. It's not been tested whether or not using the actual
    316                             //       output code page would make more sense.
    317                             .output_code_page = .windows1252,
    318                         });
    319 
    320                         while (try parser.nextUnchecked()) |parsed| {
    321                             const c = parsed.codepoint;
    322                             if (c == code_pages.Codepoint.invalid) {
    323                                 try buf.appendSlice("�");
    324                             } else {
    325                                 var codepoint_buf: [4]u8 = undefined;
    326                                 // If the codepoint cannot be encoded, we fall back to �
    327                                 if (std.unicode.utf8Encode(c, &codepoint_buf)) |len| {
    328                                     try buf.appendSlice(codepoint_buf[0..len]);
    329                                 } else |_| {
    330                                     try buf.appendSlice("�");
    331                                 }
    332                             }
    333                         }
    334 
    335                         return buf.toOwnedSlice();
    336                     },
    337                     else => unreachable, // no other token types should be in a filename literal node
    338                 }
    339             },
    340             .binary_expression => {
    341                 const binary_expression_node = expression_node.cast(.binary_expression).?;
    342                 return self.evaluateFilenameExpression(binary_expression_node.right);
    343             },
    344             .grouped_expression => {
    345                 const grouped_expression_node = expression_node.cast(.grouped_expression).?;
    346                 return self.evaluateFilenameExpression(grouped_expression_node.expression);
    347             },
    348             else => unreachable,
    349         }
    350     }
    351 
    352     /// https://learn.microsoft.com/en-us/windows/win32/menurc/searching-for-files
    353     ///
    354     /// Searches, in this order:
    355     ///  Directory of the 'root' .rc file (if different from CWD)
    356     ///  CWD
    357     ///  extra_include_paths (resolved relative to CWD)
    358     ///  system_include_paths (resolve relative to CWD)
    359     ///  INCLUDE environment var paths (only if ignore_include_env_var is false; resolved relative to CWD)
    360     ///
    361     /// Note: The CWD being searched *in addition to* the directory of the 'root' .rc file
    362     ///       is also how the Win32 RC compiler preprocessor searches for includes, but that
    363     ///       differs from how the clang preprocessor searches for includes.
    364     ///
    365     /// Note: This will always return the first matching file that can be opened.
    366     ///       This matches the Win32 RC compiler, which will fail with an error if the first
    367     ///       matching file is invalid. That is, it does not do the `cmd` PATH searching
    368     ///       thing of continuing to look for matching files until it finds a valid
    369     ///       one if a matching file is invalid.
    370     fn searchForFile(self: *Compiler, path: []const u8) !std.fs.File {
    371         // If the path is absolute, then it is not resolved relative to any search
    372         // paths, so there's no point in checking them.
    373         //
    374         // This behavior was determined/confirmed with the following test:
    375         // - A `test.rc` file with the contents `1 RCDATA "/test.bin"`
    376         // - A `test.bin` file at `C:\test.bin`
    377         // - A `test.bin` file at `inc\test.bin` relative to the .rc file
    378         // - Invoking `rc` with `rc /i inc test.rc`
    379         //
    380         // This results in a .res file with the contents of `C:\test.bin`, not
    381         // the contents of `inc\test.bin`. Further, if `C:\test.bin` is deleted,
    382         // then it start failing to find `/test.bin`, meaning that it does not resolve
    383         // `/test.bin` relative to include paths and instead only treats it as
    384         // an absolute path.
    385         if (std.fs.path.isAbsolute(path)) {
    386             const file = try utils.openFileNotDir(std.fs.cwd(), path, .{});
    387             errdefer file.close();
    388 
    389             if (self.dependencies_list) |dependencies_list| {
    390                 const duped_path = try dependencies_list.allocator.dupe(u8, path);
    391                 errdefer dependencies_list.allocator.free(duped_path);
    392                 try dependencies_list.append(duped_path);
    393             }
    394         }
    395 
    396         var first_error: ?std.fs.File.OpenError = null;
    397         for (self.search_dirs) |search_dir| {
    398             if (utils.openFileNotDir(search_dir.dir, path, .{})) |file| {
    399                 errdefer file.close();
    400 
    401                 if (self.dependencies_list) |dependencies_list| {
    402                     const searched_file_path = try std.fs.path.join(dependencies_list.allocator, &.{
    403                         search_dir.path orelse "", path,
    404                     });
    405                     errdefer dependencies_list.allocator.free(searched_file_path);
    406                     try dependencies_list.append(searched_file_path);
    407                 }
    408 
    409                 return file;
    410             } else |err| if (first_error == null) {
    411                 first_error = err;
    412             }
    413         }
    414         return first_error orelse error.FileNotFound;
    415     }
    416 
    417     /// Returns a Windows-1252 encoded string regardless of the current output code page.
    418     /// All codepoints are encoded as a maximum of 2 bytes, where unescaped codepoints
    419     /// >= 0x10000 are encoded as `??` and everything else is encoded as 1 byte.
    420     pub fn parseDlgIncludeString(self: *Compiler, token: Token) ![]u8 {
    421         const bytes = self.sourceBytesForToken(token);
    422         const output_code_page = self.output_code_pages.getForToken(token);
    423 
    424         var buf = try std.array_list.Managed(u8).initCapacity(self.allocator, bytes.slice.len);
    425         errdefer buf.deinit();
    426 
    427         var iterative_parser = literals.IterativeStringParser.init(bytes, .{
    428             .start_column = token.calculateColumn(self.source, 8, null),
    429             .diagnostics = self.errContext(token),
    430             // TODO: Potentially re-evaluate this, it's not been tested whether or not
    431             //       using the actual output code page would make more sense.
    432             .output_code_page = .windows1252,
    433         });
    434 
    435         // This is similar to the logic in parseQuotedString, but ends up with everything
    436         // encoded as Windows-1252. This effectively consolidates the two-step process
    437         // of rc.exe into one step, since rc.exe's preprocessor converts to UTF-16 (this
    438         // is when invalid sequences are replaced by the replacement character (U+FFFD)),
    439         // and then that's run through the parser. Our preprocessor keeps things in their
    440         // original encoding, meaning we emulate the <encoding> -> UTF-16 -> Windows-1252
    441         // results all at once.
    442         while (try iterative_parser.next()) |parsed| {
    443             const c = parsed.codepoint;
    444             switch (iterative_parser.declared_string_type) {
    445                 .wide => {
    446                     if (windows1252.bestFitFromCodepoint(c)) |best_fit| {
    447                         try buf.append(best_fit);
    448                     } else if (c < 0x10000 or c == code_pages.Codepoint.invalid or parsed.escaped_surrogate_pair) {
    449                         try buf.append('?');
    450                     } else {
    451                         try buf.appendSlice("??");
    452                     }
    453                 },
    454                 .ascii => {
    455                     if (parsed.from_escaped_integer) {
    456                         const truncated: u8 = @truncate(c);
    457                         switch (output_code_page) {
    458                             .utf8 => switch (truncated) {
    459                                 0...0x7F => try buf.append(truncated),
    460                                 else => try buf.append('?'),
    461                             },
    462                             .windows1252 => {
    463                                 try buf.append(truncated);
    464                             },
    465                         }
    466                     } else {
    467                         if (windows1252.bestFitFromCodepoint(c)) |best_fit| {
    468                             try buf.append(best_fit);
    469                         } else if (c < 0x10000 or c == code_pages.Codepoint.invalid) {
    470                             try buf.append('?');
    471                         } else {
    472                             try buf.appendSlice("??");
    473                         }
    474                     }
    475                 },
    476             }
    477         }
    478 
    479         return buf.toOwnedSlice();
    480     }
    481 
    482     pub fn writeResourceExternal(self: *Compiler, node: *Node.ResourceExternal, writer: anytype) !void {
    483         // Init header with data size zero for now, will need to fill it in later
    484         var header = try self.resourceHeader(node.id, node.type, .{});
    485         defer header.deinit(self.allocator);
    486 
    487         const maybe_predefined_type = header.predefinedResourceType();
    488 
    489         // DLGINCLUDE has special handling that doesn't actually need the file to exist
    490         if (maybe_predefined_type != null and maybe_predefined_type.? == .DLGINCLUDE) {
    491             const filename_token = node.filename.cast(.literal).?.token;
    492             const parsed_filename = try self.parseDlgIncludeString(filename_token);
    493             defer self.allocator.free(parsed_filename);
    494 
    495             // NUL within the parsed string acts as a terminator
    496             const parsed_filename_terminated = std.mem.sliceTo(parsed_filename, 0);
    497 
    498             header.applyMemoryFlags(node.common_resource_attributes, self.source);
    499             // This is effectively limited by `max_string_literal_codepoints` which is a u15.
    500             // Each codepoint within a DLGINCLUDE string is encoded as a maximum of
    501             // 2 bytes, which means that the maximum byte length of a DLGINCLUDE string is
    502             // (including the NUL terminator): 32,767 * 2 + 1 = 65,535 or exactly the u16 max.
    503             header.data_size = @intCast(parsed_filename_terminated.len + 1);
    504             try header.write(writer, self.errContext(node.id));
    505             try writer.writeAll(parsed_filename_terminated);
    506             try writer.writeByte(0);
    507             try writeDataPadding(writer, header.data_size);
    508             return;
    509         }
    510 
    511         const filename_utf8 = try self.evaluateFilenameExpression(node.filename);
    512         defer self.allocator.free(filename_utf8);
    513 
    514         // TODO: More robust checking of the validity of the filename.
    515         //       This currently only checks for NUL bytes, but it should probably also check for
    516         //       platform-specific invalid characters like '*', '?', '"', '<', '>', '|' (Windows)
    517         //       Related: https://github.com/ziglang/zig/pull/14533#issuecomment-1416888193
    518         if (std.mem.indexOfScalar(u8, filename_utf8, 0) != null) {
    519             return self.addErrorDetailsAndFail(.{
    520                 .err = .invalid_filename,
    521                 .token = node.filename.getFirstToken(),
    522                 .token_span_end = node.filename.getLastToken(),
    523                 .extra = .{ .number = 0 },
    524             });
    525         }
    526 
    527         // Allow plain number literals, but complex number expressions are evaluated strangely
    528         // and almost certainly lead to things not intended by the user (e.g. '(1+-1)' evaluates
    529         // to the filename '-1'), so error if the filename node is a grouped/binary expression.
    530         // Note: This is done here instead of during parsing so that we can easily include
    531         //       the evaluated filename as part of the error messages.
    532         if (node.filename.id != .literal) {
    533             const filename_string_index = try self.diagnostics.putString(filename_utf8);
    534             try self.addErrorDetails(.{
    535                 .err = .number_expression_as_filename,
    536                 .token = node.filename.getFirstToken(),
    537                 .token_span_end = node.filename.getLastToken(),
    538                 .extra = .{ .number = filename_string_index },
    539             });
    540             return self.addErrorDetailsAndFail(.{
    541                 .err = .number_expression_as_filename,
    542                 .type = .note,
    543                 .token = node.filename.getFirstToken(),
    544                 .token_span_end = node.filename.getLastToken(),
    545                 .print_source_line = false,
    546                 .extra = .{ .number = filename_string_index },
    547             });
    548         }
    549         // From here on out, we know that the filename must be comprised of a single token,
    550         // so get it here to simplify future usage.
    551         const filename_token = node.filename.getFirstToken();
    552 
    553         const file_handle = self.searchForFile(filename_utf8) catch |err| switch (err) {
    554             error.OutOfMemory => |e| return e,
    555             else => |e| {
    556                 const filename_string_index = try self.diagnostics.putString(filename_utf8);
    557                 return self.addErrorDetailsAndFail(.{
    558                     .err = .file_open_error,
    559                     .token = filename_token,
    560                     .extra = .{ .file_open_error = .{
    561                         .err = ErrorDetails.FileOpenError.enumFromError(e),
    562                         .filename_string_index = filename_string_index,
    563                     } },
    564                 });
    565             },
    566         };
    567         defer file_handle.close();
    568         var file_buffer: [2048]u8 = undefined;
    569         var file_reader = file_handle.reader(&file_buffer);
    570 
    571         if (maybe_predefined_type) |predefined_type| {
    572             switch (predefined_type) {
    573                 .GROUP_ICON, .GROUP_CURSOR => {
    574                     // Check for animated icon first
    575                     if (ani.isAnimatedIcon(file_reader.interface.adaptToOldInterface())) {
    576                         // Animated icons are just put into the resource unmodified,
    577                         // and the resource type changes to ANIICON/ANICURSOR
    578 
    579                         const new_predefined_type: res.RT = switch (predefined_type) {
    580                             .GROUP_ICON => .ANIICON,
    581                             .GROUP_CURSOR => .ANICURSOR,
    582                             else => unreachable,
    583                         };
    584                         header.type_value.ordinal = @intFromEnum(new_predefined_type);
    585                         header.memory_flags = MemoryFlags.defaults(new_predefined_type);
    586                         header.applyMemoryFlags(node.common_resource_attributes, self.source);
    587                         header.data_size = @intCast(try file_reader.getSize());
    588 
    589                         try header.write(writer, self.errContext(node.id));
    590                         try file_reader.seekTo(0);
    591                         try writeResourceData(writer, &file_reader.interface, header.data_size);
    592                         return;
    593                     }
    594 
    595                     // isAnimatedIcon moved the file cursor so reset to the start
    596                     try file_reader.seekTo(0);
    597 
    598                     const icon_dir = ico.read(self.allocator, file_reader.interface.adaptToOldInterface(), try file_reader.getSize()) catch |err| switch (err) {
    599                         error.OutOfMemory => |e| return e,
    600                         else => |e| {
    601                             return self.iconReadError(
    602                                 e,
    603                                 filename_utf8,
    604                                 filename_token,
    605                                 predefined_type,
    606                             );
    607                         },
    608                     };
    609                     defer icon_dir.deinit();
    610 
    611                     // This limit is inherent to the ico format since number of entries is a u16 field.
    612                     std.debug.assert(icon_dir.entries.len <= std.math.maxInt(u16));
    613 
    614                     // Note: The Win32 RC compiler will compile the resource as whatever type is
    615                     //       in the icon_dir regardless of the type of resource specified in the .rc.
    616                     //       This leads to unusable .res files when the types mismatch, so
    617                     //       we error instead.
    618                     const res_types_match = switch (predefined_type) {
    619                         .GROUP_ICON => icon_dir.image_type == .icon,
    620                         .GROUP_CURSOR => icon_dir.image_type == .cursor,
    621                         else => unreachable,
    622                     };
    623                     if (!res_types_match) {
    624                         return self.addErrorDetailsAndFail(.{
    625                             .err = .icon_dir_and_resource_type_mismatch,
    626                             .token = filename_token,
    627                             .extra = .{ .resource = switch (predefined_type) {
    628                                 .GROUP_ICON => .icon,
    629                                 .GROUP_CURSOR => .cursor,
    630                                 else => unreachable,
    631                             } },
    632                         });
    633                     }
    634 
    635                     // Memory flags affect the RT_ICON and the RT_GROUP_ICON differently
    636                     var icon_memory_flags = MemoryFlags.defaults(res.RT.ICON);
    637                     applyToMemoryFlags(&icon_memory_flags, node.common_resource_attributes, self.source);
    638                     applyToGroupMemoryFlags(&header.memory_flags, node.common_resource_attributes, self.source);
    639 
    640                     const first_icon_id = self.state.icon_id;
    641                     const entry_type = if (predefined_type == .GROUP_ICON) @intFromEnum(res.RT.ICON) else @intFromEnum(res.RT.CURSOR);
    642                     for (icon_dir.entries, 0..) |*entry, entry_i_usize| {
    643                         // We know that the entry index must fit within a u16, so
    644                         // cast it here to simplify usage sites.
    645                         const entry_i: u16 = @intCast(entry_i_usize);
    646                         var full_data_size = entry.data_size_in_bytes;
    647                         if (icon_dir.image_type == .cursor) {
    648                             full_data_size = std.math.add(u32, full_data_size, 4) catch {
    649                                 return self.addErrorDetailsAndFail(.{
    650                                     .err = .resource_data_size_exceeds_max,
    651                                     .token = node.id,
    652                                 });
    653                             };
    654                         }
    655 
    656                         const image_header = ResourceHeader{
    657                             .type_value = .{ .ordinal = entry_type },
    658                             .name_value = .{ .ordinal = self.state.icon_id },
    659                             .data_size = full_data_size,
    660                             .memory_flags = icon_memory_flags,
    661                             .language = self.state.language,
    662                             .version = self.state.version,
    663                             .characteristics = self.state.characteristics,
    664                         };
    665                         try image_header.write(writer, self.errContext(node.id));
    666 
    667                         // From https://learn.microsoft.com/en-us/windows/win32/menurc/localheader:
    668                         // > The LOCALHEADER structure is the first data written to the RT_CURSOR
    669                         // > resource if a RESDIR structure contains information about a cursor.
    670                         // where LOCALHEADER is `struct { WORD xHotSpot; WORD yHotSpot; }`
    671                         if (icon_dir.image_type == .cursor) {
    672                             try writer.writeInt(u16, entry.type_specific_data.cursor.hotspot_x, .little);
    673                             try writer.writeInt(u16, entry.type_specific_data.cursor.hotspot_y, .little);
    674                         }
    675 
    676                         try file_reader.seekTo(entry.data_offset_from_start_of_file);
    677                         var header_bytes: [16]u8 align(@alignOf(ico.BitmapHeader)) = (file_reader.interface.takeArray(16) catch {
    678                             return self.iconReadError(
    679                                 error.UnexpectedEOF,
    680                                 filename_utf8,
    681                                 filename_token,
    682                                 predefined_type,
    683                             );
    684                         }).*;
    685 
    686                         const image_format = ico.ImageFormat.detect(&header_bytes);
    687                         if (!image_format.validate(&header_bytes)) {
    688                             return self.iconReadError(
    689                                 error.InvalidHeader,
    690                                 filename_utf8,
    691                                 filename_token,
    692                                 predefined_type,
    693                             );
    694                         }
    695                         switch (image_format) {
    696                             .riff => switch (icon_dir.image_type) {
    697                                 .icon => {
    698                                     // The Win32 RC compiler treats this as an error, but icon dirs
    699                                     // with RIFF encoded icons within them work ~okay (they work
    700                                     // in some places but not others, they may not animate, etc) if they are
    701                                     // allowed to be compiled.
    702                                     try self.addErrorDetails(.{
    703                                         .err = .rc_would_error_on_icon_dir,
    704                                         .type = .warning,
    705                                         .token = filename_token,
    706                                         .extra = .{ .icon_dir = .{ .icon_type = .icon, .icon_format = .riff, .index = entry_i } },
    707                                     });
    708                                     try self.addErrorDetails(.{
    709                                         .err = .rc_would_error_on_icon_dir,
    710                                         .type = .note,
    711                                         .print_source_line = false,
    712                                         .token = filename_token,
    713                                         .extra = .{ .icon_dir = .{ .icon_type = .icon, .icon_format = .riff, .index = entry_i } },
    714                                     });
    715                                 },
    716                                 .cursor => {
    717                                     // The Win32 RC compiler errors in this case too, but we only error
    718                                     // here because the cursor would fail to be loaded at runtime if we
    719                                     // compiled it.
    720                                     return self.addErrorDetailsAndFail(.{
    721                                         .err = .format_not_supported_in_icon_dir,
    722                                         .token = filename_token,
    723                                         .extra = .{ .icon_dir = .{ .icon_type = .cursor, .icon_format = .riff, .index = entry_i } },
    724                                     });
    725                                 },
    726                             },
    727                             .png => switch (icon_dir.image_type) {
    728                                 .icon => {
    729                                     // PNG always seems to have 1 for color planes no matter what
    730                                     entry.type_specific_data.icon.color_planes = 1;
    731                                     // These seem to be the only values of num_colors that
    732                                     // get treated specially
    733                                     entry.type_specific_data.icon.bits_per_pixel = switch (entry.num_colors) {
    734                                         2 => 1,
    735                                         8 => 3,
    736                                         16 => 4,
    737                                         else => entry.type_specific_data.icon.bits_per_pixel,
    738                                     };
    739                                 },
    740                                 .cursor => {
    741                                     // The Win32 RC compiler treats this as an error, but cursor dirs
    742                                     // with PNG encoded icons within them work fine if they are
    743                                     // allowed to be compiled.
    744                                     try self.addErrorDetails(.{
    745                                         .err = .rc_would_error_on_icon_dir,
    746                                         .type = .warning,
    747                                         .token = filename_token,
    748                                         .extra = .{ .icon_dir = .{ .icon_type = .cursor, .icon_format = .png, .index = entry_i } },
    749                                     });
    750                                 },
    751                             },
    752                             .dib => {
    753                                 const bitmap_header: *ico.BitmapHeader = @ptrCast(@alignCast(&header_bytes));
    754                                 if (native_endian == .big) {
    755                                     std.mem.byteSwapAllFields(ico.BitmapHeader, bitmap_header);
    756                                 }
    757                                 const bitmap_version = ico.BitmapHeader.Version.get(bitmap_header.bcSize);
    758 
    759                                 // The Win32 RC compiler only allows headers with
    760                                 // `bcSize == sizeof(BITMAPINFOHEADER)`, but it seems unlikely
    761                                 // that there's a good reason for that outside of too-old
    762                                 // bitmap headers.
    763                                 // TODO: Need to test V4 and V5 bitmaps to check they actually work
    764                                 if (bitmap_version == .@"win2.0") {
    765                                     return self.addErrorDetailsAndFail(.{
    766                                         .err = .rc_would_error_on_bitmap_version,
    767                                         .token = filename_token,
    768                                         .extra = .{ .icon_dir = .{
    769                                             .icon_type = if (icon_dir.image_type == .icon) .icon else .cursor,
    770                                             .icon_format = image_format,
    771                                             .index = entry_i,
    772                                             .bitmap_version = bitmap_version,
    773                                         } },
    774                                     });
    775                                 } else if (bitmap_version != .@"nt3.1") {
    776                                     try self.addErrorDetails(.{
    777                                         .err = .rc_would_error_on_bitmap_version,
    778                                         .type = .warning,
    779                                         .token = filename_token,
    780                                         .extra = .{ .icon_dir = .{
    781                                             .icon_type = if (icon_dir.image_type == .icon) .icon else .cursor,
    782                                             .icon_format = image_format,
    783                                             .index = entry_i,
    784                                             .bitmap_version = bitmap_version,
    785                                         } },
    786                                     });
    787                                 }
    788 
    789                                 switch (icon_dir.image_type) {
    790                                     .icon => {
    791                                         // The values in the icon's BITMAPINFOHEADER always take precedence over
    792                                         // the values in the IconDir, but not in the LOCALHEADER (see above).
    793                                         entry.type_specific_data.icon.color_planes = bitmap_header.bcPlanes;
    794                                         entry.type_specific_data.icon.bits_per_pixel = bitmap_header.bcBitCount;
    795                                     },
    796                                     .cursor => {
    797                                         // Only cursors get the width/height from BITMAPINFOHEADER (icons don't)
    798                                         entry.width = @intCast(bitmap_header.bcWidth);
    799                                         entry.height = @intCast(bitmap_header.bcHeight);
    800                                         entry.type_specific_data.cursor.hotspot_x = bitmap_header.bcPlanes;
    801                                         entry.type_specific_data.cursor.hotspot_y = bitmap_header.bcBitCount;
    802                                     },
    803                                 }
    804                             },
    805                         }
    806 
    807                         try file_reader.seekTo(entry.data_offset_from_start_of_file);
    808                         try writeResourceDataNoPadding(writer, &file_reader.interface, entry.data_size_in_bytes);
    809                         try writeDataPadding(writer, full_data_size);
    810 
    811                         if (self.state.icon_id == std.math.maxInt(u16)) {
    812                             try self.addErrorDetails(.{
    813                                 .err = .max_icon_ids_exhausted,
    814                                 .print_source_line = false,
    815                                 .token = filename_token,
    816                                 .extra = .{ .icon_dir = .{
    817                                     .icon_type = if (icon_dir.image_type == .icon) .icon else .cursor,
    818                                     .icon_format = image_format,
    819                                     .index = entry_i,
    820                                 } },
    821                             });
    822                             return self.addErrorDetailsAndFail(.{
    823                                 .err = .max_icon_ids_exhausted,
    824                                 .type = .note,
    825                                 .token = filename_token,
    826                                 .extra = .{ .icon_dir = .{
    827                                     .icon_type = if (icon_dir.image_type == .icon) .icon else .cursor,
    828                                     .icon_format = image_format,
    829                                     .index = entry_i,
    830                                 } },
    831                             });
    832                         }
    833                         self.state.icon_id += 1;
    834                     }
    835 
    836                     header.data_size = icon_dir.getResDataSize();
    837 
    838                     try header.write(writer, self.errContext(node.id));
    839                     try icon_dir.writeResData(writer, first_icon_id);
    840                     try writeDataPadding(writer, header.data_size);
    841                     return;
    842                 },
    843                 .RCDATA,
    844                 .HTML,
    845                 .MESSAGETABLE,
    846                 .DLGINIT,
    847                 .PLUGPLAY,
    848                 .VXD,
    849                 // Note: All of the below can only be specified by using a number
    850                 //       as the resource type.
    851                 .MANIFEST,
    852                 .CURSOR,
    853                 .ICON,
    854                 .ANICURSOR,
    855                 .ANIICON,
    856                 .FONTDIR,
    857                 => {
    858                     header.applyMemoryFlags(node.common_resource_attributes, self.source);
    859                 },
    860                 .BITMAP => {
    861                     header.applyMemoryFlags(node.common_resource_attributes, self.source);
    862                     const file_size = try file_reader.getSize();
    863 
    864                     const bitmap_info = bmp.read(file_reader.interface.adaptToOldInterface(), file_size) catch |err| {
    865                         const filename_string_index = try self.diagnostics.putString(filename_utf8);
    866                         return self.addErrorDetailsAndFail(.{
    867                             .err = .bmp_read_error,
    868                             .token = filename_token,
    869                             .extra = .{ .bmp_read_error = .{
    870                                 .err = ErrorDetails.BitmapReadError.enumFromError(err),
    871                                 .filename_string_index = filename_string_index,
    872                             } },
    873                         });
    874                     };
    875 
    876                     if (bitmap_info.getActualPaletteByteLen() > bitmap_info.getExpectedPaletteByteLen()) {
    877                         const num_ignored_bytes = bitmap_info.getActualPaletteByteLen() - bitmap_info.getExpectedPaletteByteLen();
    878                         var number_as_bytes: [8]u8 = undefined;
    879                         std.mem.writeInt(u64, &number_as_bytes, num_ignored_bytes, native_endian);
    880                         const value_string_index = try self.diagnostics.putString(&number_as_bytes);
    881                         try self.addErrorDetails(.{
    882                             .err = .bmp_ignored_palette_bytes,
    883                             .type = .warning,
    884                             .token = filename_token,
    885                             .extra = .{ .number = value_string_index },
    886                         });
    887                     } else if (bitmap_info.getActualPaletteByteLen() < bitmap_info.getExpectedPaletteByteLen()) {
    888                         const num_padding_bytes = bitmap_info.getExpectedPaletteByteLen() - bitmap_info.getActualPaletteByteLen();
    889 
    890                         var number_as_bytes: [8]u8 = undefined;
    891                         std.mem.writeInt(u64, &number_as_bytes, num_padding_bytes, native_endian);
    892                         const value_string_index = try self.diagnostics.putString(&number_as_bytes);
    893                         try self.addErrorDetails(.{
    894                             .err = .bmp_missing_palette_bytes,
    895                             .type = .err,
    896                             .token = filename_token,
    897                             .extra = .{ .number = value_string_index },
    898                         });
    899                         const pixel_data_len = bitmap_info.getPixelDataLen(file_size);
    900                         // TODO: This is a hack, but we know we have already added
    901                         //       at least one entry to the diagnostics strings, so we can
    902                         //       get away with using 0 to mean 'no string' here.
    903                         var miscompiled_bytes_string_index: u32 = 0;
    904                         if (pixel_data_len > 0) {
    905                             const miscompiled_bytes = @min(pixel_data_len, num_padding_bytes);
    906                             std.mem.writeInt(u64, &number_as_bytes, miscompiled_bytes, native_endian);
    907                             miscompiled_bytes_string_index = try self.diagnostics.putString(&number_as_bytes);
    908                         }
    909                         return self.addErrorDetailsAndFail(.{
    910                             .err = .rc_would_miscompile_bmp_palette_padding,
    911                             .type = .note,
    912                             .print_source_line = false,
    913                             .token = filename_token,
    914                             .extra = .{ .number = miscompiled_bytes_string_index },
    915                         });
    916                     }
    917 
    918                     // TODO: It might be possible that the calculation done in this function
    919                     //       could underflow if the underlying file is modified while reading
    920                     //       it, but need to think about it more to determine if that's a
    921                     //       real possibility
    922                     const bmp_bytes_to_write: u32 = @intCast(bitmap_info.getExpectedByteLen(file_size));
    923 
    924                     header.data_size = bmp_bytes_to_write;
    925                     try header.write(writer, self.errContext(node.id));
    926                     try file_reader.seekTo(bmp.file_header_len);
    927                     try writeResourceDataNoPadding(writer, &file_reader.interface, bitmap_info.dib_header_size);
    928                     if (bitmap_info.getBitmasksByteLen() > 0) {
    929                         try writeResourceDataNoPadding(writer, &file_reader.interface, bitmap_info.getBitmasksByteLen());
    930                     }
    931                     if (bitmap_info.getExpectedPaletteByteLen() > 0) {
    932                         try writeResourceDataNoPadding(writer, &file_reader.interface, @intCast(bitmap_info.getActualPaletteByteLen()));
    933                     }
    934                     try file_reader.seekTo(bitmap_info.pixel_data_offset);
    935                     const pixel_bytes: u32 = @intCast(file_size - bitmap_info.pixel_data_offset);
    936                     try writeResourceDataNoPadding(writer, &file_reader.interface, pixel_bytes);
    937                     try writeDataPadding(writer, bmp_bytes_to_write);
    938                     return;
    939                 },
    940                 .FONT => {
    941                     if (self.state.font_dir.ids.get(header.name_value.ordinal) != null) {
    942                         // Add warning and skip this resource
    943                         // Note: The Win32 compiler prints this as an error but it doesn't fail the compilation
    944                         // and the duplicate resource is skipped.
    945                         try self.addErrorDetails(.{
    946                             .err = .font_id_already_defined,
    947                             .token = node.id,
    948                             .type = .warning,
    949                             .extra = .{ .number = header.name_value.ordinal },
    950                         });
    951                         try self.addErrorDetails(.{
    952                             .err = .font_id_already_defined,
    953                             .token = self.state.font_dir.ids.get(header.name_value.ordinal).?,
    954                             .type = .note,
    955                             .extra = .{ .number = header.name_value.ordinal },
    956                         });
    957                         return;
    958                     }
    959                     header.applyMemoryFlags(node.common_resource_attributes, self.source);
    960                     const file_size = try file_reader.getSize();
    961                     if (file_size > std.math.maxInt(u32)) {
    962                         return self.addErrorDetailsAndFail(.{
    963                             .err = .resource_data_size_exceeds_max,
    964                             .token = node.id,
    965                         });
    966                     }
    967 
    968                     // We now know that the data size will fit in a u32
    969                     header.data_size = @intCast(file_size);
    970                     try header.write(writer, self.errContext(node.id));
    971 
    972                     var header_slurping_reader = headerSlurpingReader(148, file_reader.interface.adaptToOldInterface());
    973                     var adapter = header_slurping_reader.reader().adaptToNewApi(&.{});
    974                     try writeResourceData(writer, &adapter.new_interface, header.data_size);
    975 
    976                     try self.state.font_dir.add(self.arena, FontDir.Font{
    977                         .id = header.name_value.ordinal,
    978                         .header_bytes = header_slurping_reader.slurped_header,
    979                     }, node.id);
    980                     return;
    981                 },
    982                 .ACCELERATOR, // Cannot use an external file, enforced by the parser
    983                 .DIALOG, // Cannot use an external file, enforced by the parser
    984                 .DLGINCLUDE, // Handled specially above
    985                 .MENU, // Cannot use an external file, enforced by the parser
    986                 .STRING, // Parser error if this resource is specified as a number
    987                 .TOOLBAR, // Cannot use an external file, enforced by the parser
    988                 .VERSION, // Cannot use an external file, enforced by the parser
    989                 => unreachable,
    990                 _ => unreachable,
    991             }
    992         } else {
    993             header.applyMemoryFlags(node.common_resource_attributes, self.source);
    994         }
    995 
    996         // Fallback to just writing out the entire contents of the file
    997         const data_size = try file_reader.getSize();
    998         if (data_size > std.math.maxInt(u32)) {
    999             return self.addErrorDetailsAndFail(.{
   1000                 .err = .resource_data_size_exceeds_max,
   1001                 .token = node.id,
   1002             });
   1003         }
   1004         // We now know that the data size will fit in a u32
   1005         header.data_size = @intCast(data_size);
   1006         try header.write(writer, self.errContext(node.id));
   1007         try writeResourceData(writer, &file_reader.interface, header.data_size);
   1008     }
   1009 
   1010     fn iconReadError(
   1011         self: *Compiler,
   1012         err: ico.ReadError,
   1013         filename: []const u8,
   1014         token: Token,
   1015         predefined_type: res.RT,
   1016     ) error{ CompileError, OutOfMemory } {
   1017         const filename_string_index = try self.diagnostics.putString(filename);
   1018         return self.addErrorDetailsAndFail(.{
   1019             .err = .icon_read_error,
   1020             .token = token,
   1021             .extra = .{ .icon_read_error = .{
   1022                 .err = ErrorDetails.IconReadError.enumFromError(err),
   1023                 .icon_type = switch (predefined_type) {
   1024                     .GROUP_ICON => .icon,
   1025                     .GROUP_CURSOR => .cursor,
   1026                     else => unreachable,
   1027                 },
   1028                 .filename_string_index = filename_string_index,
   1029             } },
   1030         });
   1031     }
   1032 
   1033     pub const DataType = enum {
   1034         number,
   1035         ascii_string,
   1036         wide_string,
   1037     };
   1038 
   1039     pub const Data = union(DataType) {
   1040         number: Number,
   1041         ascii_string: []const u8,
   1042         wide_string: [:0]const u16,
   1043 
   1044         pub fn deinit(self: Data, allocator: Allocator) void {
   1045             switch (self) {
   1046                 .wide_string => |wide_string| {
   1047                     allocator.free(wide_string);
   1048                 },
   1049                 .ascii_string => |ascii_string| {
   1050                     allocator.free(ascii_string);
   1051                 },
   1052                 else => {},
   1053             }
   1054         }
   1055 
   1056         pub fn write(self: Data, writer: anytype) !void {
   1057             switch (self) {
   1058                 .number => |number| switch (number.is_long) {
   1059                     false => try writer.writeInt(WORD, number.asWord(), .little),
   1060                     true => try writer.writeInt(DWORD, number.value, .little),
   1061                 },
   1062                 .ascii_string => |ascii_string| {
   1063                     try writer.writeAll(ascii_string);
   1064                 },
   1065                 .wide_string => |wide_string| {
   1066                     try writer.writeAll(std.mem.sliceAsBytes(wide_string));
   1067                 },
   1068             }
   1069         }
   1070     };
   1071 
   1072     /// Assumes that the node is a number or number expression
   1073     pub fn evaluateNumberExpression(expression_node: *Node, source: []const u8, code_page_lookup: *const CodePageLookup) Number {
   1074         switch (expression_node.id) {
   1075             .literal => {
   1076                 const literal_node = expression_node.cast(.literal).?;
   1077                 std.debug.assert(literal_node.token.id == .number);
   1078                 const bytes = SourceBytes{
   1079                     .slice = literal_node.token.slice(source),
   1080                     .code_page = code_page_lookup.getForToken(literal_node.token),
   1081                 };
   1082                 return literals.parseNumberLiteral(bytes);
   1083             },
   1084             .binary_expression => {
   1085                 const binary_expression_node = expression_node.cast(.binary_expression).?;
   1086                 const lhs = evaluateNumberExpression(binary_expression_node.left, source, code_page_lookup);
   1087                 const rhs = evaluateNumberExpression(binary_expression_node.right, source, code_page_lookup);
   1088                 const operator_char = binary_expression_node.operator.slice(source)[0];
   1089                 return lhs.evaluateOperator(operator_char, rhs);
   1090             },
   1091             .grouped_expression => {
   1092                 const grouped_expression_node = expression_node.cast(.grouped_expression).?;
   1093                 return evaluateNumberExpression(grouped_expression_node.expression, source, code_page_lookup);
   1094             },
   1095             else => unreachable,
   1096         }
   1097     }
   1098 
   1099     const FlagsNumber = struct {
   1100         value: u32,
   1101         not_mask: u32 = 0xFFFFFFFF,
   1102 
   1103         pub fn evaluateOperator(lhs: FlagsNumber, operator_char: u8, rhs: FlagsNumber) FlagsNumber {
   1104             const result = switch (operator_char) {
   1105                 '-' => lhs.value -% rhs.value,
   1106                 '+' => lhs.value +% rhs.value,
   1107                 '|' => lhs.value | rhs.value,
   1108                 '&' => lhs.value & rhs.value,
   1109                 else => unreachable, // invalid operator, this would be a lexer/parser bug
   1110             };
   1111             return .{
   1112                 .value = result,
   1113                 .not_mask = lhs.not_mask & rhs.not_mask,
   1114             };
   1115         }
   1116 
   1117         pub fn applyNotMask(self: FlagsNumber) u32 {
   1118             return self.value & self.not_mask;
   1119         }
   1120     };
   1121 
   1122     pub fn evaluateFlagsExpressionWithDefault(default: u32, expression_node: *Node, source: []const u8, code_page_lookup: *const CodePageLookup) u32 {
   1123         var context = FlagsExpressionContext{ .initial_value = default };
   1124         const number = evaluateFlagsExpression(expression_node, source, code_page_lookup, &context);
   1125         return number.value;
   1126     }
   1127 
   1128     pub const FlagsExpressionContext = struct {
   1129         initial_value: u32 = 0,
   1130         initial_value_used: bool = false,
   1131     };
   1132 
   1133     /// Assumes that the node is a number expression (which can contain not_expressions)
   1134     pub fn evaluateFlagsExpression(expression_node: *Node, source: []const u8, code_page_lookup: *const CodePageLookup, context: *FlagsExpressionContext) FlagsNumber {
   1135         switch (expression_node.id) {
   1136             .literal => {
   1137                 const literal_node = expression_node.cast(.literal).?;
   1138                 std.debug.assert(literal_node.token.id == .number);
   1139                 const bytes = SourceBytes{
   1140                     .slice = literal_node.token.slice(source),
   1141                     .code_page = code_page_lookup.getForToken(literal_node.token),
   1142                 };
   1143                 var value = literals.parseNumberLiteral(bytes).value;
   1144                 if (!context.initial_value_used) {
   1145                     context.initial_value_used = true;
   1146                     value |= context.initial_value;
   1147                 }
   1148                 return .{ .value = value };
   1149             },
   1150             .binary_expression => {
   1151                 const binary_expression_node = expression_node.cast(.binary_expression).?;
   1152                 const lhs = evaluateFlagsExpression(binary_expression_node.left, source, code_page_lookup, context);
   1153                 const rhs = evaluateFlagsExpression(binary_expression_node.right, source, code_page_lookup, context);
   1154                 const operator_char = binary_expression_node.operator.slice(source)[0];
   1155                 const result = lhs.evaluateOperator(operator_char, rhs);
   1156                 return .{ .value = result.applyNotMask() };
   1157             },
   1158             .grouped_expression => {
   1159                 const grouped_expression_node = expression_node.cast(.grouped_expression).?;
   1160                 return evaluateFlagsExpression(grouped_expression_node.expression, source, code_page_lookup, context);
   1161             },
   1162             .not_expression => {
   1163                 const not_expression = expression_node.cast(.not_expression).?;
   1164                 const bytes = SourceBytes{
   1165                     .slice = not_expression.number_token.slice(source),
   1166                     .code_page = code_page_lookup.getForToken(not_expression.number_token),
   1167                 };
   1168                 const not_number = literals.parseNumberLiteral(bytes);
   1169                 if (!context.initial_value_used) {
   1170                     context.initial_value_used = true;
   1171                     return .{ .value = context.initial_value & ~not_number.value };
   1172                 }
   1173                 return .{ .value = 0, .not_mask = ~not_number.value };
   1174             },
   1175             else => unreachable,
   1176         }
   1177     }
   1178 
   1179     pub fn evaluateDataExpression(self: *Compiler, expression_node: *Node) !Data {
   1180         switch (expression_node.id) {
   1181             .literal => {
   1182                 const literal_node = expression_node.cast(.literal).?;
   1183                 switch (literal_node.token.id) {
   1184                     .number => {
   1185                         const number = evaluateNumberExpression(expression_node, self.source, self.input_code_pages);
   1186                         return .{ .number = number };
   1187                     },
   1188                     .quoted_ascii_string => {
   1189                         const column = literal_node.token.calculateColumn(self.source, 8, null);
   1190                         const bytes = SourceBytes{
   1191                             .slice = literal_node.token.slice(self.source),
   1192                             .code_page = self.input_code_pages.getForToken(literal_node.token),
   1193                         };
   1194                         const parsed = try literals.parseQuotedAsciiString(self.allocator, bytes, .{
   1195                             .start_column = column,
   1196                             .diagnostics = self.errContext(literal_node.token),
   1197                             .output_code_page = self.output_code_pages.getForToken(literal_node.token),
   1198                         });
   1199                         errdefer self.allocator.free(parsed);
   1200                         return .{ .ascii_string = parsed };
   1201                     },
   1202                     .quoted_wide_string => {
   1203                         const column = literal_node.token.calculateColumn(self.source, 8, null);
   1204                         const bytes = SourceBytes{
   1205                             .slice = literal_node.token.slice(self.source),
   1206                             .code_page = self.input_code_pages.getForToken(literal_node.token),
   1207                         };
   1208                         const parsed_string = try literals.parseQuotedWideString(self.allocator, bytes, .{
   1209                             .start_column = column,
   1210                             .diagnostics = self.errContext(literal_node.token),
   1211                             .output_code_page = self.output_code_pages.getForToken(literal_node.token),
   1212                         });
   1213                         errdefer self.allocator.free(parsed_string);
   1214                         return .{ .wide_string = parsed_string };
   1215                     },
   1216                     else => unreachable, // no other token types should be in a data literal node
   1217                 }
   1218             },
   1219             .binary_expression, .grouped_expression => {
   1220                 const result = evaluateNumberExpression(expression_node, self.source, self.input_code_pages);
   1221                 return .{ .number = result };
   1222             },
   1223             .not_expression => unreachable,
   1224             else => unreachable,
   1225         }
   1226     }
   1227 
   1228     pub fn writeResourceRawData(self: *Compiler, node: *Node.ResourceRawData, writer: anytype) !void {
   1229         var data_buffer = std.array_list.Managed(u8).init(self.allocator);
   1230         defer data_buffer.deinit();
   1231         // The header's data length field is a u32 so limit the resource's data size so that
   1232         // we know we can always specify the real size.
   1233         var limited_writer = limitedWriter(data_buffer.writer(), std.math.maxInt(u32));
   1234         const data_writer = limited_writer.writer();
   1235 
   1236         for (node.raw_data) |expression| {
   1237             const data = try self.evaluateDataExpression(expression);
   1238             defer data.deinit(self.allocator);
   1239             data.write(data_writer) catch |err| switch (err) {
   1240                 error.NoSpaceLeft => {
   1241                     return self.addErrorDetailsAndFail(.{
   1242                         .err = .resource_data_size_exceeds_max,
   1243                         .token = node.id,
   1244                     });
   1245                 },
   1246                 else => |e| return e,
   1247             };
   1248         }
   1249 
   1250         // This intCast can't fail because the limitedWriter above guarantees that
   1251         // we will never write more than maxInt(u32) bytes.
   1252         const data_len: u32 = @intCast(data_buffer.items.len);
   1253         try self.writeResourceHeader(writer, node.id, node.type, data_len, node.common_resource_attributes, self.state.language);
   1254 
   1255         var data_fbs: std.Io.Reader = .fixed(data_buffer.items);
   1256         try writeResourceData(writer, &data_fbs, data_len);
   1257     }
   1258 
   1259     pub fn writeResourceHeader(self: *Compiler, writer: anytype, id_token: Token, type_token: Token, data_size: u32, common_resource_attributes: []Token, language: res.Language) !void {
   1260         var header = try self.resourceHeader(id_token, type_token, .{
   1261             .language = language,
   1262             .data_size = data_size,
   1263         });
   1264         defer header.deinit(self.allocator);
   1265 
   1266         header.applyMemoryFlags(common_resource_attributes, self.source);
   1267 
   1268         try header.write(writer, self.errContext(id_token));
   1269     }
   1270 
   1271     pub fn writeResourceDataNoPadding(writer: *std.Io.Writer, data_reader: *std.Io.Reader, data_size: u32) !void {
   1272         try data_reader.streamExact(writer, data_size);
   1273     }
   1274 
   1275     pub fn writeResourceData(writer: anytype, data_reader: *std.Io.Reader, data_size: u32) !void {
   1276         try writeResourceDataNoPadding(writer, data_reader, data_size);
   1277         try writeDataPadding(writer, data_size);
   1278     }
   1279 
   1280     pub fn writeDataPadding(writer: *std.Io.Writer, data_size: u32) !void {
   1281         try writer.splatByteAll(0, numPaddingBytesNeeded(data_size));
   1282     }
   1283 
   1284     pub fn numPaddingBytesNeeded(data_size: u32) u2 {
   1285         // Result is guaranteed to be between 0 and 3.
   1286         return @intCast((4 -% data_size) % 4);
   1287     }
   1288 
   1289     pub fn evaluateAcceleratorKeyExpression(self: *Compiler, node: *Node, is_virt: bool) !u16 {
   1290         if (node.isNumberExpression()) {
   1291             return evaluateNumberExpression(node, self.source, self.input_code_pages).asWord();
   1292         } else {
   1293             std.debug.assert(node.isStringLiteral());
   1294             const literal: *Node.Literal = @alignCast(@fieldParentPtr("base", node));
   1295             const bytes = SourceBytes{
   1296                 .slice = literal.token.slice(self.source),
   1297                 .code_page = self.input_code_pages.getForToken(literal.token),
   1298             };
   1299             const column = literal.token.calculateColumn(self.source, 8, null);
   1300             return res.parseAcceleratorKeyString(bytes, is_virt, .{
   1301                 .start_column = column,
   1302                 .diagnostics = self.errContext(literal.token),
   1303                 .output_code_page = self.output_code_pages.getForToken(literal.token),
   1304             });
   1305         }
   1306     }
   1307 
   1308     pub fn writeAccelerators(self: *Compiler, node: *Node.Accelerators, writer: anytype) !void {
   1309         var data_buffer = std.array_list.Managed(u8).init(self.allocator);
   1310         defer data_buffer.deinit();
   1311 
   1312         // The header's data length field is a u32 so limit the resource's data size so that
   1313         // we know we can always specify the real size.
   1314         var limited_writer = limitedWriter(data_buffer.writer(), std.math.maxInt(u32));
   1315         const data_writer = limited_writer.writer();
   1316 
   1317         self.writeAcceleratorsData(node, data_writer) catch |err| switch (err) {
   1318             error.NoSpaceLeft => {
   1319                 return self.addErrorDetailsAndFail(.{
   1320                     .err = .resource_data_size_exceeds_max,
   1321                     .token = node.id,
   1322                 });
   1323             },
   1324             else => |e| return e,
   1325         };
   1326 
   1327         // This intCast can't fail because the limitedWriter above guarantees that
   1328         // we will never write more than maxInt(u32) bytes.
   1329         const data_size: u32 = @intCast(data_buffer.items.len);
   1330         var header = try self.resourceHeader(node.id, node.type, .{
   1331             .data_size = data_size,
   1332         });
   1333         defer header.deinit(self.allocator);
   1334 
   1335         header.applyMemoryFlags(node.common_resource_attributes, self.source);
   1336         header.applyOptionalStatements(node.optional_statements, self.source, self.input_code_pages);
   1337 
   1338         try header.write(writer, self.errContext(node.id));
   1339 
   1340         var data_fbs: std.Io.Reader = .fixed(data_buffer.items);
   1341         try writeResourceData(writer, &data_fbs, data_size);
   1342     }
   1343 
   1344     /// Expects `data_writer` to be a LimitedWriter limited to u32, meaning all writes to
   1345     /// the writer within this function could return error.NoSpaceLeft
   1346     pub fn writeAcceleratorsData(self: *Compiler, node: *Node.Accelerators, data_writer: anytype) !void {
   1347         for (node.accelerators, 0..) |accel_node, i| {
   1348             const accelerator: *Node.Accelerator = @alignCast(@fieldParentPtr("base", accel_node));
   1349             var modifiers = res.AcceleratorModifiers{};
   1350             for (accelerator.type_and_options) |type_or_option| {
   1351                 const modifier = rc.AcceleratorTypeAndOptions.map.get(type_or_option.slice(self.source)).?;
   1352                 modifiers.apply(modifier);
   1353             }
   1354             if ((modifiers.isSet(.control) or modifiers.isSet(.shift)) and !modifiers.isSet(.virtkey)) {
   1355                 try self.addErrorDetails(.{
   1356                     .err = .accelerator_shift_or_control_without_virtkey,
   1357                     .type = .warning,
   1358                     // We know that one of SHIFT or CONTROL was specified, so there's at least one item
   1359                     // in this list.
   1360                     .token = accelerator.type_and_options[0],
   1361                     .token_span_end = accelerator.type_and_options[accelerator.type_and_options.len - 1],
   1362                 });
   1363             }
   1364             if (accelerator.event.isNumberExpression() and !modifiers.explicit_ascii_or_virtkey) {
   1365                 return self.addErrorDetailsAndFail(.{
   1366                     .err = .accelerator_type_required,
   1367                     .token = accelerator.event.getFirstToken(),
   1368                     .token_span_end = accelerator.event.getLastToken(),
   1369                 });
   1370             }
   1371             const key = self.evaluateAcceleratorKeyExpression(accelerator.event, modifiers.isSet(.virtkey)) catch |err| switch (err) {
   1372                 error.OutOfMemory => |e| return e,
   1373                 else => |e| {
   1374                     return self.addErrorDetailsAndFail(.{
   1375                         .err = .invalid_accelerator_key,
   1376                         .token = accelerator.event.getFirstToken(),
   1377                         .token_span_end = accelerator.event.getLastToken(),
   1378                         .extra = .{ .accelerator_error = .{
   1379                             .err = ErrorDetails.AcceleratorError.enumFromError(e),
   1380                         } },
   1381                     });
   1382                 },
   1383             };
   1384             const cmd_id = evaluateNumberExpression(accelerator.idvalue, self.source, self.input_code_pages);
   1385 
   1386             if (i == node.accelerators.len - 1) {
   1387                 modifiers.markLast();
   1388             }
   1389 
   1390             try data_writer.writeByte(modifiers.value);
   1391             try data_writer.writeByte(0); // padding
   1392             try data_writer.writeInt(u16, key, .little);
   1393             try data_writer.writeInt(u16, cmd_id.asWord(), .little);
   1394             try data_writer.writeInt(u16, 0, .little); // padding
   1395         }
   1396     }
   1397 
   1398     const DialogOptionalStatementValues = struct {
   1399         style: u32 = res.WS.SYSMENU | res.WS.BORDER | res.WS.POPUP,
   1400         exstyle: u32 = 0,
   1401         class: ?NameOrOrdinal = null,
   1402         menu: ?NameOrOrdinal = null,
   1403         font: ?FontStatementValues = null,
   1404         caption: ?Token = null,
   1405     };
   1406 
   1407     pub fn writeDialog(self: *Compiler, node: *Node.Dialog, writer: anytype) !void {
   1408         var data_buffer = std.array_list.Managed(u8).init(self.allocator);
   1409         defer data_buffer.deinit();
   1410         // The header's data length field is a u32 so limit the resource's data size so that
   1411         // we know we can always specify the real size.
   1412         var limited_writer = limitedWriter(data_buffer.writer(), std.math.maxInt(u32));
   1413         const data_writer = limited_writer.writer();
   1414 
   1415         const resource = ResourceType.fromString(.{
   1416             .slice = node.type.slice(self.source),
   1417             .code_page = self.input_code_pages.getForToken(node.type),
   1418         });
   1419         std.debug.assert(resource == .dialog or resource == .dialogex);
   1420 
   1421         var optional_statement_values: DialogOptionalStatementValues = .{};
   1422         defer {
   1423             if (optional_statement_values.class) |class| {
   1424                 class.deinit(self.allocator);
   1425             }
   1426             if (optional_statement_values.menu) |menu| {
   1427                 menu.deinit(self.allocator);
   1428             }
   1429         }
   1430         var last_menu: *Node.SimpleStatement = undefined;
   1431         var last_class: *Node.SimpleStatement = undefined;
   1432         var last_menu_would_be_forced_ordinal = false;
   1433         var last_menu_has_digit_as_first_char = false;
   1434         var last_menu_did_uppercase = false;
   1435         var last_class_would_be_forced_ordinal = false;
   1436 
   1437         for (node.optional_statements) |optional_statement| {
   1438             switch (optional_statement.id) {
   1439                 .simple_statement => {
   1440                     const simple_statement: *Node.SimpleStatement = @alignCast(@fieldParentPtr("base", optional_statement));
   1441                     const statement_identifier = simple_statement.identifier;
   1442                     const statement_type = rc.OptionalStatements.dialog_map.get(statement_identifier.slice(self.source)) orelse continue;
   1443                     switch (statement_type) {
   1444                         .style, .exstyle => {
   1445                             const style = evaluateFlagsExpressionWithDefault(0, simple_statement.value, self.source, self.input_code_pages);
   1446                             if (statement_type == .style) {
   1447                                 optional_statement_values.style = style;
   1448                             } else {
   1449                                 optional_statement_values.exstyle = style;
   1450                             }
   1451                         },
   1452                         .caption => {
   1453                             std.debug.assert(simple_statement.value.id == .literal);
   1454                             const literal_node: *Node.Literal = @alignCast(@fieldParentPtr("base", simple_statement.value));
   1455                             optional_statement_values.caption = literal_node.token;
   1456                         },
   1457                         .class => {
   1458                             const is_duplicate = optional_statement_values.class != null;
   1459                             const forced_ordinal = is_duplicate and optional_statement_values.class.? == .ordinal;
   1460                             // In the Win32 RC compiler, if any CLASS values that are interpreted as
   1461                             // an ordinal exist, it affects all future CLASS statements and forces
   1462                             // them to be treated as an ordinal no matter what.
   1463                             if (forced_ordinal) {
   1464                                 last_class_would_be_forced_ordinal = true;
   1465                             }
   1466                             // clear out the old one if it exists
   1467                             if (optional_statement_values.class) |prev| {
   1468                                 prev.deinit(self.allocator);
   1469                                 optional_statement_values.class = null;
   1470                             }
   1471 
   1472                             if (simple_statement.value.isNumberExpression()) {
   1473                                 const class_ordinal = evaluateNumberExpression(simple_statement.value, self.source, self.input_code_pages);
   1474                                 optional_statement_values.class = NameOrOrdinal{ .ordinal = class_ordinal.asWord() };
   1475                             } else {
   1476                                 std.debug.assert(simple_statement.value.isStringLiteral());
   1477                                 const literal_node: *Node.Literal = @alignCast(@fieldParentPtr("base", simple_statement.value));
   1478                                 const parsed = try self.parseQuotedStringAsWideString(literal_node.token);
   1479                                 optional_statement_values.class = NameOrOrdinal{ .name = parsed };
   1480                             }
   1481 
   1482                             last_class = simple_statement;
   1483                         },
   1484                         .menu => {
   1485                             const is_duplicate = optional_statement_values.menu != null;
   1486                             const forced_ordinal = is_duplicate and optional_statement_values.menu.? == .ordinal;
   1487                             // In the Win32 RC compiler, if any MENU values that are interpreted as
   1488                             // an ordinal exist, it affects all future MENU statements and forces
   1489                             // them to be treated as an ordinal no matter what.
   1490                             if (forced_ordinal) {
   1491                                 last_menu_would_be_forced_ordinal = true;
   1492                             }
   1493                             // clear out the old one if it exists
   1494                             if (optional_statement_values.menu) |prev| {
   1495                                 prev.deinit(self.allocator);
   1496                                 optional_statement_values.menu = null;
   1497                             }
   1498 
   1499                             std.debug.assert(simple_statement.value.id == .literal);
   1500                             const literal_node: *Node.Literal = @alignCast(@fieldParentPtr("base", simple_statement.value));
   1501 
   1502                             const token_slice = literal_node.token.slice(self.source);
   1503                             const bytes = SourceBytes{
   1504                                 .slice = token_slice,
   1505                                 .code_page = self.input_code_pages.getForToken(literal_node.token),
   1506                             };
   1507                             optional_statement_values.menu = try NameOrOrdinal.fromString(self.allocator, bytes);
   1508 
   1509                             if (optional_statement_values.menu.? == .name) {
   1510                                 if (NameOrOrdinal.maybeNonAsciiOrdinalFromString(bytes)) |win32_rc_ordinal| {
   1511                                     try self.addErrorDetails(.{
   1512                                         .err = .invalid_digit_character_in_ordinal,
   1513                                         .type = .err,
   1514                                         .token = literal_node.token,
   1515                                     });
   1516                                     return self.addErrorDetailsAndFail(.{
   1517                                         .err = .win32_non_ascii_ordinal,
   1518                                         .type = .note,
   1519                                         .token = literal_node.token,
   1520                                         .print_source_line = false,
   1521                                         .extra = .{ .number = win32_rc_ordinal.ordinal },
   1522                                     });
   1523                                 }
   1524                             }
   1525 
   1526                             // Need to keep track of some properties of the value
   1527                             // in order to emit the appropriate warning(s) later on.
   1528                             // See where the warning are emitted below (outside this loop)
   1529                             // for the full explanation.
   1530                             var did_uppercase = false;
   1531                             var codepoint_i: usize = 0;
   1532                             while (bytes.code_page.codepointAt(codepoint_i, bytes.slice)) |codepoint| : (codepoint_i += codepoint.byte_len) {
   1533                                 const c = codepoint.value;
   1534                                 switch (c) {
   1535                                     'a'...'z' => {
   1536                                         did_uppercase = true;
   1537                                         break;
   1538                                     },
   1539                                     else => {},
   1540                                 }
   1541                             }
   1542                             last_menu_did_uppercase = did_uppercase;
   1543                             last_menu_has_digit_as_first_char = std.ascii.isDigit(token_slice[0]);
   1544                             last_menu = simple_statement;
   1545                         },
   1546                         else => {},
   1547                     }
   1548                 },
   1549                 .font_statement => {
   1550                     const font: *Node.FontStatement = @alignCast(@fieldParentPtr("base", optional_statement));
   1551                     if (optional_statement_values.font != null) {
   1552                         optional_statement_values.font.?.node = font;
   1553                     } else {
   1554                         optional_statement_values.font = FontStatementValues{ .node = font };
   1555                     }
   1556                     if (font.weight) |weight| {
   1557                         const value = evaluateNumberExpression(weight, self.source, self.input_code_pages);
   1558                         optional_statement_values.font.?.weight = value.asWord();
   1559                     }
   1560                     if (font.italic) |italic| {
   1561                         const value = evaluateNumberExpression(italic, self.source, self.input_code_pages);
   1562                         optional_statement_values.font.?.italic = value.asWord() != 0;
   1563                     }
   1564                 },
   1565                 else => {},
   1566             }
   1567         }
   1568 
   1569         // The Win32 RC compiler miscompiles the value in the following scenario:
   1570         // Multiple CLASS parameters are specified and any of them are treated as a number, then
   1571         // the last CLASS is always treated as a number no matter what
   1572         if (last_class_would_be_forced_ordinal and optional_statement_values.class.? == .name) {
   1573             const literal_node: *Node.Literal = @alignCast(@fieldParentPtr("base", last_class.value));
   1574             const ordinal_value = res.ForcedOrdinal.fromUtf16Le(optional_statement_values.class.?.name);
   1575 
   1576             try self.addErrorDetails(.{
   1577                 .err = .rc_would_miscompile_dialog_class,
   1578                 .type = .warning,
   1579                 .token = literal_node.token,
   1580                 .extra = .{ .number = ordinal_value },
   1581             });
   1582             try self.addErrorDetails(.{
   1583                 .err = .rc_would_miscompile_dialog_class,
   1584                 .type = .note,
   1585                 .print_source_line = false,
   1586                 .token = literal_node.token,
   1587                 .extra = .{ .number = ordinal_value },
   1588             });
   1589             try self.addErrorDetails(.{
   1590                 .err = .rc_would_miscompile_dialog_menu_or_class_id_forced_ordinal,
   1591                 .type = .note,
   1592                 .print_source_line = false,
   1593                 .token = literal_node.token,
   1594                 .extra = .{ .menu_or_class = .class },
   1595             });
   1596         }
   1597         // The Win32 RC compiler miscompiles the id in two different scenarios:
   1598         // 1. The first character of the ID is a digit, in which case it is always treated as a number
   1599         //    no matter what (and therefore does not match how the MENU/MENUEX id is parsed)
   1600         // 2. Multiple MENU parameters are specified and any of them are treated as a number, then
   1601         //    the last MENU is always treated as a number no matter what
   1602         if ((last_menu_would_be_forced_ordinal or last_menu_has_digit_as_first_char) and optional_statement_values.menu.? == .name) {
   1603             const literal_node: *Node.Literal = @alignCast(@fieldParentPtr("base", last_menu.value));
   1604             const token_slice = literal_node.token.slice(self.source);
   1605             const bytes = SourceBytes{
   1606                 .slice = token_slice,
   1607                 .code_page = self.input_code_pages.getForToken(literal_node.token),
   1608             };
   1609             const ordinal_value = res.ForcedOrdinal.fromBytes(bytes);
   1610 
   1611             try self.addErrorDetails(.{
   1612                 .err = .rc_would_miscompile_dialog_menu_id,
   1613                 .type = .warning,
   1614                 .token = literal_node.token,
   1615                 .extra = .{ .number = ordinal_value },
   1616             });
   1617             try self.addErrorDetails(.{
   1618                 .err = .rc_would_miscompile_dialog_menu_id,
   1619                 .type = .note,
   1620                 .print_source_line = false,
   1621                 .token = literal_node.token,
   1622                 .extra = .{ .number = ordinal_value },
   1623             });
   1624             if (last_menu_would_be_forced_ordinal) {
   1625                 try self.addErrorDetails(.{
   1626                     .err = .rc_would_miscompile_dialog_menu_or_class_id_forced_ordinal,
   1627                     .type = .note,
   1628                     .print_source_line = false,
   1629                     .token = literal_node.token,
   1630                     .extra = .{ .menu_or_class = .menu },
   1631                 });
   1632             } else {
   1633                 try self.addErrorDetails(.{
   1634                     .err = .rc_would_miscompile_dialog_menu_id_starts_with_digit,
   1635                     .type = .note,
   1636                     .print_source_line = false,
   1637                     .token = literal_node.token,
   1638                 });
   1639             }
   1640         }
   1641         // The MENU id parsing uses the exact same logic as the MENU/MENUEX resource id parsing,
   1642         // which means that it will convert ASCII characters to uppercase during the 'name' parsing.
   1643         // This turns out not to matter (`LoadMenu` does a case-insensitive lookup anyway),
   1644         // but it still makes sense to share the uppercasing logic since the MENU parameter
   1645         // here is just a reference to a MENU/MENUEX id within the .exe.
   1646         // So, because this is an intentional but inconsequential-to-the-user difference
   1647         // between resinator and the Win32 RC compiler, we only emit a hint instead of
   1648         // a warning.
   1649         if (last_menu_did_uppercase) {
   1650             const literal_node: *Node.Literal = @alignCast(@fieldParentPtr("base", last_menu.value));
   1651             try self.addErrorDetails(.{
   1652                 .err = .dialog_menu_id_was_uppercased,
   1653                 .type = .hint,
   1654                 .token = literal_node.token,
   1655             });
   1656         }
   1657 
   1658         const x = evaluateNumberExpression(node.x, self.source, self.input_code_pages);
   1659         const y = evaluateNumberExpression(node.y, self.source, self.input_code_pages);
   1660         const width = evaluateNumberExpression(node.width, self.source, self.input_code_pages);
   1661         const height = evaluateNumberExpression(node.height, self.source, self.input_code_pages);
   1662 
   1663         // FONT statement requires DS_SETFONT, and if it's not present DS_SETFRONT must be unset
   1664         if (optional_statement_values.font) |_| {
   1665             optional_statement_values.style |= res.DS.SETFONT;
   1666         } else {
   1667             optional_statement_values.style &= ~res.DS.SETFONT;
   1668         }
   1669         // CAPTION statement implies WS_CAPTION
   1670         if (optional_statement_values.caption) |_| {
   1671             optional_statement_values.style |= res.WS.CAPTION;
   1672         }
   1673 
   1674         self.writeDialogHeaderAndStrings(
   1675             node,
   1676             data_writer,
   1677             resource,
   1678             &optional_statement_values,
   1679             x,
   1680             y,
   1681             width,
   1682             height,
   1683         ) catch |err| switch (err) {
   1684             // Dialog header and menu/class/title strings can never exceed u32 bytes
   1685             // on their own, so this error is unreachable.
   1686             error.NoSpaceLeft => unreachable,
   1687             else => |e| return e,
   1688         };
   1689 
   1690         var controls_by_id = std.AutoHashMap(u32, *const Node.ControlStatement).init(self.allocator);
   1691         // Number of controls are guaranteed by the parser to be within maxInt(u16).
   1692         try controls_by_id.ensureTotalCapacity(@as(u16, @intCast(node.controls.len)));
   1693         defer controls_by_id.deinit();
   1694 
   1695         for (node.controls) |control_node| {
   1696             const control: *Node.ControlStatement = @alignCast(@fieldParentPtr("base", control_node));
   1697 
   1698             self.writeDialogControl(
   1699                 control,
   1700                 data_writer,
   1701                 resource,
   1702                 // We know the data_buffer len is limited to u32 max.
   1703                 @intCast(data_buffer.items.len),
   1704                 &controls_by_id,
   1705             ) catch |err| switch (err) {
   1706                 error.NoSpaceLeft => {
   1707                     try self.addErrorDetails(.{
   1708                         .err = .resource_data_size_exceeds_max,
   1709                         .token = node.id,
   1710                     });
   1711                     return self.addErrorDetailsAndFail(.{
   1712                         .err = .resource_data_size_exceeds_max,
   1713                         .type = .note,
   1714                         .token = control.type,
   1715                     });
   1716                 },
   1717                 else => |e| return e,
   1718             };
   1719         }
   1720 
   1721         // We know the data_buffer len is limited to u32 max.
   1722         const data_size: u32 = @intCast(data_buffer.items.len);
   1723         var header = try self.resourceHeader(node.id, node.type, .{
   1724             .data_size = data_size,
   1725         });
   1726         defer header.deinit(self.allocator);
   1727 
   1728         header.applyMemoryFlags(node.common_resource_attributes, self.source);
   1729         header.applyOptionalStatements(node.optional_statements, self.source, self.input_code_pages);
   1730 
   1731         try header.write(writer, self.errContext(node.id));
   1732 
   1733         var data_fbs: std.Io.Reader = .fixed(data_buffer.items);
   1734         try writeResourceData(writer, &data_fbs, data_size);
   1735     }
   1736 
   1737     fn writeDialogHeaderAndStrings(
   1738         self: *Compiler,
   1739         node: *Node.Dialog,
   1740         data_writer: anytype,
   1741         resource: ResourceType,
   1742         optional_statement_values: *const DialogOptionalStatementValues,
   1743         x: Number,
   1744         y: Number,
   1745         width: Number,
   1746         height: Number,
   1747     ) !void {
   1748         // Header
   1749         if (resource == .dialogex) {
   1750             const help_id: u32 = help_id: {
   1751                 if (node.help_id == null) break :help_id 0;
   1752                 break :help_id evaluateNumberExpression(node.help_id.?, self.source, self.input_code_pages).value;
   1753             };
   1754             try data_writer.writeInt(u16, 1, .little); // version number, always 1
   1755             try data_writer.writeInt(u16, 0xFFFF, .little); // signature, always 0xFFFF
   1756             try data_writer.writeInt(u32, help_id, .little);
   1757             try data_writer.writeInt(u32, optional_statement_values.exstyle, .little);
   1758             try data_writer.writeInt(u32, optional_statement_values.style, .little);
   1759         } else {
   1760             try data_writer.writeInt(u32, optional_statement_values.style, .little);
   1761             try data_writer.writeInt(u32, optional_statement_values.exstyle, .little);
   1762         }
   1763         // This limit is enforced by the parser, so we know the number of controls
   1764         // is within the range of a u16.
   1765         try data_writer.writeInt(u16, @as(u16, @intCast(node.controls.len)), .little);
   1766         try data_writer.writeInt(u16, x.asWord(), .little);
   1767         try data_writer.writeInt(u16, y.asWord(), .little);
   1768         try data_writer.writeInt(u16, width.asWord(), .little);
   1769         try data_writer.writeInt(u16, height.asWord(), .little);
   1770 
   1771         // Menu
   1772         if (optional_statement_values.menu) |menu| {
   1773             try menu.write(data_writer);
   1774         } else {
   1775             try data_writer.writeInt(u16, 0, .little);
   1776         }
   1777         // Class
   1778         if (optional_statement_values.class) |class| {
   1779             try class.write(data_writer);
   1780         } else {
   1781             try data_writer.writeInt(u16, 0, .little);
   1782         }
   1783         // Caption
   1784         if (optional_statement_values.caption) |caption| {
   1785             const parsed = try self.parseQuotedStringAsWideString(caption);
   1786             defer self.allocator.free(parsed);
   1787             try data_writer.writeAll(std.mem.sliceAsBytes(parsed[0 .. parsed.len + 1]));
   1788         } else {
   1789             try data_writer.writeInt(u16, 0, .little);
   1790         }
   1791         // Font
   1792         if (optional_statement_values.font) |font| {
   1793             try self.writeDialogFont(resource, font, data_writer);
   1794         }
   1795     }
   1796 
   1797     fn writeDialogControl(
   1798         self: *Compiler,
   1799         control: *Node.ControlStatement,
   1800         data_writer: anytype,
   1801         resource: ResourceType,
   1802         bytes_written_so_far: u32,
   1803         controls_by_id: *std.AutoHashMap(u32, *const Node.ControlStatement),
   1804     ) !void {
   1805         const control_type = rc.Control.map.get(control.type.slice(self.source)).?;
   1806 
   1807         // Each control must be at a 4-byte boundary. However, the Windows RC
   1808         // compiler will miscompile controls if their extra data ends on an odd offset.
   1809         // We will avoid the miscompilation and emit a warning.
   1810         const num_padding = numPaddingBytesNeeded(bytes_written_so_far);
   1811         if (num_padding == 1 or num_padding == 3) {
   1812             try self.addErrorDetails(.{
   1813                 .err = .rc_would_miscompile_control_padding,
   1814                 .type = .warning,
   1815                 .token = control.type,
   1816             });
   1817             try self.addErrorDetails(.{
   1818                 .err = .rc_would_miscompile_control_padding,
   1819                 .type = .note,
   1820                 .print_source_line = false,
   1821                 .token = control.type,
   1822             });
   1823         }
   1824         try data_writer.writeByteNTimes(0, num_padding);
   1825 
   1826         const style = if (control.style) |style_expression|
   1827             // Certain styles are implied by the control type
   1828             evaluateFlagsExpressionWithDefault(res.ControlClass.getImpliedStyle(control_type), style_expression, self.source, self.input_code_pages)
   1829         else
   1830             res.ControlClass.getImpliedStyle(control_type);
   1831 
   1832         const exstyle = if (control.exstyle) |exstyle_expression|
   1833             evaluateFlagsExpressionWithDefault(0, exstyle_expression, self.source, self.input_code_pages)
   1834         else
   1835             0;
   1836 
   1837         switch (resource) {
   1838             .dialog => {
   1839                 // Note: Reverse order from DIALOGEX
   1840                 try data_writer.writeInt(u32, style, .little);
   1841                 try data_writer.writeInt(u32, exstyle, .little);
   1842             },
   1843             .dialogex => {
   1844                 const help_id: u32 = if (control.help_id) |help_id_expression|
   1845                     evaluateNumberExpression(help_id_expression, self.source, self.input_code_pages).value
   1846                 else
   1847                     0;
   1848                 try data_writer.writeInt(u32, help_id, .little);
   1849                 // Note: Reverse order from DIALOG
   1850                 try data_writer.writeInt(u32, exstyle, .little);
   1851                 try data_writer.writeInt(u32, style, .little);
   1852             },
   1853             else => unreachable,
   1854         }
   1855 
   1856         const control_x = evaluateNumberExpression(control.x, self.source, self.input_code_pages);
   1857         const control_y = evaluateNumberExpression(control.y, self.source, self.input_code_pages);
   1858         const control_width = evaluateNumberExpression(control.width, self.source, self.input_code_pages);
   1859         const control_height = evaluateNumberExpression(control.height, self.source, self.input_code_pages);
   1860 
   1861         try data_writer.writeInt(u16, control_x.asWord(), .little);
   1862         try data_writer.writeInt(u16, control_y.asWord(), .little);
   1863         try data_writer.writeInt(u16, control_width.asWord(), .little);
   1864         try data_writer.writeInt(u16, control_height.asWord(), .little);
   1865 
   1866         const control_id = evaluateNumberExpression(control.id, self.source, self.input_code_pages);
   1867         switch (resource) {
   1868             .dialog => try data_writer.writeInt(u16, control_id.asWord(), .little),
   1869             .dialogex => try data_writer.writeInt(u32, control_id.value, .little),
   1870             else => unreachable,
   1871         }
   1872 
   1873         const control_id_for_map: u32 = switch (resource) {
   1874             .dialog => control_id.asWord(),
   1875             .dialogex => control_id.value,
   1876             else => unreachable,
   1877         };
   1878         const result = controls_by_id.getOrPutAssumeCapacity(control_id_for_map);
   1879         if (result.found_existing) {
   1880             if (!self.silent_duplicate_control_ids) {
   1881                 try self.addErrorDetails(.{
   1882                     .err = .control_id_already_defined,
   1883                     .type = .warning,
   1884                     .token = control.id.getFirstToken(),
   1885                     .token_span_end = control.id.getLastToken(),
   1886                     .extra = .{ .number = control_id_for_map },
   1887                 });
   1888                 try self.addErrorDetails(.{
   1889                     .err = .control_id_already_defined,
   1890                     .type = .note,
   1891                     .token = result.value_ptr.*.id.getFirstToken(),
   1892                     .token_span_end = result.value_ptr.*.id.getLastToken(),
   1893                     .extra = .{ .number = control_id_for_map },
   1894                 });
   1895             }
   1896         } else {
   1897             result.value_ptr.* = control;
   1898         }
   1899 
   1900         if (res.ControlClass.fromControl(control_type)) |control_class| {
   1901             const ordinal = NameOrOrdinal{ .ordinal = @intFromEnum(control_class) };
   1902             try ordinal.write(data_writer);
   1903         } else {
   1904             const class_node = control.class.?;
   1905             if (class_node.isNumberExpression()) {
   1906                 const number = evaluateNumberExpression(class_node, self.source, self.input_code_pages);
   1907                 const ordinal = NameOrOrdinal{ .ordinal = number.asWord() };
   1908                 // This is different from how the Windows RC compiles ordinals here,
   1909                 // but I think that's a miscompilation/bug of the Windows implementation.
   1910                 // The Windows behavior is (where LSB = least significant byte):
   1911                 // - If the LSB is 0x00 => 0xFFFF0000
   1912                 // - If the LSB is < 0x80 => 0x000000<LSB>
   1913                 // - If the LSB is >= 0x80 => 0x0000FF<LSB>
   1914                 //
   1915                 // Because of this, we emit a warning about the potential miscompilation
   1916                 try self.addErrorDetails(.{
   1917                     .err = .rc_would_miscompile_control_class_ordinal,
   1918                     .type = .warning,
   1919                     .token = class_node.getFirstToken(),
   1920                     .token_span_end = class_node.getLastToken(),
   1921                 });
   1922                 try self.addErrorDetails(.{
   1923                     .err = .rc_would_miscompile_control_class_ordinal,
   1924                     .type = .note,
   1925                     .print_source_line = false,
   1926                     .token = class_node.getFirstToken(),
   1927                     .token_span_end = class_node.getLastToken(),
   1928                 });
   1929                 // And then write out the ordinal using a proper a NameOrOrdinal encoding.
   1930                 try ordinal.write(data_writer);
   1931             } else if (class_node.isStringLiteral()) {
   1932                 const literal_node: *Node.Literal = @alignCast(@fieldParentPtr("base", class_node));
   1933                 const parsed = try self.parseQuotedStringAsWideString(literal_node.token);
   1934                 defer self.allocator.free(parsed);
   1935                 if (rc.ControlClass.fromWideString(parsed)) |control_class| {
   1936                     const ordinal = NameOrOrdinal{ .ordinal = @intFromEnum(control_class) };
   1937                     try ordinal.write(data_writer);
   1938                 } else {
   1939                     // NUL acts as a terminator
   1940                     // TODO: Maybe warn when parsed_terminated.len != parsed.len, since
   1941                     //       it seems unlikely that NUL-termination is something intentional
   1942                     const parsed_terminated = std.mem.sliceTo(parsed, 0);
   1943                     const name = NameOrOrdinal{ .name = parsed_terminated };
   1944                     try name.write(data_writer);
   1945                 }
   1946             } else {
   1947                 const literal_node: *Node.Literal = @alignCast(@fieldParentPtr("base", class_node));
   1948                 const literal_slice = literal_node.token.slice(self.source);
   1949                 // This succeeding is guaranteed by the parser
   1950                 const control_class = rc.ControlClass.map.get(literal_slice) orelse unreachable;
   1951                 const ordinal = NameOrOrdinal{ .ordinal = @intFromEnum(control_class) };
   1952                 try ordinal.write(data_writer);
   1953             }
   1954         }
   1955 
   1956         if (control.text) |text_token| {
   1957             const bytes = SourceBytes{
   1958                 .slice = text_token.slice(self.source),
   1959                 .code_page = self.input_code_pages.getForToken(text_token),
   1960             };
   1961             if (text_token.isStringLiteral()) {
   1962                 const text = try self.parseQuotedStringAsWideString(text_token);
   1963                 defer self.allocator.free(text);
   1964                 const name = NameOrOrdinal{ .name = text };
   1965                 try name.write(data_writer);
   1966             } else {
   1967                 std.debug.assert(text_token.id == .number);
   1968                 const number = literals.parseNumberLiteral(bytes);
   1969                 const ordinal = NameOrOrdinal{ .ordinal = number.asWord() };
   1970                 try ordinal.write(data_writer);
   1971             }
   1972         } else {
   1973             try NameOrOrdinal.writeEmpty(data_writer);
   1974         }
   1975 
   1976         var extra_data_buf = std.array_list.Managed(u8).init(self.allocator);
   1977         defer extra_data_buf.deinit();
   1978         // The extra data byte length must be able to fit within a u16.
   1979         var limited_extra_data_writer = limitedWriter(extra_data_buf.writer(), std.math.maxInt(u16));
   1980         const extra_data_writer = limited_extra_data_writer.writer();
   1981         for (control.extra_data) |data_expression| {
   1982             const data = try self.evaluateDataExpression(data_expression);
   1983             defer data.deinit(self.allocator);
   1984             data.write(extra_data_writer) catch |err| switch (err) {
   1985                 error.NoSpaceLeft => {
   1986                     try self.addErrorDetails(.{
   1987                         .err = .control_extra_data_size_exceeds_max,
   1988                         .token = control.type,
   1989                     });
   1990                     return self.addErrorDetailsAndFail(.{
   1991                         .err = .control_extra_data_size_exceeds_max,
   1992                         .type = .note,
   1993                         .token = data_expression.getFirstToken(),
   1994                         .token_span_end = data_expression.getLastToken(),
   1995                     });
   1996                 },
   1997                 else => |e| return e,
   1998             };
   1999         }
   2000         // We know the extra_data_buf size fits within a u16.
   2001         const extra_data_size: u16 = @intCast(extra_data_buf.items.len);
   2002         try data_writer.writeInt(u16, extra_data_size, .little);
   2003         try data_writer.writeAll(extra_data_buf.items);
   2004     }
   2005 
   2006     pub fn writeToolbar(self: *Compiler, node: *Node.Toolbar, writer: anytype) !void {
   2007         var data_buffer = std.array_list.Managed(u8).init(self.allocator);
   2008         defer data_buffer.deinit();
   2009         const data_writer = data_buffer.writer();
   2010 
   2011         const button_width = evaluateNumberExpression(node.button_width, self.source, self.input_code_pages);
   2012         const button_height = evaluateNumberExpression(node.button_height, self.source, self.input_code_pages);
   2013 
   2014         // I'm assuming this is some sort of version
   2015         // TODO: Try to find something mentioning this
   2016         try data_writer.writeInt(u16, 1, .little);
   2017         try data_writer.writeInt(u16, button_width.asWord(), .little);
   2018         try data_writer.writeInt(u16, button_height.asWord(), .little);
   2019         // Number of buttons is guaranteed by the parser to be within maxInt(u16).
   2020         try data_writer.writeInt(u16, @as(u16, @intCast(node.buttons.len)), .little);
   2021 
   2022         for (node.buttons) |button_or_sep| {
   2023             switch (button_or_sep.id) {
   2024                 .literal => { // This is always SEPARATOR
   2025                     std.debug.assert(button_or_sep.cast(.literal).?.token.id == .literal);
   2026                     try data_writer.writeInt(u16, 0, .little);
   2027                 },
   2028                 .simple_statement => {
   2029                     const value_node = button_or_sep.cast(.simple_statement).?.value;
   2030                     const value = evaluateNumberExpression(value_node, self.source, self.input_code_pages);
   2031                     try data_writer.writeInt(u16, value.asWord(), .little);
   2032                 },
   2033                 else => unreachable, // This is a bug in the parser
   2034             }
   2035         }
   2036 
   2037         const data_size: u32 = @intCast(data_buffer.items.len);
   2038         var header = try self.resourceHeader(node.id, node.type, .{
   2039             .data_size = data_size,
   2040         });
   2041         defer header.deinit(self.allocator);
   2042 
   2043         header.applyMemoryFlags(node.common_resource_attributes, self.source);
   2044 
   2045         try header.write(writer, self.errContext(node.id));
   2046 
   2047         var data_fbs: std.Io.Reader = .fixed(data_buffer.items);
   2048         try writeResourceData(writer, &data_fbs, data_size);
   2049     }
   2050 
   2051     /// Weight and italic carry over from previous FONT statements within a single resource,
   2052     /// so they need to be parsed ahead-of-time and stored
   2053     const FontStatementValues = struct {
   2054         weight: u16 = 0,
   2055         italic: bool = false,
   2056         node: *Node.FontStatement,
   2057     };
   2058 
   2059     pub fn writeDialogFont(self: *Compiler, resource: ResourceType, values: FontStatementValues, writer: anytype) !void {
   2060         const node = values.node;
   2061         const point_size = evaluateNumberExpression(node.point_size, self.source, self.input_code_pages);
   2062         try writer.writeInt(u16, point_size.asWord(), .little);
   2063 
   2064         if (resource == .dialogex) {
   2065             try writer.writeInt(u16, values.weight, .little);
   2066         }
   2067 
   2068         if (resource == .dialogex) {
   2069             try writer.writeInt(u8, @intFromBool(values.italic), .little);
   2070         }
   2071 
   2072         if (node.char_set) |char_set| {
   2073             const value = evaluateNumberExpression(char_set, self.source, self.input_code_pages);
   2074             try writer.writeInt(u8, @as(u8, @truncate(value.value)), .little);
   2075         } else if (resource == .dialogex) {
   2076             try writer.writeInt(u8, 1, .little); // DEFAULT_CHARSET
   2077         }
   2078 
   2079         const typeface = try self.parseQuotedStringAsWideString(node.typeface);
   2080         defer self.allocator.free(typeface);
   2081         try writer.writeAll(std.mem.sliceAsBytes(typeface[0 .. typeface.len + 1]));
   2082     }
   2083 
   2084     pub fn writeMenu(self: *Compiler, node: *Node.Menu, writer: anytype) !void {
   2085         var data_buffer = std.array_list.Managed(u8).init(self.allocator);
   2086         defer data_buffer.deinit();
   2087         // The header's data length field is a u32 so limit the resource's data size so that
   2088         // we know we can always specify the real size.
   2089         var limited_writer = limitedWriter(data_buffer.writer(), std.math.maxInt(u32));
   2090         const data_writer = limited_writer.writer();
   2091 
   2092         const type_bytes = SourceBytes{
   2093             .slice = node.type.slice(self.source),
   2094             .code_page = self.input_code_pages.getForToken(node.type),
   2095         };
   2096         const resource = ResourceType.fromString(type_bytes);
   2097         std.debug.assert(resource == .menu or resource == .menuex);
   2098 
   2099         var adapted = data_writer.adaptToNewApi(&.{});
   2100 
   2101         self.writeMenuData(node, &adapted.new_interface, resource) catch |err| switch (err) {
   2102             error.WriteFailed => {
   2103                 return self.addErrorDetailsAndFail(.{
   2104                     .err = .resource_data_size_exceeds_max,
   2105                     .token = node.id,
   2106                 });
   2107             },
   2108             else => |e| return e,
   2109         };
   2110 
   2111         // This intCast can't fail because the limitedWriter above guarantees that
   2112         // we will never write more than maxInt(u32) bytes.
   2113         const data_size: u32 = @intCast(data_buffer.items.len);
   2114         var header = try self.resourceHeader(node.id, node.type, .{
   2115             .data_size = data_size,
   2116         });
   2117         defer header.deinit(self.allocator);
   2118 
   2119         header.applyMemoryFlags(node.common_resource_attributes, self.source);
   2120         header.applyOptionalStatements(node.optional_statements, self.source, self.input_code_pages);
   2121 
   2122         try header.write(writer, self.errContext(node.id));
   2123 
   2124         var data_fbs: std.Io.Reader = .fixed(data_buffer.items);
   2125         try writeResourceData(writer, &data_fbs, data_size);
   2126     }
   2127 
   2128     /// Expects `data_writer` to be a LimitedWriter limited to u32, meaning all writes to
   2129     /// the writer within this function could return error.NoSpaceLeft
   2130     pub fn writeMenuData(self: *Compiler, node: *Node.Menu, data_writer: *std.Io.Writer, resource: ResourceType) !void {
   2131         // menu header
   2132         const version: u16 = if (resource == .menu) 0 else 1;
   2133         try data_writer.writeInt(u16, version, .little);
   2134         const header_size: u16 = if (resource == .menu) 0 else 4;
   2135         try data_writer.writeInt(u16, header_size, .little); // cbHeaderSize
   2136         // Note: There can be extra bytes at the end of this header (`rgbExtra`),
   2137         //       but they are always zero-length for us, so we don't write anything
   2138         //       (the length of the rgbExtra field is inferred from the header_size).
   2139         // MENU   => rgbExtra: [cbHeaderSize]u8
   2140         // MENUEX => rgbExtra: [cbHeaderSize-4]u8
   2141 
   2142         if (resource == .menuex) {
   2143             if (node.help_id) |help_id_node| {
   2144                 const help_id = evaluateNumberExpression(help_id_node, self.source, self.input_code_pages);
   2145                 try data_writer.writeInt(u32, help_id.value, .little);
   2146             } else {
   2147                 try data_writer.writeInt(u32, 0, .little);
   2148             }
   2149         }
   2150 
   2151         for (node.items, 0..) |item, i| {
   2152             const is_last = i == node.items.len - 1;
   2153             try self.writeMenuItem(item, data_writer, is_last);
   2154         }
   2155     }
   2156 
   2157     pub fn writeMenuItem(self: *Compiler, node: *Node, writer: *std.Io.Writer, is_last_of_parent: bool) !void {
   2158         switch (node.id) {
   2159             .menu_item_separator => {
   2160                 // This is the 'alternate compability form' of the separator, see
   2161                 // https://devblogs.microsoft.com/oldnewthing/20080710-00/?p=21673
   2162                 //
   2163                 // The 'correct' way is to set the MF_SEPARATOR flag, but the Win32 RC
   2164                 // compiler still uses this alternate form, so that's what we use too.
   2165                 var flags = res.MenuItemFlags{};
   2166                 if (is_last_of_parent) flags.markLast();
   2167                 try writer.writeInt(u16, flags.value, .little);
   2168                 try writer.writeInt(u16, 0, .little); // id
   2169                 try writer.writeInt(u16, 0, .little); // null-terminated UTF-16 text
   2170             },
   2171             .menu_item => {
   2172                 const menu_item: *Node.MenuItem = @alignCast(@fieldParentPtr("base", node));
   2173                 var flags = res.MenuItemFlags{};
   2174                 for (menu_item.option_list) |option_token| {
   2175                     // This failing would be a bug in the parser
   2176                     const option = rc.MenuItem.Option.map.get(option_token.slice(self.source)) orelse unreachable;
   2177                     flags.apply(option);
   2178                 }
   2179                 if (is_last_of_parent) flags.markLast();
   2180                 try writer.writeInt(u16, flags.value, .little);
   2181 
   2182                 var result = evaluateNumberExpression(menu_item.result, self.source, self.input_code_pages);
   2183                 try writer.writeInt(u16, result.asWord(), .little);
   2184 
   2185                 var text = try self.parseQuotedStringAsWideString(menu_item.text);
   2186                 defer self.allocator.free(text);
   2187                 try writer.writeAll(std.mem.sliceAsBytes(text[0 .. text.len + 1]));
   2188             },
   2189             .popup => {
   2190                 const popup: *Node.Popup = @alignCast(@fieldParentPtr("base", node));
   2191                 var flags = res.MenuItemFlags{ .value = res.MF.POPUP };
   2192                 for (popup.option_list) |option_token| {
   2193                     // This failing would be a bug in the parser
   2194                     const option = rc.MenuItem.Option.map.get(option_token.slice(self.source)) orelse unreachable;
   2195                     flags.apply(option);
   2196                 }
   2197                 if (is_last_of_parent) flags.markLast();
   2198                 try writer.writeInt(u16, flags.value, .little);
   2199 
   2200                 var text = try self.parseQuotedStringAsWideString(popup.text);
   2201                 defer self.allocator.free(text);
   2202                 try writer.writeAll(std.mem.sliceAsBytes(text[0 .. text.len + 1]));
   2203 
   2204                 for (popup.items, 0..) |item, i| {
   2205                     const is_last = i == popup.items.len - 1;
   2206                     try self.writeMenuItem(item, writer, is_last);
   2207                 }
   2208             },
   2209             inline .menu_item_ex, .popup_ex => |node_type| {
   2210                 const menu_item: *node_type.Type() = @alignCast(@fieldParentPtr("base", node));
   2211 
   2212                 if (menu_item.type) |flags| {
   2213                     const value = evaluateNumberExpression(flags, self.source, self.input_code_pages);
   2214                     try writer.writeInt(u32, value.value, .little);
   2215                 } else {
   2216                     try writer.writeInt(u32, 0, .little);
   2217                 }
   2218 
   2219                 if (menu_item.state) |state| {
   2220                     const value = evaluateNumberExpression(state, self.source, self.input_code_pages);
   2221                     try writer.writeInt(u32, value.value, .little);
   2222                 } else {
   2223                     try writer.writeInt(u32, 0, .little);
   2224                 }
   2225 
   2226                 if (menu_item.id) |id| {
   2227                     const value = evaluateNumberExpression(id, self.source, self.input_code_pages);
   2228                     try writer.writeInt(u32, value.value, .little);
   2229                 } else {
   2230                     try writer.writeInt(u32, 0, .little);
   2231                 }
   2232 
   2233                 var flags: u16 = 0;
   2234                 if (is_last_of_parent) flags |= comptime @as(u16, @intCast(res.MF.END));
   2235                 // This constant doesn't seem to have a named #define, it's different than MF_POPUP
   2236                 if (node_type == .popup_ex) flags |= 0x01;
   2237                 try writer.writeInt(u16, flags, .little);
   2238 
   2239                 var text = try self.parseQuotedStringAsWideString(menu_item.text);
   2240                 defer self.allocator.free(text);
   2241                 try writer.writeAll(std.mem.sliceAsBytes(text[0 .. text.len + 1]));
   2242 
   2243                 // Only the combination of the flags u16 and the text bytes can cause
   2244                 // non-DWORD alignment, so we can just use the byte length of those
   2245                 // two values to realign to DWORD alignment.
   2246                 const relevant_bytes = 2 + (text.len + 1) * 2;
   2247                 try writeDataPadding(writer, @intCast(relevant_bytes));
   2248 
   2249                 if (node_type == .popup_ex) {
   2250                     if (menu_item.help_id) |help_id_node| {
   2251                         const help_id = evaluateNumberExpression(help_id_node, self.source, self.input_code_pages);
   2252                         try writer.writeInt(u32, help_id.value, .little);
   2253                     } else {
   2254                         try writer.writeInt(u32, 0, .little);
   2255                     }
   2256 
   2257                     for (menu_item.items, 0..) |item, i| {
   2258                         const is_last = i == menu_item.items.len - 1;
   2259                         try self.writeMenuItem(item, writer, is_last);
   2260                     }
   2261                 }
   2262             },
   2263             else => unreachable,
   2264         }
   2265     }
   2266 
   2267     pub fn writeVersionInfo(self: *Compiler, node: *Node.VersionInfo, writer: anytype) !void {
   2268         var data_buffer = std.array_list.Managed(u8).init(self.allocator);
   2269         defer data_buffer.deinit();
   2270         // The node's length field (which is inclusive of the length of all of its children) is a u16
   2271         // so limit the node's data size so that we know we can always specify the real size.
   2272         var limited_writer = limitedWriter(data_buffer.writer(), std.math.maxInt(u16));
   2273         const data_writer = limited_writer.writer();
   2274 
   2275         try data_writer.writeInt(u16, 0, .little); // placeholder size
   2276         try data_writer.writeInt(u16, res.FixedFileInfo.byte_len, .little);
   2277         try data_writer.writeInt(u16, res.VersionNode.type_binary, .little);
   2278         const key_bytes = std.mem.sliceAsBytes(res.FixedFileInfo.key[0 .. res.FixedFileInfo.key.len + 1]);
   2279         try data_writer.writeAll(key_bytes);
   2280         // The number of bytes written up to this point is always the same, since the name
   2281         // of the node is a constant (FixedFileInfo.key). The total number of bytes
   2282         // written so far is 38, so we need 2 padding bytes to get back to DWORD alignment
   2283         try data_writer.writeInt(u16, 0, .little);
   2284 
   2285         var fixed_file_info = res.FixedFileInfo{};
   2286         for (node.fixed_info) |fixed_info| {
   2287             switch (fixed_info.id) {
   2288                 .version_statement => {
   2289                     const version_statement: *Node.VersionStatement = @alignCast(@fieldParentPtr("base", fixed_info));
   2290                     const version_type = rc.VersionInfo.map.get(version_statement.type.slice(self.source)).?;
   2291 
   2292                     // Ensure that all parts are cleared for each version, to properly account for
   2293                     // potential duplicate PRODUCTVERSION/FILEVERSION statements
   2294                     switch (version_type) {
   2295                         .file_version => @memset(&fixed_file_info.file_version.parts, 0),
   2296                         .product_version => @memset(&fixed_file_info.product_version.parts, 0),
   2297                         else => unreachable,
   2298                     }
   2299 
   2300                     for (version_statement.parts, 0..) |part, i| {
   2301                         const part_value = evaluateNumberExpression(part, self.source, self.input_code_pages);
   2302                         if (part_value.is_long) {
   2303                             try self.addErrorDetails(.{
   2304                                 .err = .rc_would_error_u16_with_l_suffix,
   2305                                 .type = .warning,
   2306                                 .token = part.getFirstToken(),
   2307                                 .token_span_end = part.getLastToken(),
   2308                                 .extra = .{ .statement_with_u16_param = switch (version_type) {
   2309                                     .file_version => .fileversion,
   2310                                     .product_version => .productversion,
   2311                                     else => unreachable,
   2312                                 } },
   2313                             });
   2314                             try self.addErrorDetails(.{
   2315                                 .err = .rc_would_error_u16_with_l_suffix,
   2316                                 .print_source_line = false,
   2317                                 .type = .note,
   2318                                 .token = part.getFirstToken(),
   2319                                 .token_span_end = part.getLastToken(),
   2320                                 .extra = .{ .statement_with_u16_param = switch (version_type) {
   2321                                     .file_version => .fileversion,
   2322                                     .product_version => .productversion,
   2323                                     else => unreachable,
   2324                                 } },
   2325                             });
   2326                         }
   2327                         switch (version_type) {
   2328                             .file_version => {
   2329                                 fixed_file_info.file_version.parts[i] = part_value.asWord();
   2330                             },
   2331                             .product_version => {
   2332                                 fixed_file_info.product_version.parts[i] = part_value.asWord();
   2333                             },
   2334                             else => unreachable,
   2335                         }
   2336                     }
   2337                 },
   2338                 .simple_statement => {
   2339                     const statement: *Node.SimpleStatement = @alignCast(@fieldParentPtr("base", fixed_info));
   2340                     const statement_type = rc.VersionInfo.map.get(statement.identifier.slice(self.source)).?;
   2341                     const value = evaluateNumberExpression(statement.value, self.source, self.input_code_pages);
   2342                     switch (statement_type) {
   2343                         .file_flags_mask => fixed_file_info.file_flags_mask = value.value,
   2344                         .file_flags => fixed_file_info.file_flags = value.value,
   2345                         .file_os => fixed_file_info.file_os = value.value,
   2346                         .file_type => fixed_file_info.file_type = value.value,
   2347                         .file_subtype => fixed_file_info.file_subtype = value.value,
   2348                         else => unreachable,
   2349                     }
   2350                 },
   2351                 else => unreachable,
   2352             }
   2353         }
   2354         try fixed_file_info.write(data_writer);
   2355 
   2356         for (node.block_statements) |statement| {
   2357             var adapted = data_writer.adaptToNewApi(&.{});
   2358             self.writeVersionNode(statement, &adapted.new_interface, &data_buffer) catch |err| switch (err) {
   2359                 error.WriteFailed => {
   2360                     try self.addErrorDetails(.{
   2361                         .err = .version_node_size_exceeds_max,
   2362                         .token = node.id,
   2363                     });
   2364                     return self.addErrorDetailsAndFail(.{
   2365                         .err = .version_node_size_exceeds_max,
   2366                         .type = .note,
   2367                         .token = statement.getFirstToken(),
   2368                         .token_span_end = statement.getLastToken(),
   2369                     });
   2370                 },
   2371                 else => |e| return e,
   2372             };
   2373         }
   2374 
   2375         // We know that data_buffer.items.len is within the limits of a u16, since we
   2376         // limited the writer to maxInt(u16)
   2377         const data_size: u16 = @intCast(data_buffer.items.len);
   2378         // And now that we know the full size of this node (including its children), set its size
   2379         std.mem.writeInt(u16, data_buffer.items[0..2], data_size, .little);
   2380 
   2381         var header = try self.resourceHeader(node.id, node.versioninfo, .{
   2382             .data_size = data_size,
   2383         });
   2384         defer header.deinit(self.allocator);
   2385 
   2386         header.applyMemoryFlags(node.common_resource_attributes, self.source);
   2387 
   2388         try header.write(writer, self.errContext(node.id));
   2389 
   2390         var data_fbs: std.Io.Reader = .fixed(data_buffer.items);
   2391         try writeResourceData(writer, &data_fbs, data_size);
   2392     }
   2393 
   2394     /// Expects writer to be a LimitedWriter limited to u16, meaning all writes to
   2395     /// the writer within this function could return error.NoSpaceLeft, and that buf.items.len
   2396     /// will never be able to exceed maxInt(u16).
   2397     pub fn writeVersionNode(self: *Compiler, node: *Node, writer: *std.Io.Writer, buf: *std.array_list.Managed(u8)) !void {
   2398         // We can assume that buf.items.len will never be able to exceed the limits of a u16
   2399         try writeDataPadding(writer, @as(u16, @intCast(buf.items.len)));
   2400 
   2401         const node_and_children_size_offset = buf.items.len;
   2402         try writer.writeInt(u16, 0, .little); // placeholder for size
   2403         const data_size_offset = buf.items.len;
   2404         try writer.writeInt(u16, 0, .little); // placeholder for data size
   2405         const data_type_offset = buf.items.len;
   2406         // Data type is string unless the node contains values that are numbers.
   2407         try writer.writeInt(u16, res.VersionNode.type_string, .little);
   2408 
   2409         switch (node.id) {
   2410             inline .block, .block_value => |node_type| {
   2411                 const block_or_value: *node_type.Type() = @alignCast(@fieldParentPtr("base", node));
   2412                 const parsed_key = try self.parseQuotedStringAsWideString(block_or_value.key);
   2413                 defer self.allocator.free(parsed_key);
   2414 
   2415                 const parsed_key_to_first_null = std.mem.sliceTo(parsed_key, 0);
   2416                 try writer.writeAll(std.mem.sliceAsBytes(parsed_key_to_first_null[0 .. parsed_key_to_first_null.len + 1]));
   2417 
   2418                 var has_number_value: bool = false;
   2419                 for (block_or_value.values) |value_value_node_uncasted| {
   2420                     const value_value_node = value_value_node_uncasted.cast(.block_value_value).?;
   2421                     if (value_value_node.expression.isNumberExpression()) {
   2422                         has_number_value = true;
   2423                         break;
   2424                     }
   2425                 }
   2426                 // The units used here are dependent on the type. If there are any numbers, then
   2427                 // this is a byte count. If there are only strings, then this is a count of
   2428                 // UTF-16 code units.
   2429                 //
   2430                 // The Win32 RC compiler miscompiles this count in the case of values that
   2431                 // have a mix of numbers and strings. This is detected and a warning is emitted
   2432                 // during parsing, so we can just do the correct thing here.
   2433                 var values_size: usize = 0;
   2434 
   2435                 try writeDataPadding(writer, @intCast(buf.items.len));
   2436 
   2437                 for (block_or_value.values, 0..) |value_value_node_uncasted, i| {
   2438                     const value_value_node = value_value_node_uncasted.cast(.block_value_value).?;
   2439                     const value_node = value_value_node.expression;
   2440                     if (value_node.isNumberExpression()) {
   2441                         const number = evaluateNumberExpression(value_node, self.source, self.input_code_pages);
   2442                         // This is used to write u16 or u32 depending on the number's suffix
   2443                         const data_wrapper = Data{ .number = number };
   2444                         try data_wrapper.write(writer);
   2445                         // Numbers use byte count
   2446                         values_size += if (number.is_long) 4 else 2;
   2447                     } else {
   2448                         std.debug.assert(value_node.isStringLiteral());
   2449                         const literal_node = value_node.cast(.literal).?;
   2450                         const parsed_value = try self.parseQuotedStringAsWideString(literal_node.token);
   2451                         defer self.allocator.free(parsed_value);
   2452 
   2453                         const parsed_to_first_null = std.mem.sliceTo(parsed_value, 0);
   2454                         try writer.writeAll(std.mem.sliceAsBytes(parsed_to_first_null));
   2455                         // Strings use UTF-16 code-unit count including the null-terminator, but
   2456                         // only if there are no number values in the list.
   2457                         var value_size = parsed_to_first_null.len;
   2458                         if (has_number_value) value_size *= 2; // 2 bytes per UTF-16 code unit
   2459                         values_size += value_size;
   2460                         // The null-terminator is only included if there's a trailing comma
   2461                         // or this is the last value. If the value evaluates to empty, then
   2462                         // it never gets a null terminator. If there was an explicit null-terminator
   2463                         // in the string, we still need to potentially add one since we already
   2464                         // sliced to the terminator.
   2465                         const is_last = i == block_or_value.values.len - 1;
   2466                         const is_empty = parsed_to_first_null.len == 0;
   2467                         const is_only = block_or_value.values.len == 1;
   2468                         if ((!is_empty or !is_only) and (is_last or value_value_node.trailing_comma)) {
   2469                             try writer.writeInt(u16, 0, .little);
   2470                             values_size += if (has_number_value) 2 else 1;
   2471                         }
   2472                     }
   2473                 }
   2474                 var data_size_slice = buf.items[data_size_offset..];
   2475                 std.mem.writeInt(u16, data_size_slice[0..@sizeOf(u16)], @as(u16, @intCast(values_size)), .little);
   2476 
   2477                 if (has_number_value) {
   2478                     const data_type_slice = buf.items[data_type_offset..];
   2479                     std.mem.writeInt(u16, data_type_slice[0..@sizeOf(u16)], res.VersionNode.type_binary, .little);
   2480                 }
   2481 
   2482                 if (node_type == .block) {
   2483                     const block = block_or_value;
   2484                     for (block.children) |child| {
   2485                         try self.writeVersionNode(child, writer, buf);
   2486                     }
   2487                 }
   2488             },
   2489             else => unreachable,
   2490         }
   2491 
   2492         const node_and_children_size = buf.items.len - node_and_children_size_offset;
   2493         const node_and_children_size_slice = buf.items[node_and_children_size_offset..];
   2494         std.mem.writeInt(u16, node_and_children_size_slice[0..@sizeOf(u16)], @as(u16, @intCast(node_and_children_size)), .little);
   2495     }
   2496 
   2497     pub fn writeStringTable(self: *Compiler, node: *Node.StringTable) !void {
   2498         const language = getLanguageFromOptionalStatements(node.optional_statements, self.source, self.input_code_pages) orelse self.state.language;
   2499 
   2500         for (node.strings) |string_node| {
   2501             const string: *Node.StringTableString = @alignCast(@fieldParentPtr("base", string_node));
   2502             const string_id_data = try self.evaluateDataExpression(string.id);
   2503             const string_id = string_id_data.number.asWord();
   2504 
   2505             self.state.string_tables.set(
   2506                 self.arena,
   2507                 language,
   2508                 string_id,
   2509                 string.string,
   2510                 &node.base,
   2511                 self.source,
   2512                 self.input_code_pages,
   2513                 self.state.version,
   2514                 self.state.characteristics,
   2515             ) catch |err| switch (err) {
   2516                 error.StringAlreadyDefined => {
   2517                     // It might be nice to have these errors point to the ids rather than the
   2518                     // string tokens, but that would mean storing the id token of each string
   2519                     // which doesn't seem worth it just for slightly better error messages.
   2520                     try self.addErrorDetails(.{
   2521                         .err = .string_already_defined,
   2522                         .token = string.string,
   2523                         .extra = .{ .string_and_language = .{ .id = string_id, .language = language } },
   2524                     });
   2525                     const existing_def_table = self.state.string_tables.tables.getPtr(language).?;
   2526                     const existing_definition = existing_def_table.get(string_id).?;
   2527                     return self.addErrorDetailsAndFail(.{
   2528                         .err = .string_already_defined,
   2529                         .type = .note,
   2530                         .token = existing_definition,
   2531                         .extra = .{ .string_and_language = .{ .id = string_id, .language = language } },
   2532                     });
   2533                 },
   2534                 error.OutOfMemory => |e| return e,
   2535             };
   2536         }
   2537     }
   2538 
   2539     /// Expects this to be a top-level LANGUAGE statement
   2540     pub fn writeLanguageStatement(self: *Compiler, node: *Node.LanguageStatement) void {
   2541         const primary = Compiler.evaluateNumberExpression(node.primary_language_id, self.source, self.input_code_pages);
   2542         const sublanguage = Compiler.evaluateNumberExpression(node.sublanguage_id, self.source, self.input_code_pages);
   2543         self.state.language.primary_language_id = @truncate(primary.value);
   2544         self.state.language.sublanguage_id = @truncate(sublanguage.value);
   2545     }
   2546 
   2547     /// Expects this to be a top-level VERSION or CHARACTERISTICS statement
   2548     pub fn writeTopLevelSimpleStatement(self: *Compiler, node: *Node.SimpleStatement) void {
   2549         const value = Compiler.evaluateNumberExpression(node.value, self.source, self.input_code_pages);
   2550         const statement_type = rc.TopLevelKeywords.map.get(node.identifier.slice(self.source)).?;
   2551         switch (statement_type) {
   2552             .characteristics => self.state.characteristics = value.value,
   2553             .version => self.state.version = value.value,
   2554             else => unreachable,
   2555         }
   2556     }
   2557 
   2558     pub const ResourceHeaderOptions = struct {
   2559         language: ?res.Language = null,
   2560         data_size: DWORD = 0,
   2561     };
   2562 
   2563     pub fn resourceHeader(self: *Compiler, id_token: Token, type_token: Token, options: ResourceHeaderOptions) !ResourceHeader {
   2564         const id_bytes = self.sourceBytesForToken(id_token);
   2565         const type_bytes = self.sourceBytesForToken(type_token);
   2566         return ResourceHeader.init(
   2567             self.allocator,
   2568             id_bytes,
   2569             type_bytes,
   2570             options.data_size,
   2571             options.language orelse self.state.language,
   2572             self.state.version,
   2573             self.state.characteristics,
   2574         ) catch |err| switch (err) {
   2575             error.OutOfMemory => |e| return e,
   2576             error.TypeNonAsciiOrdinal => {
   2577                 const win32_rc_ordinal = NameOrOrdinal.maybeNonAsciiOrdinalFromString(type_bytes).?;
   2578                 try self.addErrorDetails(.{
   2579                     .err = .invalid_digit_character_in_ordinal,
   2580                     .type = .err,
   2581                     .token = type_token,
   2582                 });
   2583                 return self.addErrorDetailsAndFail(.{
   2584                     .err = .win32_non_ascii_ordinal,
   2585                     .type = .note,
   2586                     .token = type_token,
   2587                     .print_source_line = false,
   2588                     .extra = .{ .number = win32_rc_ordinal.ordinal },
   2589                 });
   2590             },
   2591             error.IdNonAsciiOrdinal => {
   2592                 const win32_rc_ordinal = NameOrOrdinal.maybeNonAsciiOrdinalFromString(id_bytes).?;
   2593                 try self.addErrorDetails(.{
   2594                     .err = .invalid_digit_character_in_ordinal,
   2595                     .type = .err,
   2596                     .token = id_token,
   2597                 });
   2598                 return self.addErrorDetailsAndFail(.{
   2599                     .err = .win32_non_ascii_ordinal,
   2600                     .type = .note,
   2601                     .token = id_token,
   2602                     .print_source_line = false,
   2603                     .extra = .{ .number = win32_rc_ordinal.ordinal },
   2604                 });
   2605             },
   2606         };
   2607     }
   2608 
   2609     pub const ResourceHeader = struct {
   2610         name_value: NameOrOrdinal,
   2611         type_value: NameOrOrdinal,
   2612         language: res.Language,
   2613         memory_flags: MemoryFlags,
   2614         data_size: DWORD,
   2615         version: DWORD,
   2616         characteristics: DWORD,
   2617         data_version: DWORD = 0,
   2618 
   2619         pub const InitError = error{ OutOfMemory, IdNonAsciiOrdinal, TypeNonAsciiOrdinal };
   2620 
   2621         pub fn init(allocator: Allocator, id_bytes: SourceBytes, type_bytes: SourceBytes, data_size: DWORD, language: res.Language, version: DWORD, characteristics: DWORD) InitError!ResourceHeader {
   2622             const type_value = type: {
   2623                 const resource_type = ResourceType.fromString(type_bytes);
   2624                 if (res.RT.fromResource(resource_type)) |rt_constant| {
   2625                     break :type NameOrOrdinal{ .ordinal = @intFromEnum(rt_constant) };
   2626                 } else {
   2627                     break :type try NameOrOrdinal.fromString(allocator, type_bytes);
   2628                 }
   2629             };
   2630             errdefer type_value.deinit(allocator);
   2631             if (type_value == .name) {
   2632                 if (NameOrOrdinal.maybeNonAsciiOrdinalFromString(type_bytes)) |_| {
   2633                     return error.TypeNonAsciiOrdinal;
   2634                 }
   2635             }
   2636 
   2637             const name_value = try NameOrOrdinal.fromString(allocator, id_bytes);
   2638             errdefer name_value.deinit(allocator);
   2639             if (name_value == .name) {
   2640                 if (NameOrOrdinal.maybeNonAsciiOrdinalFromString(id_bytes)) |_| {
   2641                     return error.IdNonAsciiOrdinal;
   2642                 }
   2643             }
   2644 
   2645             const predefined_resource_type = type_value.predefinedResourceType();
   2646 
   2647             return ResourceHeader{
   2648                 .name_value = name_value,
   2649                 .type_value = type_value,
   2650                 .data_size = data_size,
   2651                 .memory_flags = MemoryFlags.defaults(predefined_resource_type),
   2652                 .language = language,
   2653                 .version = version,
   2654                 .characteristics = characteristics,
   2655             };
   2656         }
   2657 
   2658         pub fn deinit(self: ResourceHeader, allocator: Allocator) void {
   2659             self.name_value.deinit(allocator);
   2660             self.type_value.deinit(allocator);
   2661         }
   2662 
   2663         pub const SizeInfo = struct {
   2664             bytes: u32,
   2665             padding_after_name: u2,
   2666         };
   2667 
   2668         pub fn calcSize(self: ResourceHeader) error{Overflow}!SizeInfo {
   2669             var header_size: u32 = 8;
   2670             header_size = try std.math.add(
   2671                 u32,
   2672                 header_size,
   2673                 std.math.cast(u32, self.name_value.byteLen()) orelse return error.Overflow,
   2674             );
   2675             header_size = try std.math.add(
   2676                 u32,
   2677                 header_size,
   2678                 std.math.cast(u32, self.type_value.byteLen()) orelse return error.Overflow,
   2679             );
   2680             const padding_after_name = numPaddingBytesNeeded(header_size);
   2681             header_size = try std.math.add(u32, header_size, padding_after_name);
   2682             header_size = try std.math.add(u32, header_size, 16);
   2683             return .{ .bytes = header_size, .padding_after_name = padding_after_name };
   2684         }
   2685 
   2686         pub fn writeAssertNoOverflow(self: ResourceHeader, writer: anytype) !void {
   2687             return self.writeSizeInfo(writer, self.calcSize() catch unreachable);
   2688         }
   2689 
   2690         pub fn write(self: ResourceHeader, writer: anytype, err_ctx: errors.DiagnosticsContext) !void {
   2691             const size_info = self.calcSize() catch {
   2692                 try err_ctx.diagnostics.append(.{
   2693                     .err = .resource_data_size_exceeds_max,
   2694                     .code_page = err_ctx.code_page,
   2695                     .token = err_ctx.token,
   2696                 });
   2697                 return error.CompileError;
   2698             };
   2699             return self.writeSizeInfo(writer, size_info);
   2700         }
   2701 
   2702         pub fn writeSizeInfo(self: ResourceHeader, writer: *std.Io.Writer, size_info: SizeInfo) !void {
   2703             try writer.writeInt(DWORD, self.data_size, .little); // DataSize
   2704             try writer.writeInt(DWORD, size_info.bytes, .little); // HeaderSize
   2705             try self.type_value.write(writer); // TYPE
   2706             try self.name_value.write(writer); // NAME
   2707             try writer.splatByteAll(0, size_info.padding_after_name);
   2708 
   2709             try writer.writeInt(DWORD, self.data_version, .little); // DataVersion
   2710             try writer.writeInt(WORD, self.memory_flags.value, .little); // MemoryFlags
   2711             try writer.writeInt(WORD, self.language.asInt(), .little); // LanguageId
   2712             try writer.writeInt(DWORD, self.version, .little); // Version
   2713             try writer.writeInt(DWORD, self.characteristics, .little); // Characteristics
   2714         }
   2715 
   2716         pub fn predefinedResourceType(self: ResourceHeader) ?res.RT {
   2717             return self.type_value.predefinedResourceType();
   2718         }
   2719 
   2720         pub fn applyMemoryFlags(self: *ResourceHeader, tokens: []Token, source: []const u8) void {
   2721             applyToMemoryFlags(&self.memory_flags, tokens, source);
   2722         }
   2723 
   2724         pub fn applyOptionalStatements(self: *ResourceHeader, statements: []*Node, source: []const u8, code_page_lookup: *const CodePageLookup) void {
   2725             applyToOptionalStatements(&self.language, &self.version, &self.characteristics, statements, source, code_page_lookup);
   2726         }
   2727     };
   2728 
   2729     fn applyToMemoryFlags(flags: *MemoryFlags, tokens: []Token, source: []const u8) void {
   2730         for (tokens) |token| {
   2731             const attribute = rc.CommonResourceAttributes.map.get(token.slice(source)).?;
   2732             flags.set(attribute);
   2733         }
   2734     }
   2735 
   2736     /// RT_GROUP_ICON and RT_GROUP_CURSOR have their own special rules for memory flags
   2737     fn applyToGroupMemoryFlags(flags: *MemoryFlags, tokens: []Token, source: []const u8) void {
   2738         // There's probably a cleaner implementation of this, but this will result in the same
   2739         // flags as the Win32 RC compiler for all 986,410 K-permutations of memory flags
   2740         // for an ICON resource.
   2741         //
   2742         // This was arrived at by iterating over the permutations and creating a
   2743         // list where each line looks something like this:
   2744         // MOVEABLE PRELOAD -> 0x1050 (MOVEABLE|PRELOAD|DISCARDABLE)
   2745         //
   2746         // and then noticing a few things:
   2747 
   2748         // 1. Any permutation that does not have PRELOAD in it just uses the
   2749         //    default flags.
   2750         const initial_flags = flags.*;
   2751         var flags_set = std.enums.EnumSet(rc.CommonResourceAttributes).initEmpty();
   2752         for (tokens) |token| {
   2753             const attribute = rc.CommonResourceAttributes.map.get(token.slice(source)).?;
   2754             flags_set.insert(attribute);
   2755         }
   2756         if (!flags_set.contains(.preload)) return;
   2757 
   2758         // 2. Any permutation of flags where applying only the PRELOAD and LOADONCALL flags
   2759         //    results in no actual change by the end will just use the default flags.
   2760         //    For example, `PRELOAD LOADONCALL` will result in default flags, but
   2761         //    `LOADONCALL PRELOAD` will have PRELOAD set after they are both applied in order.
   2762         for (tokens) |token| {
   2763             const attribute = rc.CommonResourceAttributes.map.get(token.slice(source)).?;
   2764             switch (attribute) {
   2765                 .preload, .loadoncall => flags.set(attribute),
   2766                 else => {},
   2767             }
   2768         }
   2769         if (flags.value == initial_flags.value) return;
   2770 
   2771         // 3. If none of DISCARDABLE, SHARED, or PURE is specified, then PRELOAD
   2772         //    implies `flags &= ~SHARED` and LOADONCALL implies `flags |= SHARED`
   2773         const shared_set = comptime blk: {
   2774             var set = std.enums.EnumSet(rc.CommonResourceAttributes).initEmpty();
   2775             set.insert(.discardable);
   2776             set.insert(.shared);
   2777             set.insert(.pure);
   2778             break :blk set;
   2779         };
   2780         const discardable_shared_or_pure_specified = flags_set.intersectWith(shared_set).count() != 0;
   2781         for (tokens) |token| {
   2782             const attribute = rc.CommonResourceAttributes.map.get(token.slice(source)).?;
   2783             flags.setGroup(attribute, !discardable_shared_or_pure_specified);
   2784         }
   2785     }
   2786 
   2787     /// Only handles the 'base' optional statements that are shared between resource types.
   2788     fn applyToOptionalStatements(language: *res.Language, version: *u32, characteristics: *u32, statements: []*Node, source: []const u8, code_page_lookup: *const CodePageLookup) void {
   2789         for (statements) |node| switch (node.id) {
   2790             .language_statement => {
   2791                 const language_statement: *Node.LanguageStatement = @alignCast(@fieldParentPtr("base", node));
   2792                 language.* = languageFromLanguageStatement(language_statement, source, code_page_lookup);
   2793             },
   2794             .simple_statement => {
   2795                 const simple_statement: *Node.SimpleStatement = @alignCast(@fieldParentPtr("base", node));
   2796                 const statement_type = rc.OptionalStatements.map.get(simple_statement.identifier.slice(source)) orelse continue;
   2797                 const result = Compiler.evaluateNumberExpression(simple_statement.value, source, code_page_lookup);
   2798                 switch (statement_type) {
   2799                     .version => version.* = result.value,
   2800                     .characteristics => characteristics.* = result.value,
   2801                     else => unreachable, // only VERSION and CHARACTERISTICS should be in an optional statements list
   2802                 }
   2803             },
   2804             else => {},
   2805         };
   2806     }
   2807 
   2808     pub fn languageFromLanguageStatement(language_statement: *const Node.LanguageStatement, source: []const u8, code_page_lookup: *const CodePageLookup) res.Language {
   2809         const primary = Compiler.evaluateNumberExpression(language_statement.primary_language_id, source, code_page_lookup);
   2810         const sublanguage = Compiler.evaluateNumberExpression(language_statement.sublanguage_id, source, code_page_lookup);
   2811         return .{
   2812             .primary_language_id = @truncate(primary.value),
   2813             .sublanguage_id = @truncate(sublanguage.value),
   2814         };
   2815     }
   2816 
   2817     pub fn getLanguageFromOptionalStatements(statements: []*Node, source: []const u8, code_page_lookup: *const CodePageLookup) ?res.Language {
   2818         for (statements) |node| switch (node.id) {
   2819             .language_statement => {
   2820                 const language_statement: *Node.LanguageStatement = @alignCast(@fieldParentPtr("base", node));
   2821                 return languageFromLanguageStatement(language_statement, source, code_page_lookup);
   2822             },
   2823             else => continue,
   2824         };
   2825         return null;
   2826     }
   2827 
   2828     pub fn writeEmptyResource(writer: anytype) !void {
   2829         const header = ResourceHeader{
   2830             .name_value = .{ .ordinal = 0 },
   2831             .type_value = .{ .ordinal = 0 },
   2832             .language = .{
   2833                 .primary_language_id = 0,
   2834                 .sublanguage_id = 0,
   2835             },
   2836             .memory_flags = .{ .value = 0 },
   2837             .data_size = 0,
   2838             .version = 0,
   2839             .characteristics = 0,
   2840         };
   2841         try header.writeAssertNoOverflow(writer);
   2842     }
   2843 
   2844     pub fn sourceBytesForToken(self: *Compiler, token: Token) SourceBytes {
   2845         return .{
   2846             .slice = token.slice(self.source),
   2847             .code_page = self.input_code_pages.getForToken(token),
   2848         };
   2849     }
   2850 
   2851     /// Helper that calls parseQuotedStringAsWideString with the relevant context
   2852     /// Resulting slice is allocated by `self.allocator`.
   2853     pub fn parseQuotedStringAsWideString(self: *Compiler, token: Token) ![:0]u16 {
   2854         return literals.parseQuotedStringAsWideString(
   2855             self.allocator,
   2856             self.sourceBytesForToken(token),
   2857             .{
   2858                 .start_column = token.calculateColumn(self.source, 8, null),
   2859                 .diagnostics = self.errContext(token),
   2860                 .output_code_page = self.output_code_pages.getForToken(token),
   2861             },
   2862         );
   2863     }
   2864 
   2865     fn addErrorDetailsWithCodePage(self: *Compiler, details: ErrorDetails) Allocator.Error!void {
   2866         try self.diagnostics.append(details);
   2867     }
   2868 
   2869     /// Code page is looked up in input_code_pages using the token
   2870     fn addErrorDetails(self: *Compiler, details_without_code_page: errors.ErrorDetailsWithoutCodePage) Allocator.Error!void {
   2871         const details = ErrorDetails{
   2872             .err = details_without_code_page.err,
   2873             .code_page = self.input_code_pages.getForToken(details_without_code_page.token),
   2874             .token = details_without_code_page.token,
   2875             .token_span_start = details_without_code_page.token_span_start,
   2876             .token_span_end = details_without_code_page.token_span_end,
   2877             .type = details_without_code_page.type,
   2878             .print_source_line = details_without_code_page.print_source_line,
   2879             .extra = details_without_code_page.extra,
   2880         };
   2881         try self.addErrorDetailsWithCodePage(details);
   2882     }
   2883 
   2884     /// Code page is looked up in input_code_pages using the token
   2885     fn addErrorDetailsAndFail(self: *Compiler, details_without_code_page: errors.ErrorDetailsWithoutCodePage) error{ CompileError, OutOfMemory } {
   2886         try self.addErrorDetails(details_without_code_page);
   2887         return error.CompileError;
   2888     }
   2889 
   2890     fn errContext(self: *Compiler, token: Token) errors.DiagnosticsContext {
   2891         return .{
   2892             .diagnostics = self.diagnostics,
   2893             .token = token,
   2894             .code_page = self.input_code_pages.getForToken(token),
   2895         };
   2896     }
   2897 };
   2898 
   2899 pub const OpenSearchPathError = std.fs.Dir.OpenError;
   2900 
   2901 fn openSearchPathDir(dir: std.fs.Dir, path: []const u8) OpenSearchPathError!std.fs.Dir {
   2902     // Validate the search path to avoid possible unreachable on invalid paths,
   2903     // see https://github.com/ziglang/zig/issues/15607 for why this is currently necessary.
   2904     try validateSearchPath(path);
   2905     return dir.openDir(path, .{});
   2906 }
   2907 
   2908 /// Very crude attempt at validating a path. This is imperfect
   2909 /// and AFAIK it is effectively impossible to implement perfect path
   2910 /// validation, since it ultimately depends on the underlying filesystem.
   2911 /// Note that this function won't be necessary if/when
   2912 /// https://github.com/ziglang/zig/issues/15607
   2913 /// is accepted/implemented.
   2914 fn validateSearchPath(path: []const u8) error{BadPathName}!void {
   2915     switch (builtin.os.tag) {
   2916         .windows => {
   2917             // This will return error.BadPathName on non-Win32 namespaced paths
   2918             // (e.g. the NT \??\ prefix, the device \\.\ prefix, etc).
   2919             // Those path types are something of an unavoidable way to
   2920             // still hit unreachable during the openDir call.
   2921             var component_iterator = try std.fs.path.componentIterator(path);
   2922             while (component_iterator.next()) |component| {
   2923                 // https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file
   2924                 if (std.mem.indexOfAny(u8, component.name, "\x00<>:\"|?*") != null) return error.BadPathName;
   2925             }
   2926         },
   2927         else => {
   2928             if (std.mem.indexOfScalar(u8, path, 0) != null) return error.BadPathName;
   2929         },
   2930     }
   2931 }
   2932 
   2933 pub const SearchDir = struct {
   2934     dir: std.fs.Dir,
   2935     path: ?[]const u8,
   2936 
   2937     pub fn deinit(self: *SearchDir, allocator: Allocator) void {
   2938         self.dir.close();
   2939         if (self.path) |path| {
   2940             allocator.free(path);
   2941         }
   2942     }
   2943 };
   2944 
   2945 /// Slurps the first `size` bytes read into `slurped_header`
   2946 pub fn HeaderSlurpingReader(comptime size: usize, comptime ReaderType: anytype) type {
   2947     return struct {
   2948         child_reader: ReaderType,
   2949         bytes_read: usize = 0,
   2950         slurped_header: [size]u8 = [_]u8{0x00} ** size,
   2951 
   2952         pub const Error = ReaderType.Error;
   2953         pub const Reader = std.io.GenericReader(*@This(), Error, read);
   2954 
   2955         pub fn read(self: *@This(), buf: []u8) Error!usize {
   2956             const amt = try self.child_reader.read(buf);
   2957             if (self.bytes_read < size) {
   2958                 const bytes_to_add = @min(amt, size - self.bytes_read);
   2959                 const end_index = self.bytes_read + bytes_to_add;
   2960                 @memcpy(self.slurped_header[self.bytes_read..end_index], buf[0..bytes_to_add]);
   2961             }
   2962             self.bytes_read +|= amt;
   2963             return amt;
   2964         }
   2965 
   2966         pub fn reader(self: *@This()) Reader {
   2967             return .{ .context = self };
   2968         }
   2969     };
   2970 }
   2971 
   2972 pub fn headerSlurpingReader(comptime size: usize, reader: anytype) HeaderSlurpingReader(size, @TypeOf(reader)) {
   2973     return .{ .child_reader = reader };
   2974 }
   2975 
   2976 /// Sort of like std.io.LimitedReader, but a Writer.
   2977 /// Returns an error if writing the requested number of bytes
   2978 /// would ever exceed bytes_left, i.e. it does not always
   2979 /// write up to the limit and instead will error if the
   2980 /// limit would be breached if the entire slice was written.
   2981 pub fn LimitedWriter(comptime WriterType: type) type {
   2982     return struct {
   2983         inner_writer: WriterType,
   2984         bytes_left: u64,
   2985 
   2986         pub const Error = error{NoSpaceLeft} || WriterType.Error;
   2987         pub const Writer = std.io.GenericWriter(*Self, Error, write);
   2988 
   2989         const Self = @This();
   2990 
   2991         pub fn write(self: *Self, bytes: []const u8) Error!usize {
   2992             if (bytes.len > self.bytes_left) return error.NoSpaceLeft;
   2993             const amt = try self.inner_writer.write(bytes);
   2994             self.bytes_left -= amt;
   2995             return amt;
   2996         }
   2997 
   2998         pub fn writer(self: *Self) Writer {
   2999             return .{ .context = self };
   3000         }
   3001     };
   3002 }
   3003 
   3004 /// Returns an initialised `LimitedWriter`
   3005 /// `bytes_left` is a `u64` to be able to take 64 bit file offsets
   3006 pub fn limitedWriter(inner_writer: anytype, bytes_left: u64) LimitedWriter(@TypeOf(inner_writer)) {
   3007     return .{ .inner_writer = inner_writer, .bytes_left = bytes_left };
   3008 }
   3009 
   3010 test "limitedWriter basic usage" {
   3011     var buf: [4]u8 = undefined;
   3012     var fbs = std.io.fixedBufferStream(&buf);
   3013     var limited_stream = limitedWriter(fbs.writer(), 4);
   3014     var writer = limited_stream.writer();
   3015 
   3016     try std.testing.expectEqual(@as(usize, 3), try writer.write("123"));
   3017     try std.testing.expectEqualSlices(u8, "123", buf[0..3]);
   3018     try std.testing.expectError(error.NoSpaceLeft, writer.write("45"));
   3019     try std.testing.expectEqual(@as(usize, 1), try writer.write("4"));
   3020     try std.testing.expectEqualSlices(u8, "1234", buf[0..4]);
   3021     try std.testing.expectError(error.NoSpaceLeft, writer.write("5"));
   3022 }
   3023 
   3024 pub const FontDir = struct {
   3025     fonts: std.ArrayListUnmanaged(Font) = .empty,
   3026     /// To keep track of which ids are set and where they were set from
   3027     ids: std.AutoHashMapUnmanaged(u16, Token) = .empty,
   3028 
   3029     pub const Font = struct {
   3030         id: u16,
   3031         header_bytes: [148]u8,
   3032     };
   3033 
   3034     pub fn deinit(self: *FontDir, allocator: Allocator) void {
   3035         self.fonts.deinit(allocator);
   3036     }
   3037 
   3038     pub fn add(self: *FontDir, allocator: Allocator, font: Font, id_token: Token) !void {
   3039         try self.ids.putNoClobber(allocator, font.id, id_token);
   3040         try self.fonts.append(allocator, font);
   3041     }
   3042 
   3043     pub fn writeResData(self: *FontDir, compiler: *Compiler, writer: anytype) !void {
   3044         if (self.fonts.items.len == 0) return;
   3045 
   3046         // We know the number of fonts is limited to maxInt(u16) because fonts
   3047         // must have a valid and unique u16 ordinal ID (trying to specify a FONT
   3048         // with e.g. id 65537 will wrap around to 1 and be ignored if there's already
   3049         // a font with that ID in the file).
   3050         const num_fonts: u16 = @intCast(self.fonts.items.len);
   3051 
   3052         // u16 count + [(u16 id + 150 bytes) for each font]
   3053         // Note: This works out to a maximum data_size of 9,961,322.
   3054         const data_size: u32 = 2 + (2 + 150) * num_fonts;
   3055 
   3056         var header = Compiler.ResourceHeader{
   3057             .name_value = try NameOrOrdinal.nameFromString(compiler.allocator, .{ .slice = "FONTDIR", .code_page = .windows1252 }),
   3058             .type_value = NameOrOrdinal{ .ordinal = @intFromEnum(res.RT.FONTDIR) },
   3059             .memory_flags = res.MemoryFlags.defaults(res.RT.FONTDIR),
   3060             .language = compiler.state.language,
   3061             .version = compiler.state.version,
   3062             .characteristics = compiler.state.characteristics,
   3063             .data_size = data_size,
   3064         };
   3065         defer header.deinit(compiler.allocator);
   3066 
   3067         try header.writeAssertNoOverflow(writer);
   3068         try writer.writeInt(u16, num_fonts, .little);
   3069         for (self.fonts.items) |font| {
   3070             // The format of the FONTDIR is a strange beast.
   3071             // Technically, each FONT is seemingly meant to be written as a
   3072             // FONTDIRENTRY with two trailing NUL-terminated strings corresponding to
   3073             // the 'device name' and 'face name' of the .FNT file, but:
   3074             //
   3075             // 1. When dealing with .FNT files, the Win32 implementation
   3076             //    gets the device name and face name from the wrong locations,
   3077             //    so it's basically never going to write the real device/face name
   3078             //    strings.
   3079             // 2. When dealing with files 76-140 bytes long, the Win32 implementation
   3080             //    can just crash (if there are no NUL bytes in the file).
   3081             // 3. The 32-bit Win32 rc.exe uses a 148 byte size for the portion of
   3082             //    the FONTDIRENTRY before the NUL-terminated strings, which
   3083             //    does not match the documented FONTDIRENTRY size that (presumably)
   3084             //    this format is meant to be using, so anything iterating the
   3085             //    FONTDIR according to the available documentation will get bogus results.
   3086             // 4. The FONT resource can be used for non-.FNT types like TTF and OTF,
   3087             //    in which case emulating the Win32 behavior of unconditionally
   3088             //    interpreting the bytes as a .FNT and trying to grab device/face names
   3089             //    from random bytes in the TTF/OTF file can lead to weird behavior
   3090             //    and errors in the Win32 implementation (for example, the device/face
   3091             //    name fields are offsets into the file where the NUL-terminated
   3092             //    string is located, but the Win32 implementation actually treats
   3093             //    them as signed so if they are negative then the Win32 implementation
   3094             //    will error; this happening for TTF fonts would just be a bug
   3095             //    since the TTF could otherwise be valid)
   3096             // 5. The FONTDIR resource doesn't actually seem to be used at all by
   3097             //    anything that I've found, and instead in Windows 3.0 and newer
   3098             //    it seems like the FONT resources are always just iterated/accessed
   3099             //    directly without ever looking at the FONTDIR.
   3100             //
   3101             // All of these combined means that we:
   3102             // - Do not need or want to emulate Win32 behavior here
   3103             // - For maximum simplicity and compatibility, we just write the first
   3104             //   148 bytes of the file without any interpretation (padded with
   3105             //   zeroes to get up to 148 bytes if necessary), and then
   3106             //   unconditionally write two NUL bytes, meaning that we always
   3107             //   write 'device name' and 'face name' as if they were 0-length
   3108             //   strings.
   3109             //
   3110             // This gives us byte-for-byte .RES compatibility in the common case while
   3111             // allowing us to avoid any erroneous errors caused by trying to read
   3112             // the face/device name from a bogus location. Note that the Win32
   3113             // implementation never actually writes the real device/face name here
   3114             // anyway (except in the bizarre case that a .FNT file has the proper
   3115             // device/face name offsets within a reserved section of the .FNT file)
   3116             // so there's no feasible way that anything can actually think that the
   3117             // device name/face name in the FONTDIR is reliable.
   3118 
   3119             // First, the ID is written, though
   3120             try writer.writeInt(u16, font.id, .little);
   3121             try writer.writeAll(&font.header_bytes);
   3122             try writer.splatByteAll(0, 2);
   3123         }
   3124         try Compiler.writeDataPadding(writer, data_size);
   3125     }
   3126 };
   3127 
   3128 pub const StringTablesByLanguage = struct {
   3129     /// String tables for each language are written to the .res file in order depending on
   3130     /// when the first STRINGTABLE for the language was defined, and all blocks for a given
   3131     /// language are written contiguously.
   3132     /// Using an ArrayHashMap here gives us this property for free.
   3133     tables: std.AutoArrayHashMapUnmanaged(res.Language, StringTable) = .empty,
   3134 
   3135     pub fn deinit(self: *StringTablesByLanguage, allocator: Allocator) void {
   3136         self.tables.deinit(allocator);
   3137     }
   3138 
   3139     pub fn set(
   3140         self: *StringTablesByLanguage,
   3141         allocator: Allocator,
   3142         language: res.Language,
   3143         id: u16,
   3144         string_token: Token,
   3145         node: *Node,
   3146         source: []const u8,
   3147         code_page_lookup: *const CodePageLookup,
   3148         version: u32,
   3149         characteristics: u32,
   3150     ) StringTable.SetError!void {
   3151         var get_or_put_result = try self.tables.getOrPut(allocator, language);
   3152         if (!get_or_put_result.found_existing) {
   3153             get_or_put_result.value_ptr.* = StringTable{};
   3154         }
   3155         return get_or_put_result.value_ptr.set(allocator, id, string_token, node, source, code_page_lookup, version, characteristics);
   3156     }
   3157 };
   3158 
   3159 pub const StringTable = struct {
   3160     /// Blocks are written to the .res file in order depending on when the first string
   3161     /// was added to the block (i.e. `STRINGTABLE { 16 "b" 0 "a" }` would then get written
   3162     /// with block ID 2 (the one with "b") first and block ID 1 (the one with "a") second).
   3163     /// Using an ArrayHashMap here gives us this property for free.
   3164     blocks: std.AutoArrayHashMapUnmanaged(u16, Block) = .empty,
   3165 
   3166     pub const Block = struct {
   3167         strings: std.ArrayListUnmanaged(Token) = .empty,
   3168         set_indexes: std.bit_set.IntegerBitSet(16) = .{ .mask = 0 },
   3169         memory_flags: MemoryFlags = MemoryFlags.defaults(res.RT.STRING),
   3170         characteristics: u32,
   3171         version: u32,
   3172 
   3173         /// Returns the index to insert the string into the `strings` list.
   3174         /// Returns null if the string should be appended.
   3175         fn getInsertionIndex(self: *Block, index: u8) ?u8 {
   3176             std.debug.assert(!self.set_indexes.isSet(index));
   3177 
   3178             const first_set = self.set_indexes.findFirstSet() orelse return null;
   3179             if (first_set > index) return 0;
   3180 
   3181             const last_set = 15 - @clz(self.set_indexes.mask);
   3182             if (index > last_set) return null;
   3183 
   3184             var bit = first_set + 1;
   3185             var insertion_index: u8 = 1;
   3186             while (bit != index) : (bit += 1) {
   3187                 if (self.set_indexes.isSet(bit)) insertion_index += 1;
   3188             }
   3189             return insertion_index;
   3190         }
   3191 
   3192         fn getTokenIndex(self: *Block, string_index: u8) ?u8 {
   3193             const count = self.strings.items.len;
   3194             if (count == 0) return null;
   3195             if (count == 1) return 0;
   3196 
   3197             const first_set = self.set_indexes.findFirstSet() orelse unreachable;
   3198             if (first_set == string_index) return 0;
   3199             const last_set = 15 - @clz(self.set_indexes.mask);
   3200             if (last_set == string_index) return @intCast(count - 1);
   3201 
   3202             if (first_set == last_set) return null;
   3203 
   3204             var bit = first_set + 1;
   3205             var token_index: u8 = 1;
   3206             while (bit < last_set) : (bit += 1) {
   3207                 if (!self.set_indexes.isSet(bit)) continue;
   3208                 if (bit == string_index) return token_index;
   3209                 token_index += 1;
   3210             }
   3211             return null;
   3212         }
   3213 
   3214         fn dump(self: *Block) void {
   3215             var bit_it = self.set_indexes.iterator(.{});
   3216             var string_index: usize = 0;
   3217             while (bit_it.next()) |bit_index| {
   3218                 const token = self.strings.items[string_index];
   3219                 std.debug.print("{}: [{}] {any}\n", .{ bit_index, string_index, token });
   3220                 string_index += 1;
   3221             }
   3222         }
   3223 
   3224         pub fn applyAttributes(self: *Block, string_table: *Node.StringTable, source: []const u8, code_page_lookup: *const CodePageLookup) void {
   3225             Compiler.applyToMemoryFlags(&self.memory_flags, string_table.common_resource_attributes, source);
   3226             var dummy_language: res.Language = undefined;
   3227             Compiler.applyToOptionalStatements(&dummy_language, &self.version, &self.characteristics, string_table.optional_statements, source, code_page_lookup);
   3228         }
   3229 
   3230         fn trimToDoubleNUL(comptime T: type, str: []const T) []const T {
   3231             var last_was_null = false;
   3232             for (str, 0..) |c, i| {
   3233                 if (c == 0) {
   3234                     if (last_was_null) return str[0 .. i - 1];
   3235                     last_was_null = true;
   3236                 } else {
   3237                     last_was_null = false;
   3238                 }
   3239             }
   3240             return str;
   3241         }
   3242 
   3243         test "trimToDoubleNUL" {
   3244             try std.testing.expectEqualStrings("a\x00b", trimToDoubleNUL(u8, "a\x00b"));
   3245             try std.testing.expectEqualStrings("a", trimToDoubleNUL(u8, "a\x00\x00b"));
   3246         }
   3247 
   3248         pub fn writeResData(self: *Block, compiler: *Compiler, language: res.Language, block_id: u16, writer: anytype) !void {
   3249             var data_buffer = std.array_list.Managed(u8).init(compiler.allocator);
   3250             defer data_buffer.deinit();
   3251             const data_writer = data_buffer.writer();
   3252 
   3253             var i: u8 = 0;
   3254             var string_i: u8 = 0;
   3255             while (true) : (i += 1) {
   3256                 if (!self.set_indexes.isSet(i)) {
   3257                     try data_writer.writeInt(u16, 0, .little);
   3258                     if (i == 15) break else continue;
   3259                 }
   3260 
   3261                 const string_token = self.strings.items[string_i];
   3262                 const slice = string_token.slice(compiler.source);
   3263                 const column = string_token.calculateColumn(compiler.source, 8, null);
   3264                 const code_page = compiler.input_code_pages.getForToken(string_token);
   3265                 const bytes = SourceBytes{ .slice = slice, .code_page = code_page };
   3266                 const utf16_string = try literals.parseQuotedStringAsWideString(compiler.allocator, bytes, .{
   3267                     .start_column = column,
   3268                     .diagnostics = compiler.errContext(string_token),
   3269                     .output_code_page = compiler.output_code_pages.getForToken(string_token),
   3270                 });
   3271                 defer compiler.allocator.free(utf16_string);
   3272 
   3273                 const trimmed_string = trim: {
   3274                     // Two NUL characters in a row act as a terminator
   3275                     // Note: This is only the case for STRINGTABLE strings
   3276                     const trimmed = trimToDoubleNUL(u16, utf16_string);
   3277                     // We also want to trim any trailing NUL characters
   3278                     break :trim std.mem.trimEnd(u16, trimmed, &[_]u16{0});
   3279                 };
   3280 
   3281                 // String literals are limited to maxInt(u15) codepoints, so these UTF-16 encoded
   3282                 // strings are limited to maxInt(u15) * 2 = 65,534 code units (since 2 is the
   3283                 // maximum number of UTF-16 code units per codepoint).
   3284                 // This leaves room for exactly one NUL terminator.
   3285                 var string_len_in_utf16_code_units: u16 = @intCast(trimmed_string.len);
   3286                 // If the option is set, then a NUL terminator is added unconditionally.
   3287                 // We already trimmed any trailing NULs, so we know it will be a new addition to the string.
   3288                 if (compiler.null_terminate_string_table_strings) string_len_in_utf16_code_units += 1;
   3289                 try data_writer.writeInt(u16, string_len_in_utf16_code_units, .little);
   3290                 try data_writer.writeAll(std.mem.sliceAsBytes(trimmed_string));
   3291                 if (compiler.null_terminate_string_table_strings) {
   3292                     try data_writer.writeInt(u16, 0, .little);
   3293                 }
   3294 
   3295                 if (i == 15) break;
   3296                 string_i += 1;
   3297             }
   3298 
   3299             // This intCast will never be able to fail due to the length constraints on string literals.
   3300             //
   3301             // - STRINGTABLE resource definitions can can only provide one string literal per index.
   3302             // - STRINGTABLE strings are limited to maxInt(u16) UTF-16 code units (see 'string_len_in_utf16_code_units'
   3303             //   above), which means that the maximum number of bytes per string literal is
   3304             //   2 * maxInt(u16) = 131,070 (since there are 2 bytes per UTF-16 code unit).
   3305             // - Each Block/RT_STRING resource includes exactly 16 strings and each have a 2 byte
   3306             //   length field, so the maximum number of total bytes in a RT_STRING resource's data is
   3307             //   16 * (131,070 + 2) = 2,097,152 which is well within the u32 max.
   3308             //
   3309             // Note: The string literal maximum length is enforced by the lexer.
   3310             const data_size: u32 = @intCast(data_buffer.items.len);
   3311 
   3312             const header = Compiler.ResourceHeader{
   3313                 .name_value = .{ .ordinal = block_id },
   3314                 .type_value = .{ .ordinal = @intFromEnum(res.RT.STRING) },
   3315                 .memory_flags = self.memory_flags,
   3316                 .language = language,
   3317                 .version = self.version,
   3318                 .characteristics = self.characteristics,
   3319                 .data_size = data_size,
   3320             };
   3321             // The only variable parts of the header are name and type, which in this case
   3322             // we fully control and know are numbers, so they have a fixed size.
   3323             try header.writeAssertNoOverflow(writer);
   3324 
   3325             var data_fbs: std.Io.Reader = .fixed(data_buffer.items);
   3326             try Compiler.writeResourceData(writer, &data_fbs, data_size);
   3327         }
   3328     };
   3329 
   3330     pub fn deinit(self: *StringTable, allocator: Allocator) void {
   3331         var it = self.blocks.iterator();
   3332         while (it.next()) |entry| {
   3333             entry.value_ptr.strings.deinit(allocator);
   3334         }
   3335         self.blocks.deinit(allocator);
   3336     }
   3337 
   3338     const SetError = error{StringAlreadyDefined} || Allocator.Error;
   3339 
   3340     pub fn set(
   3341         self: *StringTable,
   3342         allocator: Allocator,
   3343         id: u16,
   3344         string_token: Token,
   3345         node: *Node,
   3346         source: []const u8,
   3347         code_page_lookup: *const CodePageLookup,
   3348         version: u32,
   3349         characteristics: u32,
   3350     ) SetError!void {
   3351         const block_id = (id / 16) + 1;
   3352         const string_index: u8 = @intCast(id & 0xF);
   3353 
   3354         var get_or_put_result = try self.blocks.getOrPut(allocator, block_id);
   3355         if (!get_or_put_result.found_existing) {
   3356             get_or_put_result.value_ptr.* = Block{ .version = version, .characteristics = characteristics };
   3357             get_or_put_result.value_ptr.applyAttributes(node.cast(.string_table).?, source, code_page_lookup);
   3358         } else {
   3359             if (get_or_put_result.value_ptr.set_indexes.isSet(string_index)) {
   3360                 return error.StringAlreadyDefined;
   3361             }
   3362         }
   3363 
   3364         var block = get_or_put_result.value_ptr;
   3365         if (block.getInsertionIndex(string_index)) |insertion_index| {
   3366             try block.strings.insert(allocator, insertion_index, string_token);
   3367         } else {
   3368             try block.strings.append(allocator, string_token);
   3369         }
   3370         block.set_indexes.set(string_index);
   3371     }
   3372 
   3373     pub fn get(self: *StringTable, id: u16) ?Token {
   3374         const block_id = (id / 16) + 1;
   3375         const string_index: u8 = @intCast(id & 0xF);
   3376 
   3377         const block = self.blocks.getPtr(block_id) orelse return null;
   3378         const token_index = block.getTokenIndex(string_index) orelse return null;
   3379         return block.strings.items[token_index];
   3380     }
   3381 
   3382     pub fn dump(self: *StringTable) !void {
   3383         var it = self.iterator();
   3384         while (it.next()) |entry| {
   3385             std.debug.print("block: {}\n", .{entry.key_ptr.*});
   3386             entry.value_ptr.dump();
   3387         }
   3388     }
   3389 };
   3390 
   3391 test "StringTable" {
   3392     const S = struct {
   3393         fn makeDummyToken(id: usize) Token {
   3394             return Token{
   3395                 .id = .invalid,
   3396                 .start = id,
   3397                 .end = id,
   3398                 .line_number = id,
   3399             };
   3400         }
   3401     };
   3402     const allocator = std.testing.allocator;
   3403     var string_table = StringTable{};
   3404     defer string_table.deinit(allocator);
   3405 
   3406     var code_page_lookup = CodePageLookup.init(allocator, .windows1252);
   3407     defer code_page_lookup.deinit();
   3408 
   3409     var dummy_node = Node.StringTable{
   3410         .type = S.makeDummyToken(0),
   3411         .common_resource_attributes = &.{},
   3412         .optional_statements = &.{},
   3413         .begin_token = S.makeDummyToken(0),
   3414         .strings = &.{},
   3415         .end_token = S.makeDummyToken(0),
   3416     };
   3417 
   3418     // randomize an array of ids 0-99
   3419     var ids = ids: {
   3420         var buf: [100]u16 = undefined;
   3421         var i: u16 = 0;
   3422         while (i < buf.len) : (i += 1) {
   3423             buf[i] = i;
   3424         }
   3425         break :ids buf;
   3426     };
   3427     var prng = std.Random.DefaultPrng.init(0);
   3428     var random = prng.random();
   3429     random.shuffle(u16, &ids);
   3430 
   3431     // set each one in the randomized order
   3432     for (ids) |id| {
   3433         try string_table.set(allocator, id, S.makeDummyToken(id), &dummy_node.base, "", &code_page_lookup, 0, 0);
   3434     }
   3435 
   3436     // make sure each one exists and is the right value when gotten
   3437     var id: u16 = 0;
   3438     while (id < 100) : (id += 1) {
   3439         const dummy = S.makeDummyToken(id);
   3440         try std.testing.expectError(error.StringAlreadyDefined, string_table.set(allocator, id, dummy, &dummy_node.base, "", &code_page_lookup, 0, 0));
   3441         try std.testing.expectEqual(dummy, string_table.get(id).?);
   3442     }
   3443 
   3444     // make sure non-existent string ids are not found
   3445     try std.testing.expectEqual(@as(?Token, null), string_table.get(100));
   3446 }