Commit 52de2802c4

Ryan Liptak <squeek502@hotmail.com>
2024-03-02 09:50:53
Lazily compile the `zig rc` subcommand and use it during `zig build-exe`
This moves .rc/.manifest compilation out of the main Zig binary, contributing towards #19063 Also: - Make resinator use Aro as its preprocessor instead of clang - Sync resinator with upstream
1 parent d0c06ca
src/resinator/ani.zig → lib/compiler/resinator/ani.zig
File renamed without changes
src/resinator/ast.zig → lib/compiler/resinator/ast.zig
File renamed without changes
src/resinator/bmp.zig → lib/compiler/resinator/bmp.zig
File renamed without changes
src/resinator/cli.zig → lib/compiler/resinator/cli.zig
@@ -50,8 +50,14 @@ pub const usage_string_after_command_name =
     \\  /:auto-includes <value>   Set the automatic include path detection behavior.
     \\    any                     (default) Use MSVC if available, fall back to MinGW
     \\    msvc                    Use MSVC include paths (must be present on the system)
-    \\    gnu                     Use MinGW include paths (requires Zig as the preprocessor)
+    \\    gnu                     Use MinGW include paths
     \\    none                    Do not use any autodetected include paths
+    \\  /:depfile <path>          Output a file containing a list of all the files that
+    \\                            the .rc includes or otherwise depends on.
+    \\  /:depfile-fmt <value>     Output format of the depfile, if /:depfile is set.
+    \\    json                    (default) A top-level JSON array of paths
+    \\  /:mingw-includes <path>   Path to a directory containing MinGW include files. If
+    \\                            not specified, bundled MinGW include files will be used.
     \\
     \\Note: For compatibility reasons, all custom options start with :
     \\
@@ -140,8 +146,12 @@ pub const Options = struct {
     debug: bool = false,
     print_help_and_exit: bool = false,
     auto_includes: AutoIncludes = .any,
+    depfile_path: ?[]const u8 = null,
+    depfile_fmt: DepfileFormat = .json,
+    mingw_includes_dir: ?[]const u8 = null,
 
     pub const AutoIncludes = enum { any, msvc, gnu, none };
+    pub const DepfileFormat = enum { json };
     pub const Preprocess = enum { no, yes, only };
     pub const SymbolAction = enum { define, undefine };
     pub const SymbolValue = union(SymbolAction) {
@@ -207,7 +217,7 @@ pub const Options = struct {
             cwd.access(options.input_filename, .{}) catch |err| switch (err) {
                 error.FileNotFound => {
                     var filename_bytes = try options.allocator.alloc(u8, options.input_filename.len + 3);
-                    @memcpy(filename_bytes[0 .. filename_bytes.len - 3], options.input_filename);
+                    @memcpy(filename_bytes[0..options.input_filename.len], options.input_filename);
                     @memcpy(filename_bytes[filename_bytes.len - 3 ..], ".rc");
                     options.allocator.free(options.input_filename);
                     options.input_filename = filename_bytes;
@@ -230,6 +240,12 @@ pub const Options = struct {
             entry.value_ptr.deinit(self.allocator);
         }
         self.symbols.deinit(self.allocator);
+        if (self.depfile_path) |depfile_path| {
+            self.allocator.free(depfile_path);
+        }
+        if (self.mingw_includes_dir) |mingw_includes_dir| {
+            self.allocator.free(mingw_includes_dir);
+        }
     }
 
     pub fn dumpVerbose(self: *const Options, writer: anytype) !void {
@@ -394,7 +410,7 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn
     var output_filename: ?[]const u8 = null;
     var output_filename_context: Arg.Context = undefined;
 
-    var arg_i: usize = 1; // start at 1 to skip past the exe name
+    var arg_i: usize = 0;
     next_arg: while (arg_i < args.len) {
         var arg = Arg.fromString(args[arg_i]) orelse break;
         if (arg.name().len == 0) {
@@ -424,6 +440,24 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn
             if (std.ascii.startsWithIgnoreCase(arg_name, ":no-preprocess")) {
                 options.preprocess = .no;
                 arg.name_offset += ":no-preprocess".len;
+            } else if (std.ascii.startsWithIgnoreCase(arg_name, ":mingw-includes")) {
+                const value = arg.value(":mingw-includes".len, arg_i, args) catch {
+                    var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
+                    var msg_writer = err_details.msg.writer(allocator);
+                    try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(":mingw-includes".len) });
+                    try diagnostics.append(err_details);
+                    arg_i += 1;
+                    break :next_arg;
+                };
+                if (options.mingw_includes_dir) |overwritten_path| {
+                    allocator.free(overwritten_path);
+                    options.mingw_includes_dir = null;
+                }
+                const path = try allocator.dupe(u8, value.slice);
+                errdefer allocator.free(path);
+                options.mingw_includes_dir = path;
+                arg_i += value.index_increment;
+                continue :next_arg;
             } else if (std.ascii.startsWithIgnoreCase(arg_name, ":auto-includes")) {
                 const value = arg.value(":auto-includes".len, arg_i, args) catch {
                     var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
@@ -442,6 +476,42 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn
                 };
                 arg_i += value.index_increment;
                 continue :next_arg;
+            } else if (std.ascii.startsWithIgnoreCase(arg_name, ":depfile-fmt")) {
+                const value = arg.value(":depfile-fmt".len, arg_i, args) catch {
+                    var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
+                    var msg_writer = err_details.msg.writer(allocator);
+                    try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(":depfile-fmt".len) });
+                    try diagnostics.append(err_details);
+                    arg_i += 1;
+                    break :next_arg;
+                };
+                options.depfile_fmt = std.meta.stringToEnum(Options.DepfileFormat, value.slice) orelse blk: {
+                    var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = value.argSpan(arg) };
+                    var msg_writer = err_details.msg.writer(allocator);
+                    try msg_writer.print("invalid depfile format setting: {s} ", .{value.slice});
+                    try diagnostics.append(err_details);
+                    break :blk options.depfile_fmt;
+                };
+                arg_i += value.index_increment;
+                continue :next_arg;
+            } else if (std.ascii.startsWithIgnoreCase(arg_name, ":depfile")) {
+                const value = arg.value(":depfile".len, arg_i, args) catch {
+                    var err_details = Diagnostics.ErrorDetails{ .arg_index = arg_i, .arg_span = arg.missingSpan() };
+                    var msg_writer = err_details.msg.writer(allocator);
+                    try msg_writer.print("missing value after {s}{s} option", .{ arg.prefixSlice(), arg.optionWithoutPrefix(":depfile".len) });
+                    try diagnostics.append(err_details);
+                    arg_i += 1;
+                    break :next_arg;
+                };
+                if (options.depfile_path) |overwritten_path| {
+                    allocator.free(overwritten_path);
+                    options.depfile_path = null;
+                }
+                const path = try allocator.dupe(u8, value.slice);
+                errdefer allocator.free(path);
+                options.depfile_path = path;
+                arg_i += value.index_increment;
+                continue :next_arg;
             } else if (std.ascii.startsWithIgnoreCase(arg_name, "nologo")) {
                 // No-op, we don't display any 'logo' to suppress
                 arg.name_offset += "nologo".len;
@@ -837,7 +907,7 @@ pub fn parse(allocator: Allocator, args: []const []const u8, diagnostics: *Diagn
         try diagnostics.append(err_details);
 
         const last_arg = args[args.len - 1];
-        if (arg_i > 1 and last_arg.len > 0 and last_arg[0] == '/' and std.ascii.endsWithIgnoreCase(last_arg, ".rc")) {
+        if (arg_i > 0 and last_arg.len > 0 and last_arg[0] == '/' and std.ascii.endsWithIgnoreCase(last_arg, ".rc")) {
             var note_details = Diagnostics.ErrorDetails{ .type = .note, .print_args = true, .arg_index = arg_i - 1 };
             var note_writer = note_details.msg.writer(allocator);
             try note_writer.writeAll("if this argument was intended to be the input filename, then -- should be specified in front of it to exclude it from option parsing");
@@ -1116,7 +1186,7 @@ fn testParseOutput(args: []const []const u8, expected_output: []const u8) !?Opti
 }
 
 test "parse errors: basic" {
-    try testParseError(&.{ "foo.exe", "/" },
+    try testParseError(&.{"/"},
         \\<cli>: error: invalid option: /
         \\ ... /
         \\     ^
@@ -1124,7 +1194,7 @@ test "parse errors: basic" {
         \\
         \\
     );
-    try testParseError(&.{ "foo.exe", "/ln" },
+    try testParseError(&.{"/ln"},
         \\<cli>: error: missing language tag after /ln option
         \\ ... /ln
         \\     ~~~~^
@@ -1132,7 +1202,7 @@ test "parse errors: basic" {
         \\
         \\
     );
-    try testParseError(&.{ "foo.exe", "-vln" },
+    try testParseError(&.{"-vln"},
         \\<cli>: error: missing language tag after -ln option
         \\ ... -vln
         \\     ~ ~~~^
@@ -1140,7 +1210,7 @@ test "parse errors: basic" {
         \\
         \\
     );
-    try testParseError(&.{ "foo.exe", "/_not-an-option" },
+    try testParseError(&.{"/_not-an-option"},
         \\<cli>: error: invalid option: /_not-an-option
         \\ ... /_not-an-option
         \\     ~^~~~~~~~~~~~~~
@@ -1148,7 +1218,7 @@ test "parse errors: basic" {
         \\
         \\
     );
-    try testParseError(&.{ "foo.exe", "-_not-an-option" },
+    try testParseError(&.{"-_not-an-option"},
         \\<cli>: error: invalid option: -_not-an-option
         \\ ... -_not-an-option
         \\     ~^~~~~~~~~~~~~~
@@ -1156,7 +1226,7 @@ test "parse errors: basic" {
         \\
         \\
     );
-    try testParseError(&.{ "foo.exe", "--_not-an-option" },
+    try testParseError(&.{"--_not-an-option"},
         \\<cli>: error: invalid option: --_not-an-option
         \\ ... --_not-an-option
         \\     ~~^~~~~~~~~~~~~~
@@ -1164,7 +1234,7 @@ test "parse errors: basic" {
         \\
         \\
     );
-    try testParseError(&.{ "foo.exe", "/v_not-an-option" },
+    try testParseError(&.{"/v_not-an-option"},
         \\<cli>: error: invalid option: /_not-an-option
         \\ ... /v_not-an-option
         \\     ~ ^~~~~~~~~~~~~~
@@ -1172,7 +1242,7 @@ test "parse errors: basic" {
         \\
         \\
     );
-    try testParseError(&.{ "foo.exe", "-v_not-an-option" },
+    try testParseError(&.{"-v_not-an-option"},
         \\<cli>: error: invalid option: -_not-an-option
         \\ ... -v_not-an-option
         \\     ~ ^~~~~~~~~~~~~~
@@ -1180,7 +1250,7 @@ test "parse errors: basic" {
         \\
         \\
     );
-    try testParseError(&.{ "foo.exe", "--v_not-an-option" },
+    try testParseError(&.{"--v_not-an-option"},
         \\<cli>: error: invalid option: --_not-an-option
         \\ ... --v_not-an-option
         \\     ~~ ^~~~~~~~~~~~~~
@@ -1188,7 +1258,7 @@ test "parse errors: basic" {
         \\
         \\
     );
-    try testParseError(&.{ "foo.exe", "/some/absolute/path/parsed/as/an/option.rc" },
+    try testParseError(&.{"/some/absolute/path/parsed/as/an/option.rc"},
         \\<cli>: error: the /s option is unsupported
         \\ ... /some/absolute/path/parsed/as/an/option.rc
         \\     ~^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -1202,13 +1272,13 @@ test "parse errors: basic" {
 }
 
 test "parse errors: /ln" {
-    try testParseError(&.{ "foo.exe", "/ln", "invalid", "foo.rc" },
+    try testParseError(&.{ "/ln", "invalid", "foo.rc" },
         \\<cli>: error: invalid language tag: invalid
         \\ ... /ln invalid ...
         \\     ~~~~^~~~~~~
         \\
     );
-    try testParseError(&.{ "foo.exe", "/lninvalid", "foo.rc" },
+    try testParseError(&.{ "/lninvalid", "foo.rc" },
         \\<cli>: error: invalid language tag: invalid
         \\ ... /lninvalid ...
         \\     ~~~^~~~~~~
@@ -1218,7 +1288,7 @@ test "parse errors: /ln" {
 
 test "parse: options" {
     {
-        var options = try testParse(&.{ "foo.exe", "/v", "foo.rc" });
+        var options = try testParse(&.{ "/v", "foo.rc" });
         defer options.deinit();
 
         try std.testing.expectEqual(true, options.verbose);
@@ -1226,7 +1296,7 @@ test "parse: options" {
         try std.testing.expectEqualStrings("foo.res", options.output_filename);
     }
     {
-        var options = try testParse(&.{ "foo.exe", "/vx", "foo.rc" });
+        var options = try testParse(&.{ "/vx", "foo.rc" });
         defer options.deinit();
 
         try std.testing.expectEqual(true, options.verbose);
@@ -1235,7 +1305,7 @@ test "parse: options" {
         try std.testing.expectEqualStrings("foo.res", options.output_filename);
     }
     {
-        var options = try testParse(&.{ "foo.exe", "/xv", "foo.rc" });
+        var options = try testParse(&.{ "/xv", "foo.rc" });
         defer options.deinit();
 
         try std.testing.expectEqual(true, options.verbose);
@@ -1244,7 +1314,7 @@ test "parse: options" {
         try std.testing.expectEqualStrings("foo.res", options.output_filename);
     }
     {
-        var options = try testParse(&.{ "foo.exe", "/xvFObar.res", "foo.rc" });
+        var options = try testParse(&.{ "/xvFObar.res", "foo.rc" });
         defer options.deinit();
 
         try std.testing.expectEqual(true, options.verbose);
@@ -1256,23 +1326,21 @@ test "parse: options" {
 
 test "parse: define and undefine" {
     {
-        var options = try testParse(&.{ "foo.exe", "/dfoo", "foo.rc" });
+        var options = try testParse(&.{ "/dfoo", "foo.rc" });
         defer options.deinit();
 
         const action = options.symbols.get("foo").?;
-        try std.testing.expectEqual(Options.SymbolAction.define, action);
         try std.testing.expectEqualStrings("1", action.define);
     }
     {
-        var options = try testParse(&.{ "foo.exe", "/dfoo=bar", "/dfoo=baz", "foo.rc" });
+        var options = try testParse(&.{ "/dfoo=bar", "/dfoo=baz", "foo.rc" });
         defer options.deinit();
 
         const action = options.symbols.get("foo").?;
-        try std.testing.expectEqual(Options.SymbolAction.define, action);
         try std.testing.expectEqualStrings("baz", action.define);
     }
     {
-        var options = try testParse(&.{ "foo.exe", "/ufoo", "foo.rc" });
+        var options = try testParse(&.{ "/ufoo", "foo.rc" });
         defer options.deinit();
 
         const action = options.symbols.get("foo").?;
@@ -1280,7 +1348,7 @@ test "parse: define and undefine" {
     }
     {
         // Once undefined, future defines are ignored
-        var options = try testParse(&.{ "foo.exe", "/ufoo", "/dfoo", "foo.rc" });
+        var options = try testParse(&.{ "/ufoo", "/dfoo", "foo.rc" });
         defer options.deinit();
 
         const action = options.symbols.get("foo").?;
@@ -1288,7 +1356,7 @@ test "parse: define and undefine" {
     }
     {
         // Undefined always takes precedence
-        var options = try testParse(&.{ "foo.exe", "/dfoo", "/ufoo", "/dfoo", "foo.rc" });
+        var options = try testParse(&.{ "/dfoo", "/ufoo", "/dfoo", "foo.rc" });
         defer options.deinit();
 
         const action = options.symbols.get("foo").?;
@@ -1297,7 +1365,7 @@ test "parse: define and undefine" {
     {
         // Warn + ignore invalid identifiers
         var options = try testParseWarning(
-            &.{ "foo.exe", "/dfoo bar", "/u", "0leadingdigit", "foo.rc" },
+            &.{ "/dfoo bar", "/u", "0leadingdigit", "foo.rc" },
             \\<cli>: warning: symbol "foo bar" is not a valid identifier and therefore cannot be defined
             \\ ... /dfoo bar ...
             \\     ~~^~~~~~~
@@ -1314,7 +1382,7 @@ test "parse: define and undefine" {
 }
 
 test "parse: /sl" {
-    try testParseError(&.{ "foo.exe", "/sl", "0", "foo.rc" },
+    try testParseError(&.{ "/sl", "0", "foo.rc" },
         \\<cli>: error: percent out of range: 0 (parsed from '0')
         \\ ... /sl 0 ...
         \\     ~~~~^
@@ -1322,7 +1390,7 @@ test "parse: /sl" {
         \\
         \\
     );
-    try testParseError(&.{ "foo.exe", "/sl", "abcd", "foo.rc" },
+    try testParseError(&.{ "/sl", "abcd", "foo.rc" },
         \\<cli>: error: invalid percent format 'abcd'
         \\ ... /sl abcd ...
         \\     ~~~~^~~~
@@ -1331,25 +1399,25 @@ test "parse: /sl" {
         \\
     );
     {
-        var options = try testParse(&.{ "foo.exe", "foo.rc" });
+        var options = try testParse(&.{"foo.rc"});
         defer options.deinit();
 
         try std.testing.expectEqual(@as(u15, lex.default_max_string_literal_codepoints), options.max_string_literal_codepoints);
     }
     {
-        var options = try testParse(&.{ "foo.exe", "/sl100", "foo.rc" });
+        var options = try testParse(&.{ "/sl100", "foo.rc" });
         defer options.deinit();
 
         try std.testing.expectEqual(@as(u15, max_string_literal_length_100_percent), options.max_string_literal_codepoints);
     }
     {
-        var options = try testParse(&.{ "foo.exe", "-SL33", "foo.rc" });
+        var options = try testParse(&.{ "-SL33", "foo.rc" });
         defer options.deinit();
 
         try std.testing.expectEqual(@as(u15, 2703), options.max_string_literal_codepoints);
     }
     {
-        var options = try testParse(&.{ "foo.exe", "/sl15", "foo.rc" });
+        var options = try testParse(&.{ "/sl15", "foo.rc" });
         defer options.deinit();
 
         try std.testing.expectEqual(@as(u15, 1228), options.max_string_literal_codepoints);
@@ -1357,7 +1425,7 @@ test "parse: /sl" {
 }
 
 test "parse: unsupported MUI-related options" {
-    try testParseError(&.{ "foo.exe", "/q", "blah", "/g1", "-G2", "blah", "/fm", "blah", "/g", "blah", "foo.rc" },
+    try testParseError(&.{ "/q", "blah", "/g1", "-G2", "blah", "/fm", "blah", "/g", "blah", "foo.rc" },
         \\<cli>: error: the /q option is unsupported
         \\ ... /q ...
         \\     ~^
@@ -1378,7 +1446,7 @@ test "parse: unsupported MUI-related options" {
 }
 
 test "parse: unsupported LCX/LCE-related options" {
-    try testParseError(&.{ "foo.exe", "/t", "/tp:", "/tp:blah", "/tm", "/tc", "/tw", "-TEti", "/ta", "/tn", "blah", "foo.rc" },
+    try testParseError(&.{ "/t", "/tp:", "/tp:blah", "/tm", "/tc", "/tw", "-TEti", "/ta", "/tn", "blah", "foo.rc" },
         \\<cli>: error: the /t option is unsupported
         \\ ... /t ...
         \\     ~^
@@ -1420,7 +1488,7 @@ test "maybeAppendRC" {
     var tmp = std.testing.tmpDir(.{});
     defer tmp.cleanup();
 
-    var options = try testParse(&.{ "foo.exe", "foo" });
+    var options = try testParse(&.{"foo"});
     defer options.deinit();
     try std.testing.expectEqualStrings("foo", options.input_filename);
 
src/resinator/code_pages.zig → lib/compiler/resinator/code_pages.zig
@@ -279,6 +279,9 @@ pub const CodePage = enum(u16) {
 pub const Utf8 = struct {
     /// Implements decoding with rejection of ill-formed UTF-8 sequences based on section
     /// D92 of Chapter 3 of the Unicode standard (Table 3-7 specifically).
+    ///
+    /// Note: This does not match "U+FFFD Substitution of Maximal Subparts", but instead
+    ///       matches the behavior of the Windows RC compiler.
     pub const WellFormedDecoder = struct {
         /// Like std.unicode.utf8ByteSequenceLength, but:
         /// - Rejects non-well-formed first bytes, i.e. C0-C1, F5-FF
@@ -347,9 +350,6 @@ pub const Utf8 = struct {
                     // Only include the byte in the invalid sequence if it's in the range
                     // of a continuation byte. All other values should not be included in the
                     // invalid sequence.
-                    //
-                    // Note: This is how the Windows RC compiler handles this, this may not
-                    //       be the correct-as-according-to-the-Unicode-standard way to do it.
                     if (isContinuationByte(byte)) len += 1;
                     return .{ .value = Codepoint.invalid, .byte_len = len };
                 }
@@ -437,6 +437,19 @@ test "codepointAt invalid utf8" {
         }, CodePage.utf8.codepointAt(1, invalid_utf8).?);
         try std.testing.expectEqual(@as(?Codepoint, null), CodePage.windows1252.codepointAt(2, invalid_utf8));
     }
+
+    {
+        // encoded high surrogate
+        const invalid_utf8 = "\xED\xA0\xBD";
+        try std.testing.expectEqual(Codepoint{
+            .value = Codepoint.invalid,
+            .byte_len = 2,
+        }, CodePage.utf8.codepointAt(0, invalid_utf8).?);
+        try std.testing.expectEqual(Codepoint{
+            .value = Codepoint.invalid,
+            .byte_len = 1,
+        }, CodePage.utf8.codepointAt(2, invalid_utf8).?);
+    }
 }
 
 test "codepointAt utf8 encoded" {
src/resinator/comments.zig → lib/compiler/resinator/comments.zig
@@ -22,7 +22,7 @@ const formsLineEndingPair = @import("source_mapping.zig").formsLineEndingPair;
 
 /// `buf` must be at least as long as `source`
 /// In-place transformation is supported (i.e. `source` and `buf` can be the same slice)
-pub fn removeComments(source: []const u8, buf: []u8, source_mappings: ?*SourceMappings) []u8 {
+pub fn removeComments(source: []const u8, buf: []u8, source_mappings: ?*SourceMappings) ![]u8 {
     std.debug.assert(buf.len >= source.len);
     var result = UncheckedSliceWriter{ .slice = buf };
     const State = enum {
@@ -85,7 +85,7 @@ pub fn removeComments(source: []const u8, buf: []u8, source_mappings: ?*SourceMa
                 else => {},
             },
             .multiline_comment => switch (c) {
-                '\r' => handleMultilineCarriageReturn(source, &line_handler, index, &result, source_mappings),
+                '\r' => try handleMultilineCarriageReturn(source, &line_handler, index, &result, source_mappings),
                 '\n' => {
                     _ = line_handler.incrementLineNumber(index);
                     result.write(c);
@@ -95,7 +95,7 @@ pub fn removeComments(source: []const u8, buf: []u8, source_mappings: ?*SourceMa
             },
             .multiline_comment_end => switch (c) {
                 '\r' => {
-                    handleMultilineCarriageReturn(source, &line_handler, index, &result, source_mappings);
+                    try handleMultilineCarriageReturn(source, &line_handler, index, &result, source_mappings);
                     // We only want to treat this as a newline if it's part of a CRLF pair. If it's
                     // not, then we still want to stay in .multiline_comment_end, so that e.g. `*<\r>/` still
                     // functions as a `*/` comment ending. Kinda crazy, but that's how the Win32 implementation works.
@@ -184,13 +184,21 @@ inline fn handleMultilineCarriageReturn(
     index: usize,
     result: *UncheckedSliceWriter,
     source_mappings: ?*SourceMappings,
-) void {
+) !void {
+    // This is a dumb way to go about this, but basically we want to determine
+    // if this is part of a distinct CRLF or LFCR pair. This function call will detect
+    // LFCR pairs correctly since the function we're in will only be called on CR,
+    // but will not detect CRLF pairs since it only looks at the line ending before the
+    // CR. So, we do a second (forward) check if the first fails to detect CRLF that is
+    // not part of another pair.
+    const is_lfcr_pair = line_handler.currentIndexFormsLineEndingPair(index);
+    const is_crlf_pair = !is_lfcr_pair and formsLineEndingPair(source, '\r', index + 1);
     // Note: Bare \r within a multiline comment should *not* be treated as a line ending for the
     // purposes of removing comments, but *should* be treated as a line ending for the
     // purposes of line counting/source mapping
     _ = line_handler.incrementLineNumber(index);
-    // So only write the \r if it's part of a CRLF pair
-    if (formsLineEndingPair(source, '\r', index + 1)) {
+    // So only write the \r if it's part of a CRLF/LFCR pair
+    if (is_lfcr_pair or is_crlf_pair) {
         result.write('\r');
     }
     // And otherwise, we want to collapse the source mapping so that we can still know which
@@ -200,7 +208,7 @@ inline fn handleMultilineCarriageReturn(
         // the next collapse acts on the first of the collapsed line numbers
         line_handler.line_number -= 1;
         if (source_mappings) |mappings| {
-            mappings.collapse(line_handler.line_number, 1);
+            try mappings.collapse(line_handler.line_number, 1);
         }
     }
 }
@@ -208,7 +216,7 @@ inline fn handleMultilineCarriageReturn(
 pub fn removeCommentsAlloc(allocator: Allocator, source: []const u8, source_mappings: ?*SourceMappings) ![]u8 {
     const buf = try allocator.alloc(u8, source.len);
     errdefer allocator.free(buf);
-    const result = removeComments(source, buf, source_mappings);
+    const result = try removeComments(source, buf, source_mappings);
     return allocator.realloc(buf, result.len);
 }
 
@@ -252,6 +260,16 @@ test "line comments retain newlines" {
     try testRemoveComments("\r\n", "//comment\r\n");
 }
 
+test "unfinished multiline comment" {
+    try testRemoveComments(
+        \\unfinished
+        \\
+    ,
+        \\unfinished/*
+        \\
+    );
+}
+
 test "crazy" {
     try testRemoveComments(
         \\blah"/*som*/\""BLAH
@@ -321,20 +339,20 @@ test "remove comments with mappings" {
     var mut_source = "blah/*\rcommented line*\r/blah".*;
     var mappings = SourceMappings{};
     _ = try mappings.files.put(allocator, "test.rc");
-    try mappings.set(allocator, 1, .{ .start_line = 1, .end_line = 1, .filename_offset = 0 });
-    try mappings.set(allocator, 2, .{ .start_line = 2, .end_line = 2, .filename_offset = 0 });
-    try mappings.set(allocator, 3, .{ .start_line = 3, .end_line = 3, .filename_offset = 0 });
+    try mappings.set(1, 1, 0);
+    try mappings.set(2, 2, 0);
+    try mappings.set(3, 3, 0);
     defer mappings.deinit(allocator);
 
-    const result = removeComments(&mut_source, &mut_source, &mappings);
+    const result = try removeComments(&mut_source, &mut_source, &mappings);
 
     try std.testing.expectEqualStrings("blahblah", result);
-    try std.testing.expectEqual(@as(usize, 1), mappings.mapping.items.len);
-    try std.testing.expectEqual(@as(usize, 3), mappings.mapping.items[0].end_line);
+    try std.testing.expectEqual(@as(usize, 1), mappings.end_line);
+    try std.testing.expectEqual(@as(usize, 3), mappings.getCorrespondingSpan(1).?.end_line);
 }
 
 test "in place" {
     var mut_source = "blah /* comment */ blah".*;
-    const result = removeComments(&mut_source, &mut_source, null);
+    const result = try removeComments(&mut_source, &mut_source, null);
     try std.testing.expectEqualStrings("blah  blah", result);
 }
src/resinator/compile.zig → lib/compiler/resinator/compile.zig
@@ -321,10 +321,7 @@ pub const Compiler = struct {
 
                         return buf.toOwnedSlice();
                     },
-                    else => {
-                        std.debug.print("unexpected filename token type: {}\n", .{literal_node.token});
-                        unreachable; // no other token types should be in a filename literal node
-                    },
+                    else => unreachable, // no other token types should be in a filename literal node
                 }
             },
             .binary_expression => {
@@ -404,6 +401,72 @@ pub const Compiler = struct {
         return first_error orelse error.FileNotFound;
     }
 
+    pub fn parseDlgIncludeString(self: *Compiler, token: Token) ![]u8 {
+        // For the purposes of parsing, we want to strip the L prefix
+        // if it exists since we want escaped integers to be limited to
+        // their ascii string range.
+        //
+        // We keep track of whether or not there was an L prefix, though,
+        // since there's more weirdness to come.
+        var bytes = self.sourceBytesForToken(token);
+        var was_wide_string = false;
+        if (bytes.slice[0] == 'L' or bytes.slice[0] == 'l') {
+            was_wide_string = true;
+            bytes.slice = bytes.slice[1..];
+        }
+
+        var buf = try std.ArrayList(u8).initCapacity(self.allocator, bytes.slice.len);
+        errdefer buf.deinit();
+
+        var iterative_parser = literals.IterativeStringParser.init(bytes, .{
+            .start_column = token.calculateColumn(self.source, 8, null),
+            .diagnostics = .{ .diagnostics = self.diagnostics, .token = token },
+        });
+
+        // No real idea what's going on here, but this matches the rc.exe behavior
+        while (try iterative_parser.next()) |parsed| {
+            const c = parsed.codepoint;
+            switch (was_wide_string) {
+                true => {
+                    switch (c) {
+                        0...0x7F, 0xA0...0xFF => try buf.append(@intCast(c)),
+                        0x80...0x9F => {
+                            if (windows1252.bestFitFromCodepoint(c)) |_| {
+                                try buf.append(@intCast(c));
+                            } else {
+                                try buf.append('?');
+                            }
+                        },
+                        else => {
+                            if (windows1252.bestFitFromCodepoint(c)) |best_fit| {
+                                try buf.append(best_fit);
+                            } else if (c < 0x10000 or c == code_pages.Codepoint.invalid) {
+                                try buf.append('?');
+                            } else {
+                                try buf.appendSlice("??");
+                            }
+                        },
+                    }
+                },
+                false => {
+                    if (parsed.from_escaped_integer) {
+                        try buf.append(@truncate(c));
+                    } else {
+                        if (windows1252.bestFitFromCodepoint(c)) |best_fit| {
+                            try buf.append(best_fit);
+                        } else if (c < 0x10000 or c == code_pages.Codepoint.invalid) {
+                            try buf.append('?');
+                        } else {
+                            try buf.appendSlice("??");
+                        }
+                    }
+                },
+            }
+        }
+
+        return buf.toOwnedSlice();
+    }
+
     pub fn writeResourceExternal(self: *Compiler, node: *Node.ResourceExternal, writer: anytype) !void {
         // Init header with data size zero for now, will need to fill it in later
         var header = try self.resourceHeader(node.id, node.type, .{});
@@ -414,13 +477,16 @@ pub const Compiler = struct {
         // DLGINCLUDE has special handling that doesn't actually need the file to exist
         if (maybe_predefined_type != null and maybe_predefined_type.? == .DLGINCLUDE) {
             const filename_token = node.filename.cast(.literal).?.token;
-            const parsed_filename = try self.parseQuotedStringAsAsciiString(filename_token);
+            const parsed_filename = try self.parseDlgIncludeString(filename_token);
             defer self.allocator.free(parsed_filename);
 
+            // NUL within the parsed string acts as a terminator
+            const parsed_filename_terminated = std.mem.sliceTo(parsed_filename, 0);
+
             header.applyMemoryFlags(node.common_resource_attributes, self.source);
-            header.data_size = @intCast(parsed_filename.len + 1);
+            header.data_size = @intCast(parsed_filename_terminated.len + 1);
             try header.write(writer, .{ .diagnostics = self.diagnostics, .token = node.id });
-            try writer.writeAll(parsed_filename);
+            try writer.writeAll(parsed_filename_terminated);
             try writer.writeByte(0);
             try writeDataPadding(writer, header.data_size);
             return;
@@ -1141,10 +1207,7 @@ pub const Compiler = struct {
                         errdefer self.allocator.free(parsed_string);
                         return .{ .wide_string = parsed_string };
                     },
-                    else => {
-                        std.debug.print("unexpected token in literal node: {}\n", .{literal_node.token});
-                        unreachable; // no other token types should be in a data literal node
-                    },
+                    else => unreachable, // no other token types should be in a data literal node
                 }
             },
             .binary_expression, .grouped_expression => {
@@ -1152,10 +1215,7 @@ pub const Compiler = struct {
                 return .{ .number = result };
             },
             .not_expression => unreachable,
-            else => {
-                std.debug.print("{}\n", .{expression_node.id});
-                @panic("TODO: evaluateDataExpression");
-            },
+            else => unreachable,
         }
     }
 
@@ -1669,6 +1729,7 @@ pub const Compiler = struct {
             };
         }
 
+        // We know the data_buffer len is limited to u32 max.
         const data_size: u32 = @intCast(data_buffer.items.len);
         var header = try self.resourceHeader(node.id, node.type, .{
             .data_size = data_size,
@@ -1966,6 +2027,7 @@ pub const Compiler = struct {
         try data_writer.writeInt(u16, 1, .little);
         try data_writer.writeInt(u16, button_width.asWord(), .little);
         try data_writer.writeInt(u16, button_height.asWord(), .little);
+        // Number of buttons is guaranteed by the parser to be within maxInt(u16).
         try data_writer.writeInt(u16, @as(u16, @intCast(node.buttons.len)), .little);
 
         for (node.buttons) |button_or_sep| {
@@ -2806,19 +2868,6 @@ pub const Compiler = struct {
         );
     }
 
-    /// Helper that calls parseQuotedStringAsAsciiString with the relevant context
-    /// Resulting slice is allocated by `self.allocator`.
-    pub fn parseQuotedStringAsAsciiString(self: *Compiler, token: Token) ![]u8 {
-        return literals.parseQuotedStringAsAsciiString(
-            self.allocator,
-            self.sourceBytesForToken(token),
-            .{
-                .start_column = token.calculateColumn(self.source, 8, null),
-                .diagnostics = .{ .diagnostics = self.diagnostics, .token = token },
-            },
-        );
-    }
-
     fn addErrorDetails(self: *Compiler, details: ErrorDetails) Allocator.Error!void {
         try self.diagnostics.append(details);
     }
@@ -3356,7 +3405,7 @@ test "StringTable" {
         }
         break :ids buf;
     };
-    var prng = std.Random.DefaultPrng.init(0);
+    var prng = std.rand.DefaultPrng.init(0);
     var random = prng.random();
     random.shuffle(u16, &ids);
 
src/resinator/errors.zig → lib/compiler/resinator/errors.zig
@@ -316,7 +316,7 @@ pub const ErrorDetails = struct {
         rc_would_miscompile_version_value_byte_count,
         code_page_pragma_in_included_file,
         nested_resource_level_exceeds_max,
-        too_many_dialog_controls,
+        too_many_dialog_controls_or_toolbar_buttons,
         nested_expression_level_exceeds_max,
         close_paren_expression,
         unary_plus_expression,
@@ -543,9 +543,15 @@ pub const ErrorDetails = struct {
                 .note => return writer.print("max {s} nesting level exceeded here", .{self.extra.resource.nameForErrorDisplay()}),
                 .hint => return,
             },
-            .too_many_dialog_controls => switch (self.type) {
-                .err, .warning => return writer.print("{s} contains too many controls (max is {})", .{ self.extra.resource.nameForErrorDisplay(), std.math.maxInt(u16) }),
-                .note => return writer.writeAll("maximum number of controls exceeded here"),
+            .too_many_dialog_controls_or_toolbar_buttons => switch (self.type) {
+                .err, .warning => return writer.print("{s} contains too many {s} (max is {})", .{ self.extra.resource.nameForErrorDisplay(), switch (self.extra.resource) {
+                    .toolbar => "buttons",
+                    else => "controls",
+                }, std.math.maxInt(u16) }),
+                .note => return writer.print("maximum number of {s} exceeded here", .{switch (self.extra.resource) {
+                    .toolbar => "buttons",
+                    else => "controls",
+                }}),
                 .hint => return,
             },
             .nested_expression_level_exceeds_max => switch (self.type) {
@@ -825,13 +831,13 @@ pub const ErrorDetails = struct {
 pub fn renderErrorMessage(allocator: std.mem.Allocator, writer: anytype, tty_config: std.io.tty.Config, cwd: std.fs.Dir, err_details: ErrorDetails, source: []const u8, strings: []const []const u8, source_mappings: ?SourceMappings) !void {
     if (err_details.type == .hint) return;
 
-    const source_line_start = err_details.token.getLineStart(source);
+    const source_line_start = err_details.token.getLineStartForErrorDisplay(source);
     // Treat tab stops as 1 column wide for error display purposes,
     // and add one to get a 1-based column
     const column = err_details.token.calculateColumn(source, 1, source_line_start) + 1;
 
-    const corresponding_span: ?SourceMappings.SourceSpan = if (source_mappings != null and source_mappings.?.has(err_details.token.line_number))
-        source_mappings.?.get(err_details.token.line_number)
+    const corresponding_span: ?SourceMappings.CorrespondingSpan = if (source_mappings) |mappings|
+        mappings.getCorrespondingSpan(err_details.token.line_number)
     else
         null;
     const corresponding_file: ?[]const u8 = if (source_mappings != null and corresponding_span != null)
@@ -877,7 +883,7 @@ pub fn renderErrorMessage(allocator: std.mem.Allocator, writer: anytype, tty_con
         return;
     }
 
-    const source_line = err_details.token.getLine(source, source_line_start);
+    const source_line = err_details.token.getLineForErrorDisplay(source, source_line_start);
     const visual_info = err_details.visualTokenInfo(source_line_start, source_line_start + source_line.len);
 
     // Need this to determine if the 'line originated from' note is worth printing
@@ -965,7 +971,7 @@ const CorrespondingLines = struct {
     lines: std.ArrayListUnmanaged(u8) = .{},
     lines_is_error_message: bool = false,
 
-    pub fn init(allocator: std.mem.Allocator, cwd: std.fs.Dir, err_details: ErrorDetails, lines_for_comparison: []const u8, corresponding_span: SourceMappings.SourceSpan, corresponding_file: []const u8) !CorrespondingLines {
+    pub fn init(allocator: std.mem.Allocator, cwd: std.fs.Dir, err_details: ErrorDetails, lines_for_comparison: []const u8, corresponding_span: SourceMappings.CorrespondingSpan, corresponding_file: []const u8) !CorrespondingLines {
         var corresponding_lines = CorrespondingLines{};
 
         // We don't do line comparison for this error, so don't print the note if the line
@@ -1035,17 +1041,27 @@ inline fn writeSourceByte(writer: anytype, byte: u8) !void {
 
 pub fn writeLinesFromStream(writer: anytype, input: anytype, start_line: usize, end_line: usize) !void {
     var line_num: usize = 1;
+    var last_byte: u8 = 0;
     while (try readByteOrEof(input)) |byte| {
         switch (byte) {
-            '\n' => {
-                if (line_num == end_line) return;
-                if (line_num >= start_line) try writeSourceByte(writer, byte);
-                line_num += 1;
+            '\n', '\r' => {
+                if (!utils.isLineEndingPair(last_byte, byte)) {
+                    if (line_num == end_line) return;
+                    if (line_num >= start_line) try writeSourceByte(writer, byte);
+                    line_num += 1;
+                } else {
+                    // reset last_byte to a non-line ending so that
+                    // consecutive CRLF pairs don't get treated as one
+                    // long line ending 'pair'
+                    last_byte = 0;
+                    continue;
+                }
             },
             else => {
                 if (line_num >= start_line) try writeSourceByte(writer, byte);
             },
         }
+        last_byte = byte;
     }
     if (line_num != end_line) {
         return error.LinesNotFound;
src/resinator/ico.zig → lib/compiler/resinator/ico.zig
File renamed without changes
src/resinator/lang.zig → lib/compiler/resinator/lang.zig
@@ -140,7 +140,7 @@ test "exhaustive tagToId" {
         writer.writeAll(parsed_sort.suffix.?) catch unreachable;
         const expected_field_name = comptime field: {
             var name_buf: [5]u8 = undefined;
-            @memcpy(&name_buf[0..parsed_sort.language_code.len], parsed_sort.language_code);
+            @memcpy(name_buf[0..parsed_sort.language_code.len], parsed_sort.language_code);
             name_buf[2] = '_';
             @memcpy(name_buf[3..], parsed_sort.country_code.?);
             break :field name_buf;
src/resinator/lex.zig → lib/compiler/resinator/lex.zig
@@ -71,7 +71,7 @@ pub const Token = struct {
 
     /// Returns 0-based column
     pub fn calculateColumn(token: Token, source: []const u8, tab_columns: usize, maybe_line_start: ?usize) usize {
-        const line_start = maybe_line_start orelse token.getLineStart(source);
+        const line_start = maybe_line_start orelse token.getLineStartForColumnCalc(source);
 
         var i: usize = line_start;
         var column: usize = 0;
@@ -81,13 +81,9 @@ pub const Token = struct {
         return column;
     }
 
-    // TODO: This doesn't necessarily match up with how we count line numbers, but where a line starts
-    //       has a knock-on effect on calculateColumn. More testing is needed to determine what needs
-    //       to be changed to make this both (1) match how line numbers are counted and (2) match how
-    //       the Win32 RC compiler counts tab columns.
-    //
+    // TODO: More testing is needed to determine if this can be merged with getLineStartForErrorDisplay
     //       (the TODO in currentIndexFormsLineEndingPair should be taken into account as well)
-    pub fn getLineStart(token: Token, source: []const u8) usize {
+    pub fn getLineStartForColumnCalc(token: Token, source: []const u8) usize {
         const line_start = line_start: {
             if (token.start != 0) {
                 // start checking at the byte before the token
@@ -102,14 +98,26 @@ pub const Token = struct {
         return line_start;
     }
 
-    pub fn getLine(token: Token, source: []const u8, maybe_line_start: ?usize) []const u8 {
-        const line_start = maybe_line_start orelse token.getLineStart(source);
+    pub fn getLineStartForErrorDisplay(token: Token, source: []const u8) usize {
+        const line_start = line_start: {
+            if (token.start != 0) {
+                // start checking at the byte before the token
+                var index = token.start - 1;
+                while (true) {
+                    if (source[index] == '\r' or source[index] == '\n') break :line_start @min(source.len - 1, index + 1);
+                    if (index != 0) index -= 1 else break;
+                }
+            }
+            break :line_start 0;
+        };
+        return line_start;
+    }
 
-        var line_end = line_start + 1;
-        if (line_end >= source.len or source[line_end] == '\n') return source[line_start..line_start];
-        while (line_end < source.len and source[line_end] != '\n') : (line_end += 1) {}
-        while (line_end > 0 and source[line_end - 1] == '\r') : (line_end -= 1) {}
+    pub fn getLineForErrorDisplay(token: Token, source: []const u8, maybe_line_start: ?usize) []const u8 {
+        const line_start = maybe_line_start orelse token.getLineStartForErrorDisplay(source);
 
+        var line_end = line_start;
+        while (line_end < source.len and source[line_end] != '\r' and source[line_end] != '\n') : (line_end += 1) {}
         return source[line_start..line_end];
     }
 
src/resinator/literals.zig → lib/compiler/resinator/literals.zig
@@ -98,6 +98,11 @@ pub const IterativeStringParser = struct {
 
     pub const ParsedCodepoint = struct {
         codepoint: u21,
+        /// Note: If this is true, `codepoint` will be a value with a max of maxInt(u16).
+        /// This is enforced by using saturating arithmetic, so in e.g. a wide string literal the
+        /// octal escape sequence \7777777 (2,097,151) will be parsed into the value 0xFFFF (65,535).
+        /// If the value needs to be truncated to a smaller integer (for ASCII string literals), then that
+        /// must be done by the caller.
         from_escaped_integer: bool = false,
     };
 
@@ -156,13 +161,14 @@ pub const IterativeStringParser = struct {
             .wide => 4,
         };
 
-        while (self.code_page.codepointAt(self.index, self.source)) |codepoint| : (self.index += codepoint.byte_len) {
+        var backtrack: bool = undefined;
+        while (self.code_page.codepointAt(self.index, self.source)) |codepoint| : ({
+            if (!backtrack) self.index += codepoint.byte_len;
+        }) {
+            backtrack = false;
             const c = codepoint.value;
-            var backtrack = false;
             defer {
-                if (backtrack) {
-                    self.index -= codepoint.byte_len;
-                } else {
+                if (!backtrack) {
                     if (c == '\t') {
                         self.column += columnsUntilTabStop(self.column, 8);
                     } else {
@@ -213,10 +219,12 @@ pub const IterativeStringParser = struct {
                 .newline => switch (c) {
                     '\r', ' ', '\t', '\n', '\x0b', '\x0c', '\xa0' => {},
                     else => {
-                        // backtrack so that we handle the current char properly
+                        // we intentionally avoid incrementing self.index
+                        // to handle the current char in the next call,
+                        // and we set backtrack so column count is handled correctly
                         backtrack = true;
+
                         // <space><newline>
-                        self.index += codepoint.byte_len;
                         self.pending_codepoint = '\n';
                         return .{ .codepoint = ' ' };
                     },
@@ -263,9 +271,10 @@ pub const IterativeStringParser = struct {
                             else => switch (self.declared_string_type) {
                                 .wide => {}, // invalid escape sequences are skipped in wide strings
                                 .ascii => {
-                                    // backtrack so that we handle the current char properly
+                                    // we intentionally avoid incrementing self.index
+                                    // to handle the current char in the next call,
+                                    // and we set backtrack so column count is handled correctly
                                     backtrack = true;
-                                    self.index += codepoint.byte_len;
                                     return .{ .codepoint = '\\' };
                                 },
                             },
@@ -277,9 +286,10 @@ pub const IterativeStringParser = struct {
                     '\r' => {},
                     '\n' => state = .escaped_newlines,
                     else => {
-                        // backtrack so that we handle the current char properly
+                        // we intentionally avoid incrementing self.index
+                        // to handle the current char in the next call,
+                        // and we set backtrack so column count is handled correctly
                         backtrack = true;
-                        self.index += codepoint.byte_len;
                         return .{ .codepoint = '\\' };
                     },
                 },
@@ -297,24 +307,18 @@ pub const IterativeStringParser = struct {
                         string_escape_n +%= std.fmt.charToDigit(@intCast(c), 8) catch unreachable;
                         string_escape_i += 1;
                         if (string_escape_i == max_octal_escape_digits) {
-                            const escaped_value = switch (self.declared_string_type) {
-                                .ascii => @as(u8, @truncate(string_escape_n)),
-                                .wide => string_escape_n,
-                            };
                             self.index += codepoint.byte_len;
-                            return .{ .codepoint = escaped_value, .from_escaped_integer = true };
+                            return .{ .codepoint = string_escape_n, .from_escaped_integer = true };
                         }
                     },
                     else => {
-                        // backtrack so that we handle the current char properly
+                        // we intentionally avoid incrementing self.index
+                        // to handle the current char in the next call,
+                        // and we set backtrack so column count is handled correctly
                         backtrack = true;
+
                         // write out whatever byte we have parsed so far
-                        const escaped_value = switch (self.declared_string_type) {
-                            .ascii => @as(u8, @truncate(string_escape_n)),
-                            .wide => string_escape_n,
-                        };
-                        self.index += codepoint.byte_len;
-                        return .{ .codepoint = escaped_value, .from_escaped_integer = true };
+                        return .{ .codepoint = string_escape_n, .from_escaped_integer = true };
                     },
                 },
                 .escaped_hex => switch (c) {
@@ -323,24 +327,19 @@ pub const IterativeStringParser = struct {
                         string_escape_n += std.fmt.charToDigit(@intCast(c), 16) catch unreachable;
                         string_escape_i += 1;
                         if (string_escape_i == max_hex_escape_digits) {
-                            const escaped_value = switch (self.declared_string_type) {
-                                .ascii => @as(u8, @truncate(string_escape_n)),
-                                .wide => string_escape_n,
-                            };
                             self.index += codepoint.byte_len;
-                            return .{ .codepoint = escaped_value, .from_escaped_integer = true };
+                            return .{ .codepoint = string_escape_n, .from_escaped_integer = true };
                         }
                     },
                     else => {
-                        // backtrack so that we handle the current char properly
+                        // we intentionally avoid incrementing self.index
+                        // to handle the current char in the next call,
+                        // and we set backtrack so column count is handled correctly
                         backtrack = true;
+
                         // write out whatever byte we have parsed so far
                         // (even with 0 actual digits, \x alone parses to 0)
-                        const escaped_value = switch (self.declared_string_type) {
-                            .ascii => @as(u8, @truncate(string_escape_n)),
-                            .wide => string_escape_n,
-                        };
-                        self.index += codepoint.byte_len;
+                        const escaped_value = string_escape_n;
                         return .{ .codepoint = escaped_value, .from_escaped_integer = true };
                     },
                 },
@@ -356,11 +355,7 @@ pub const IterativeStringParser = struct {
             },
             .escaped, .escaped_cr => return .{ .codepoint = '\\' },
             .escaped_octal, .escaped_hex => {
-                const escaped_value = switch (self.declared_string_type) {
-                    .ascii => @as(u8, @truncate(string_escape_n)),
-                    .wide => string_escape_n,
-                };
-                return .{ .codepoint = escaped_value, .from_escaped_integer = true };
+                return .{ .codepoint = string_escape_n, .from_escaped_integer = true };
             },
             .quote => unreachable, // this is a bug in the lexer
         }
@@ -395,7 +390,8 @@ pub fn parseQuotedString(
     while (try iterative_parser.next()) |parsed| {
         const c = parsed.codepoint;
         if (parsed.from_escaped_integer) {
-            try buf.append(std.mem.nativeToLittle(T, @intCast(c)));
+            // We truncate here to get the correct behavior for ascii strings
+            try buf.append(std.mem.nativeToLittle(T, @truncate(c)));
         } else {
             switch (literal_type) {
                 .ascii => switch (options.output_code_page) {
@@ -458,11 +454,6 @@ pub fn parseQuotedStringAsWideString(allocator: std.mem.Allocator, bytes: Source
     return parseQuotedString(.wide, allocator, bytes, options);
 }
 
-pub fn parseQuotedStringAsAsciiString(allocator: std.mem.Allocator, bytes: SourceBytes, options: StringParseOptions) ![]u8 {
-    std.debug.assert(bytes.slice.len >= 2); // ""
-    return parseQuotedString(.ascii, allocator, bytes, options);
-}
-
 test "parse quoted ascii string" {
     var arena_allocator = std.heap.ArenaAllocator.init(std.testing.allocator);
     defer arena_allocator.deinit();
@@ -651,6 +642,14 @@ test "parse quoted ascii string with utf8 code page" {
         .{ .slice = "\"\xF2\xAF\xBA\xB4\"", .code_page = .utf8 },
         .{ .output_code_page = .utf8 },
     ));
+
+    // This used to cause integer overflow when reconsuming the 4-byte long codepoint
+    // after the escaped CRLF pair.
+    try std.testing.expectEqualSlices(u8, "\u{10348}", try parseQuotedAsciiString(
+        arena,
+        .{ .slice = "\"\\\r\n\u{10348}\"", .code_page = .utf8 },
+        .{ .output_code_page = .utf8 },
+    ));
 }
 
 test "parse quoted wide string" {
lib/compiler/resinator/main.zig
@@ -0,0 +1,298 @@
+const std = @import("std");
+const builtin = @import("builtin");
+const removeComments = @import("comments.zig").removeComments;
+const parseAndRemoveLineCommands = @import("source_mapping.zig").parseAndRemoveLineCommands;
+const compile = @import("compile.zig").compile;
+const Diagnostics = @import("errors.zig").Diagnostics;
+const cli = @import("cli.zig");
+const preprocess = @import("preprocess.zig");
+const renderErrorMessage = @import("utils.zig").renderErrorMessage;
+const aro = @import("aro");
+
+pub fn main() !void {
+    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
+    defer std.debug.assert(gpa.deinit() == .ok);
+    const allocator = gpa.allocator();
+
+    const stderr = std.io.getStdErr();
+    const stderr_config = std.io.tty.detectConfig(stderr);
+
+    const args = try std.process.argsAlloc(allocator);
+    defer std.process.argsFree(allocator, args);
+
+    if (args.len < 2) {
+        try renderErrorMessage(stderr.writer(), stderr_config, .err, "expected zig lib dir as first argument", .{});
+        std.os.exit(1);
+    }
+    const zig_lib_dir = args[1];
+
+    var options = options: {
+        var cli_diagnostics = cli.Diagnostics.init(allocator);
+        defer cli_diagnostics.deinit();
+        var options = cli.parse(allocator, args[2..], &cli_diagnostics) catch |err| switch (err) {
+            error.ParseError => {
+                cli_diagnostics.renderToStdErr(args, stderr_config);
+                std.os.exit(1);
+            },
+            else => |e| return e,
+        };
+        try options.maybeAppendRC(std.fs.cwd());
+
+        // print any warnings/notes
+        cli_diagnostics.renderToStdErr(args, stderr_config);
+        // If there was something printed, then add an extra newline separator
+        // so that there is a clear separation between the cli diagnostics and whatever
+        // gets printed after
+        if (cli_diagnostics.errors.items.len > 0) {
+            try stderr.writeAll("\n");
+        }
+        break :options options;
+    };
+    defer options.deinit();
+
+    if (options.print_help_and_exit) {
+        try cli.writeUsage(stderr.writer(), "zig rc");
+        return;
+    }
+
+    const stdout_writer = std.io.getStdOut().writer();
+    if (options.verbose) {
+        try options.dumpVerbose(stdout_writer);
+        try stdout_writer.writeByte('\n');
+    }
+
+    var dependencies_list = std.ArrayList([]const u8).init(allocator);
+    defer {
+        for (dependencies_list.items) |item| {
+            allocator.free(item);
+        }
+        dependencies_list.deinit();
+    }
+    const maybe_dependencies_list: ?*std.ArrayList([]const u8) = if (options.depfile_path != null) &dependencies_list else null;
+
+    const full_input = full_input: {
+        if (options.preprocess != .no) {
+            var preprocessed_buf = std.ArrayList(u8).init(allocator);
+            errdefer preprocessed_buf.deinit();
+
+            // We're going to throw away everything except the final preprocessed output anyway,
+            // so we can use a scoped arena for everything else.
+            var aro_arena_state = std.heap.ArenaAllocator.init(allocator);
+            defer aro_arena_state.deinit();
+            const aro_arena = aro_arena_state.allocator();
+
+            const include_paths = getIncludePaths(aro_arena, options.auto_includes, zig_lib_dir) catch |err| switch (err) {
+                error.OutOfMemory => |e| return e,
+                else => |e| {
+                    switch (e) {
+                        error.MsvcIncludesNotFound => {
+                            try renderErrorMessage(stderr.writer(), stderr_config, .err, "MSVC include paths could not be automatically detected", .{});
+                        },
+                        error.MingwIncludesNotFound => {
+                            try renderErrorMessage(stderr.writer(), stderr_config, .err, "MinGW include paths could not be automatically detected", .{});
+                        },
+                    }
+                    try renderErrorMessage(stderr.writer(), stderr_config, .note, "to disable auto includes, use the option /:auto-includes none", .{});
+                    std.os.exit(1);
+                },
+            };
+
+            var comp = aro.Compilation.init(aro_arena);
+            defer comp.deinit();
+
+            var argv = std.ArrayList([]const u8).init(comp.gpa);
+            defer argv.deinit();
+
+            try argv.append("arocc"); // dummy command name
+            try preprocess.appendAroArgs(aro_arena, &argv, options, include_paths);
+            try argv.append(options.input_filename);
+
+            if (options.verbose) {
+                try stdout_writer.writeAll("Preprocessor: arocc (built-in)\n");
+                for (argv.items[0 .. argv.items.len - 1]) |arg| {
+                    try stdout_writer.print("{s} ", .{arg});
+                }
+                try stdout_writer.print("{s}\n\n", .{argv.items[argv.items.len - 1]});
+            }
+
+            preprocess.preprocess(&comp, preprocessed_buf.writer(), argv.items, maybe_dependencies_list) catch |err| switch (err) {
+                error.GeneratedSourceError => {
+                    // extra newline to separate this line from the aro errors
+                    try renderErrorMessage(stderr.writer(), stderr_config, .err, "failed during preprocessor setup (this is always a bug):\n", .{});
+                    aro.Diagnostics.render(&comp, stderr_config);
+                    std.os.exit(1);
+                },
+                // ArgError can occur if e.g. the .rc file is not found
+                error.ArgError, error.PreprocessError => {
+                    // extra newline to separate this line from the aro errors
+                    try renderErrorMessage(stderr.writer(), stderr_config, .err, "failed during preprocessing:\n", .{});
+                    aro.Diagnostics.render(&comp, stderr_config);
+                    std.os.exit(1);
+                },
+                error.StreamTooLong => {
+                    try renderErrorMessage(stderr.writer(), stderr_config, .err, "failed during preprocessing: maximum file size exceeded", .{});
+                    std.os.exit(1);
+                },
+                error.OutOfMemory => |e| return e,
+            };
+
+            break :full_input try preprocessed_buf.toOwnedSlice();
+        } else {
+            break :full_input std.fs.cwd().readFileAlloc(allocator, options.input_filename, std.math.maxInt(usize)) catch |err| {
+                try renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to read input file path '{s}': {s}", .{ options.input_filename, @errorName(err) });
+                std.os.exit(1);
+            };
+        }
+    };
+    defer allocator.free(full_input);
+
+    if (options.preprocess == .only) {
+        try std.fs.cwd().writeFile(options.output_filename, full_input);
+        return;
+    }
+
+    // Note: We still want to run this when no-preprocess is set because:
+    //   1. We want to print accurate line numbers after removing multiline comments
+    //   2. We want to be able to handle an already-preprocessed input with #line commands in it
+    var mapping_results = try parseAndRemoveLineCommands(allocator, full_input, full_input, .{ .initial_filename = options.input_filename });
+    defer mapping_results.mappings.deinit(allocator);
+
+    const final_input = removeComments(mapping_results.result, mapping_results.result, &mapping_results.mappings) catch |err| switch (err) {
+        error.InvalidSourceMappingCollapse => {
+            try renderErrorMessage(stderr.writer(), stderr_config, .err, "failed during comment removal; this is a known bug", .{});
+            std.os.exit(1);
+        },
+        else => |e| return e,
+    };
+
+    var output_file = std.fs.cwd().createFile(options.output_filename, .{}) catch |err| {
+        try renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to create output file '{s}': {s}", .{ options.output_filename, @errorName(err) });
+        std.os.exit(1);
+    };
+    var output_file_closed = false;
+    defer if (!output_file_closed) output_file.close();
+
+    var diagnostics = Diagnostics.init(allocator);
+    defer diagnostics.deinit();
+
+    var output_buffered_stream = std.io.bufferedWriter(output_file.writer());
+
+    compile(allocator, final_input, output_buffered_stream.writer(), .{
+        .cwd = std.fs.cwd(),
+        .diagnostics = &diagnostics,
+        .source_mappings = &mapping_results.mappings,
+        .dependencies_list = maybe_dependencies_list,
+        .ignore_include_env_var = options.ignore_include_env_var,
+        .extra_include_paths = options.extra_include_paths.items,
+        .default_language_id = options.default_language_id,
+        .default_code_page = options.default_code_page orelse .windows1252,
+        .verbose = options.verbose,
+        .null_terminate_string_table_strings = options.null_terminate_string_table_strings,
+        .max_string_literal_codepoints = options.max_string_literal_codepoints,
+        .silent_duplicate_control_ids = options.silent_duplicate_control_ids,
+        .warn_instead_of_error_on_invalid_code_page = options.warn_instead_of_error_on_invalid_code_page,
+    }) catch |err| switch (err) {
+        error.ParseError, error.CompileError => {
+            diagnostics.renderToStdErr(std.fs.cwd(), final_input, stderr_config, mapping_results.mappings);
+            // Delete the output file on error
+            output_file.close();
+            output_file_closed = true;
+            // Failing to delete is not really a big deal, so swallow any errors
+            std.fs.cwd().deleteFile(options.output_filename) catch {};
+            std.os.exit(1);
+        },
+        else => |e| return e,
+    };
+
+    try output_buffered_stream.flush();
+
+    // print any warnings/notes
+    diagnostics.renderToStdErr(std.fs.cwd(), final_input, stderr_config, mapping_results.mappings);
+
+    // write the depfile
+    if (options.depfile_path) |depfile_path| {
+        var depfile = std.fs.cwd().createFile(depfile_path, .{}) catch |err| {
+            try renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to create depfile '{s}': {s}", .{ depfile_path, @errorName(err) });
+            std.os.exit(1);
+        };
+        defer depfile.close();
+
+        const depfile_writer = depfile.writer();
+        var depfile_buffered_writer = std.io.bufferedWriter(depfile_writer);
+        switch (options.depfile_fmt) {
+            .json => {
+                var write_stream = std.json.writeStream(depfile_buffered_writer.writer(), .{ .whitespace = .indent_2 });
+                defer write_stream.deinit();
+
+                try write_stream.beginArray();
+                for (dependencies_list.items) |dep_path| {
+                    try write_stream.write(dep_path);
+                }
+                try write_stream.endArray();
+            },
+        }
+        try depfile_buffered_writer.flush();
+    }
+}
+
+fn getIncludePaths(arena: std.mem.Allocator, auto_includes_option: cli.Options.AutoIncludes, zig_lib_dir: []const u8) ![]const []const u8 {
+    var includes = auto_includes_option;
+    if (builtin.target.os.tag != .windows) {
+        switch (includes) {
+            // MSVC can't be found when the host isn't Windows, so short-circuit.
+            .msvc => return error.MsvcIncludesNotFound,
+            // Skip straight to gnu since we won't be able to detect MSVC on non-Windows hosts.
+            .any => includes = .gnu,
+            .none, .gnu => {},
+        }
+    }
+
+    while (true) {
+        switch (includes) {
+            .none => return &[_][]const u8{},
+            .any, .msvc => {
+                // MSVC is only detectable on Windows targets. This unreachable is to signify
+                // that .any and .msvc should be dealt with on non-Windows targets before this point,
+                // since getting MSVC include paths uses Windows-only APIs.
+                if (builtin.target.os.tag != .windows) unreachable;
+
+                const target_query: std.Target.Query = .{
+                    .os_tag = .windows,
+                    .abi = .msvc,
+                };
+                const target = std.zig.resolveTargetQueryOrFatal(target_query);
+                const is_native_abi = target_query.isNativeAbi();
+                const detected_libc = std.zig.LibCDirs.detect(arena, zig_lib_dir, target, is_native_abi, true, null) catch {
+                    if (includes == .any) {
+                        // fall back to mingw
+                        includes = .gnu;
+                        continue;
+                    }
+                    return error.MsvcIncludesNotFound;
+                };
+                if (detected_libc.libc_include_dir_list.len == 0) {
+                    if (includes == .any) {
+                        // fall back to mingw
+                        includes = .gnu;
+                        continue;
+                    }
+                    return error.MsvcIncludesNotFound;
+                }
+                return detected_libc.libc_include_dir_list;
+            },
+            .gnu => {
+                const target_query: std.Target.Query = .{
+                    .os_tag = .windows,
+                    .abi = .gnu,
+                };
+                const target = std.zig.resolveTargetQueryOrFatal(target_query);
+                const is_native_abi = target_query.isNativeAbi();
+                const detected_libc = std.zig.LibCDirs.detect(arena, zig_lib_dir, target, is_native_abi, true, null) catch |err| switch (err) {
+                    error.OutOfMemory => |e| return e,
+                    else => return error.MingwIncludesNotFound,
+                };
+                return detected_libc.libc_include_dir_list;
+            },
+        }
+    }
+}
src/resinator/parse.zig → lib/compiler/resinator/parse.zig
@@ -174,8 +174,6 @@ pub const Parser = struct {
                             } },
                         });
                     }
-                    // TODO: Wrapping this in a Node.Literal is superfluous but necessary
-                    //       to put it in a SimpleStatement
                     const value_node = try self.state.arena.create(Node.Literal);
                     value_node.* = .{
                         .token = value,
@@ -203,8 +201,6 @@ pub const Parser = struct {
                     const identifier = self.state.token;
                     try self.nextToken(.whitespace_delimiter_only);
                     try self.check(.literal);
-                    // TODO: Wrapping this in a Node.Literal is superfluous but necessary
-                    //       to put it in a SimpleStatement
                     const value_node = try self.state.arena.create(Node.Literal);
                     value_node.* = .{
                         .token = self.state.token,
@@ -539,12 +535,12 @@ pub const Parser = struct {
                     // be able to be written into the relevant field in the .res data.
                     if (controls.items.len >= std.math.maxInt(u16)) {
                         try self.addErrorDetails(.{
-                            .err = .too_many_dialog_controls,
+                            .err = .too_many_dialog_controls_or_toolbar_buttons,
                             .token = id_token,
                             .extra = .{ .resource = resource },
                         });
                         return self.addErrorDetailsAndFail(.{
-                            .err = .too_many_dialog_controls,
+                            .err = .too_many_dialog_controls_or_toolbar_buttons,
                             .type = .note,
                             .token = control_node.getFirstToken(),
                             .token_span_end = control_node.getLastToken(),
@@ -592,8 +588,26 @@ pub const Parser = struct {
                 try self.check(.begin);
 
                 var buttons = std.ArrayListUnmanaged(*Node){};
+                defer buttons.deinit(self.state.allocator);
                 while (try self.parseToolbarButtonStatement()) |button_node| {
-                    try buttons.append(self.state.arena, button_node);
+                    // The number of buttons must fit in a u16 in order for it to
+                    // be able to be written into the relevant field in the .res data.
+                    if (buttons.items.len >= std.math.maxInt(u16)) {
+                        try self.addErrorDetails(.{
+                            .err = .too_many_dialog_controls_or_toolbar_buttons,
+                            .token = id_token,
+                            .extra = .{ .resource = resource },
+                        });
+                        return self.addErrorDetailsAndFail(.{
+                            .err = .too_many_dialog_controls_or_toolbar_buttons,
+                            .type = .note,
+                            .token = button_node.getFirstToken(),
+                            .token_span_end = button_node.getLastToken(),
+                            .extra = .{ .resource = resource },
+                        });
+                    }
+
+                    try buttons.append(self.state.allocator, button_node);
                 }
 
                 try self.nextToken(.normal);
@@ -608,7 +622,7 @@ pub const Parser = struct {
                     .button_width = button_width,
                     .button_height = button_height,
                     .begin_token = begin_token,
-                    .buttons = try buttons.toOwnedSlice(self.state.arena),
+                    .buttons = try self.state.arena.dupe(*Node, buttons.items),
                     .end_token = end_token,
                 };
                 return &node.base;
lib/compiler/resinator/preprocess.zig
@@ -0,0 +1,140 @@
+const std = @import("std");
+const builtin = @import("builtin");
+const Allocator = std.mem.Allocator;
+const cli = @import("cli.zig");
+const aro = @import("aro");
+
+const PreprocessError = error{ ArgError, GeneratedSourceError, PreprocessError, StreamTooLong, OutOfMemory };
+
+pub fn preprocess(
+    comp: *aro.Compilation,
+    writer: anytype,
+    /// Expects argv[0] to be the command name
+    argv: []const []const u8,
+    maybe_dependencies_list: ?*std.ArrayList([]const u8),
+) PreprocessError!void {
+    try comp.addDefaultPragmaHandlers();
+
+    var driver: aro.Driver = .{ .comp = comp, .aro_name = "arocc" };
+    defer driver.deinit();
+
+    var macro_buf = std.ArrayList(u8).init(comp.gpa);
+    defer macro_buf.deinit();
+
+    _ = driver.parseArgs(std.io.null_writer, macro_buf.writer(), argv) catch |err| switch (err) {
+        error.FatalError => return error.ArgError,
+        error.OutOfMemory => |e| return e,
+    };
+
+    if (hasAnyErrors(comp)) return error.ArgError;
+
+    // .include_system_defines gives us things like _WIN32
+    const builtin_macros = comp.generateBuiltinMacros(.include_system_defines) catch |err| switch (err) {
+        error.FatalError => return error.GeneratedSourceError,
+        else => |e| return e,
+    };
+    const user_macros = comp.addSourceFromBuffer("<command line>", macro_buf.items) catch |err| switch (err) {
+        error.FatalError => return error.GeneratedSourceError,
+        else => |e| return e,
+    };
+    const source = driver.inputs.items[0];
+
+    if (hasAnyErrors(comp)) return error.GeneratedSourceError;
+
+    comp.generated_buf.items.len = 0;
+    var pp = try aro.Preprocessor.initDefault(comp);
+    defer pp.deinit();
+
+    if (comp.langopts.ms_extensions) {
+        comp.ms_cwd_source_id = source.id;
+    }
+
+    pp.preserve_whitespace = true;
+    pp.linemarkers = .line_directives;
+
+    pp.preprocessSources(&.{ source, builtin_macros, user_macros }) catch |err| switch (err) {
+        error.FatalError => return error.PreprocessError,
+        else => |e| return e,
+    };
+
+    if (hasAnyErrors(comp)) return error.PreprocessError;
+
+    try pp.prettyPrintTokens(writer);
+
+    if (maybe_dependencies_list) |dependencies_list| {
+        for (comp.sources.values()) |comp_source| {
+            if (comp_source.id == builtin_macros.id or comp_source.id == user_macros.id) continue;
+            if (comp_source.id == .unused or comp_source.id == .generated) continue;
+            const duped_path = try dependencies_list.allocator.dupe(u8, comp_source.path);
+            errdefer dependencies_list.allocator.free(duped_path);
+            try dependencies_list.append(duped_path);
+        }
+    }
+}
+
+fn hasAnyErrors(comp: *aro.Compilation) bool {
+    // In theory we could just check Diagnostics.errors != 0, but that only
+    // gets set during rendering of the error messages, see:
+    // https://github.com/Vexu/arocc/issues/603
+    for (comp.diagnostics.list.items) |msg| {
+        switch (msg.kind) {
+            .@"fatal error", .@"error" => return true,
+            else => {},
+        }
+    }
+    return false;
+}
+
+/// `arena` is used for temporary -D argument strings and the INCLUDE environment variable.
+/// The arena should be kept alive at least as long as `argv`.
+pub fn appendAroArgs(arena: Allocator, argv: *std.ArrayList([]const u8), options: cli.Options, system_include_paths: []const []const u8) !void {
+    try argv.appendSlice(&.{
+        "-E",
+        "--comments",
+        "-fuse-line-directives",
+        "--target=x86_64-windows-msvc",
+        "--emulate=msvc",
+        "-nostdinc",
+        "-DRC_INVOKED",
+    });
+    for (options.extra_include_paths.items) |extra_include_path| {
+        try argv.append("-I");
+        try argv.append(extra_include_path);
+    }
+
+    for (system_include_paths) |include_path| {
+        try argv.append("-isystem");
+        try argv.append(include_path);
+    }
+
+    if (!options.ignore_include_env_var) {
+        const INCLUDE = std.process.getEnvVarOwned(arena, "INCLUDE") catch "";
+
+        // The only precedence here is llvm-rc which also uses the platform-specific
+        // delimiter. There's no precedence set by `rc.exe` since it's Windows-only.
+        const delimiter = switch (builtin.os.tag) {
+            .windows => ';',
+            else => ':',
+        };
+        var it = std.mem.tokenizeScalar(u8, INCLUDE, delimiter);
+        while (it.next()) |include_path| {
+            try argv.append("-isystem");
+            try argv.append(include_path);
+        }
+    }
+
+    var symbol_it = options.symbols.iterator();
+    while (symbol_it.next()) |entry| {
+        switch (entry.value_ptr.*) {
+            .define => |value| {
+                try argv.append("-D");
+                const define_arg = try std.fmt.allocPrint(arena, "{s}={s}", .{ entry.key_ptr.*, value });
+                try argv.append(define_arg);
+            },
+            .undefine => {
+                try argv.append("-U");
+                try argv.append(entry.key_ptr.*);
+            },
+        }
+    }
+}
src/resinator/rc.zig → lib/compiler/resinator/rc.zig
File renamed without changes
src/resinator/res.zig → lib/compiler/resinator/res.zig
@@ -608,7 +608,7 @@ const AcceleratorKeyCodepointTranslator = struct {
         const parsed = maybe_parsed orelse return null;
         if (parsed.codepoint == Codepoint.invalid) return 0xFFFD;
         if (parsed.from_escaped_integer and self.string_type == .ascii) {
-            return windows1252.toCodepoint(@intCast(parsed.codepoint));
+            return windows1252.toCodepoint(@truncate(parsed.codepoint));
         }
         return parsed.codepoint;
     }
src/resinator/source_mapping.zig → lib/compiler/resinator/source_mapping.zig
@@ -1,8 +1,7 @@
 const std = @import("std");
 const Allocator = std.mem.Allocator;
-const UncheckedSliceWriter = @import("utils.zig").UncheckedSliceWriter;
-const parseQuotedAsciiString = @import("literals.zig").parseQuotedAsciiString;
-const lex = @import("lex.zig");
+const utils = @import("utils.zig");
+const UncheckedSliceWriter = utils.UncheckedSliceWriter;
 
 pub const ParseLineCommandsResult = struct {
     result: []u8,
@@ -79,8 +78,9 @@ pub fn parseAndRemoveLineCommands(allocator: Allocator, source: []const u8, buf:
                 },
                 '\r', '\n' => {
                     const is_crlf = formsLineEndingPair(source, c, index + 1);
-                    try handleLineEnd(allocator, line_number, &parse_result.mappings, &current_mapping);
                     if (!current_mapping.ignore_contents) {
+                        try handleLineEnd(allocator, line_number, &parse_result.mappings, &current_mapping);
+
                         result.write(c);
                         if (is_crlf) result.write(source[index + 1]);
                         line_number += 1;
@@ -115,8 +115,9 @@ pub fn parseAndRemoveLineCommands(allocator: Allocator, source: []const u8, buf:
                     if (std.mem.startsWith(u8, preprocessor_str, "#line")) {
                         try handleLineCommand(allocator, preprocessor_str, &current_mapping);
                     } else {
-                        try handleLineEnd(allocator, line_number, &parse_result.mappings, &current_mapping);
                         if (!current_mapping.ignore_contents) {
+                            try handleLineEnd(allocator, line_number, &parse_result.mappings, &current_mapping);
+
                             const line_ending_len: usize = if (is_crlf) 2 else 1;
                             result.writeSlice(source[pending_start.? .. index + line_ending_len]);
                             line_number += 1;
@@ -131,8 +132,9 @@ pub fn parseAndRemoveLineCommands(allocator: Allocator, source: []const u8, buf:
             .non_preprocessor => switch (c) {
                 '\r', '\n' => {
                     const is_crlf = formsLineEndingPair(source, c, index + 1);
-                    try handleLineEnd(allocator, line_number, &parse_result.mappings, &current_mapping);
                     if (!current_mapping.ignore_contents) {
+                        try handleLineEnd(allocator, line_number, &parse_result.mappings, &current_mapping);
+
                         result.write(c);
                         if (is_crlf) result.write(source[index + 1]);
                         line_number += 1;
@@ -185,7 +187,7 @@ pub fn parseAndRemoveLineCommands(allocator: Allocator, source: []const u8, buf:
     // If there have been no line mappings at all, then we're dealing with an empty file.
     // In this case, we want to fake a line mapping just so that we return something
     // that is useable in the same way that a non-empty mapping would be.
-    if (parse_result.mappings.mapping.items.len == 0) {
+    if (parse_result.mappings.sources.root == null) {
         try handleLineEnd(allocator, line_number, &parse_result.mappings, &current_mapping);
     }
 
@@ -197,22 +199,13 @@ pub fn formsLineEndingPair(source: []const u8, line_ending: u8, next_index: usiz
     if (next_index >= source.len) return false;
 
     const next_ending = source[next_index];
-    if (next_ending != '\r' and next_ending != '\n') return false;
-
-    // can't be \n\n or \r\r
-    if (line_ending == next_ending) return false;
-
-    return true;
+    return utils.isLineEndingPair(line_ending, next_ending);
 }
 
 pub fn handleLineEnd(allocator: Allocator, post_processed_line_number: usize, mapping: *SourceMappings, current_mapping: *CurrentMapping) !void {
     const filename_offset = try mapping.files.put(allocator, current_mapping.filename.items);
 
-    try mapping.set(allocator, post_processed_line_number, .{
-        .start_line = current_mapping.line_num,
-        .end_line = current_mapping.line_num,
-        .filename_offset = filename_offset,
-    });
+    try mapping.set(post_processed_line_number, current_mapping.line_num, filename_offset);
 
     current_mapping.line_num += 1;
     current_mapping.pending = false;
@@ -421,72 +414,192 @@ test parseFilename {
 }
 
 pub const SourceMappings = struct {
-    /// line number -> span where the index is (line number - 1)
-    mapping: std.ArrayListUnmanaged(SourceSpan) = .{},
+    sources: Sources = .{},
     files: StringTable = .{},
     /// The default assumes that the first filename added is the root file.
     /// The value should be set to the correct offset if that assumption does not hold.
     root_filename_offset: u32 = 0,
+    source_node_pool: std.heap.MemoryPool(Sources.Node) = std.heap.MemoryPool(Sources.Node).init(std.heap.page_allocator),
+    end_line: usize = 0,
+
+    const sourceCompare = struct {
+        fn compare(a: Source, b: Source) std.math.Order {
+            return std.math.order(a.start_line, b.start_line);
+        }
+    }.compare;
+    const Sources = std.Treap(Source, sourceCompare);
 
-    pub const SourceSpan = struct {
+    pub const Source = struct {
         start_line: usize,
-        end_line: usize,
+        span: usize = 0,
+        corresponding_start_line: usize,
         filename_offset: u32,
     };
 
     pub fn deinit(self: *SourceMappings, allocator: Allocator) void {
         self.files.deinit(allocator);
-        self.mapping.deinit(allocator);
+        self.source_node_pool.deinit();
     }
 
-    pub fn set(self: *SourceMappings, allocator: Allocator, line_num: usize, span: SourceSpan) !void {
-        const ptr = try self.expandAndGet(allocator, line_num);
-        ptr.* = span;
-    }
+    /// Find the node that 'contains' the `line`, i.e. the node's start_line is
+    /// >= `line`
+    fn findNode(self: SourceMappings, line: usize) ?*Sources.Node {
+        var node = self.sources.root;
+        var last_gt: ?*Sources.Node = null;
+
+        var search_key: Source = undefined;
+        search_key.start_line = line;
+        while (node) |current| {
+            const order = sourceCompare(search_key, current.key);
+            if (order == .eq) break;
+            if (order == .gt) last_gt = current;
+
+            node = current.children[@intFromBool(order == .gt)] orelse {
+                // Regardless of the current order, last_gt will contain the
+                // the node we want to return.
+                //
+                // If search key is > current node's key, then last_gt will be
+                // current which we now know is the closest node that is <=
+                // the search key.
+                //
+                //
+                // If the key is < current node's key, we want to jump back to the
+                // node that the search key was most recently greater than.
+                // This is necessary for scenarios like (where the search key is 2):
+                //
+                //   1
+                //    \
+                //     6
+                //    /
+                //   3
+                //
+                // In this example, we'll get down to the '3' node but ultimately want
+                // to return the '1' node.
+                //
+                // Note: If we've never seen a key that the search key is greater than,
+                // then we know that there's no valid node, so last_gt will be null.
+                return last_gt;
+            };
+        }
 
-    pub fn has(self: SourceMappings, line_num: usize) bool {
-        return self.mapping.items.len >= line_num;
+        return node;
     }
 
-    /// Note: `line_num` is 1-indexed
-    pub fn get(self: SourceMappings, line_num: usize) SourceSpan {
-        return self.mapping.items[line_num - 1];
-    }
+    /// Note: `line_num` and `corresponding_line_num` start at 1
+    pub fn set(self: *SourceMappings, line_num: usize, corresponding_line_num: usize, filename_offset: u32) !void {
+        const maybe_node = self.findNode(line_num);
 
-    pub fn getPtr(self: SourceMappings, line_num: usize) *SourceSpan {
-        return &self.mapping.items[line_num - 1];
+        const need_new_node = need_new_node: {
+            if (maybe_node) |node| {
+                if (node.key.filename_offset != filename_offset) {
+                    break :need_new_node true;
+                }
+                const exist_delta = @as(i64, @intCast(node.key.corresponding_start_line)) - @as(i64, @intCast(node.key.start_line));
+                const cur_delta = @as(i64, @intCast(corresponding_line_num)) - @as(i64, @intCast(line_num));
+                if (exist_delta != cur_delta) {
+                    break :need_new_node true;
+                }
+                break :need_new_node false;
+            }
+            break :need_new_node true;
+        };
+        if (need_new_node) {
+            // spans must not overlap
+            if (maybe_node) |node| {
+                std.debug.assert(node.key.start_line != line_num);
+            }
+
+            const key = Source{
+                .start_line = line_num,
+                .corresponding_start_line = corresponding_line_num,
+                .filename_offset = filename_offset,
+            };
+            var entry = self.sources.getEntryFor(key);
+            var new_node = try self.source_node_pool.create();
+            new_node.key = key;
+            entry.set(new_node);
+        }
+        if (line_num > self.end_line) {
+            self.end_line = line_num;
+        }
     }
 
-    /// Expands the number of lines in the mapping to include the requested
-    /// line number (if necessary) and returns a pointer to the value at that
-    /// line number.
-    ///
-    /// Note: `line_num` is 1-indexed
-    pub fn expandAndGet(self: *SourceMappings, allocator: Allocator, line_num: usize) !*SourceSpan {
-        try self.mapping.resize(allocator, line_num);
-        return &self.mapping.items[line_num - 1];
+    /// Note: `line_num` starts at 1
+    pub fn get(self: SourceMappings, line_num: usize) ?Source {
+        const node = self.findNode(line_num) orelse return null;
+        return node.key;
     }
 
-    pub fn collapse(self: *SourceMappings, line_num: usize, num_following_lines_to_collapse: usize) void {
-        std.debug.assert(num_following_lines_to_collapse > 0);
+    pub const CorrespondingSpan = struct {
+        start_line: usize,
+        end_line: usize,
+        filename_offset: u32,
+    };
 
-        var span_to_collapse_into = self.getPtr(line_num);
-        const last_collapsed_span = self.get(line_num + num_following_lines_to_collapse);
-        span_to_collapse_into.end_line = last_collapsed_span.end_line;
+    pub fn getCorrespondingSpan(self: SourceMappings, line_num: usize) ?CorrespondingSpan {
+        const source = self.get(line_num) orelse return null;
+        const diff = line_num - source.start_line;
+        const start_line = source.corresponding_start_line + (if (line_num == source.start_line) 0 else source.span + diff);
+        const end_line = start_line + (if (line_num == source.start_line) source.span else 0);
+        return CorrespondingSpan{
+            .start_line = start_line,
+            .end_line = end_line,
+            .filename_offset = source.filename_offset,
+        };
+    }
 
-        const after_collapsed_start = line_num + num_following_lines_to_collapse;
-        const new_num_lines = self.mapping.items.len - num_following_lines_to_collapse;
-        std.mem.copyForwards(SourceSpan, self.mapping.items[line_num..new_num_lines], self.mapping.items[after_collapsed_start..]);
+    pub fn collapse(self: *SourceMappings, line_num: usize, num_following_lines_to_collapse: usize) !void {
+        std.debug.assert(num_following_lines_to_collapse > 0);
+        var node = self.findNode(line_num).?;
+        const span_diff = num_following_lines_to_collapse;
+        if (node.key.start_line != line_num) {
+            const offset = line_num - node.key.start_line;
+            const key = Source{
+                .start_line = line_num,
+                .span = num_following_lines_to_collapse,
+                .corresponding_start_line = node.key.corresponding_start_line + node.key.span + offset,
+                .filename_offset = node.key.filename_offset,
+            };
+            var entry = self.sources.getEntryFor(key);
+            var new_node = try self.source_node_pool.create();
+            new_node.key = key;
+            entry.set(new_node);
+            node = new_node;
+        } else {
+            node.key.span += span_diff;
+        }
 
-        self.mapping.items.len = new_num_lines;
+        // now subtract the span diff from the start line number of all of
+        // the following nodes in order
+        var it = Sources.InorderIterator{
+            .current = node,
+            .previous = node.children[0],
+        };
+        // skip past current, but store it
+        var prev = it.next().?;
+        while (it.next()) |inorder_node| {
+            inorder_node.key.start_line -= span_diff;
+
+            // This can only really happen if there are #line commands within
+            // a multiline comment, which in theory should be skipped over.
+            // However, currently, parseAndRemoveLineCommands is not aware of
+            // comments at all.
+            //
+            // TODO: Make parseAndRemoveLineCommands aware of comments/strings
+            //       and turn this into an assertion
+            if (prev.key.start_line > inorder_node.key.start_line) {
+                return error.InvalidSourceMappingCollapse;
+            }
+            prev = inorder_node;
+        }
+        self.end_line -= span_diff;
     }
 
     /// Returns true if the line is from the main/root file (i.e. not a file that has been
     /// `#include`d).
     pub fn isRootFile(self: *SourceMappings, line_num: usize) bool {
-        const line_mapping = self.get(line_num);
-        if (line_mapping.filename_offset == self.root_filename_offset) return true;
-        return false;
+        const source = self.get(line_num) orelse return false;
+        return source.filename_offset == self.root_filename_offset;
     }
 };
 
@@ -497,16 +610,21 @@ test "SourceMappings collapse" {
     defer mappings.deinit(allocator);
     const filename_offset = try mappings.files.put(allocator, "test.rc");
 
-    try mappings.set(allocator, 1, .{ .start_line = 1, .end_line = 1, .filename_offset = filename_offset });
-    try mappings.set(allocator, 2, .{ .start_line = 2, .end_line = 3, .filename_offset = filename_offset });
-    try mappings.set(allocator, 3, .{ .start_line = 4, .end_line = 4, .filename_offset = filename_offset });
-    try mappings.set(allocator, 4, .{ .start_line = 5, .end_line = 5, .filename_offset = filename_offset });
-
-    mappings.collapse(1, 2);
-
-    try std.testing.expectEqual(@as(usize, 2), mappings.mapping.items.len);
-    try std.testing.expectEqual(@as(usize, 4), mappings.mapping.items[0].end_line);
-    try std.testing.expectEqual(@as(usize, 5), mappings.mapping.items[1].end_line);
+    try mappings.set(1, 1, filename_offset);
+    try mappings.set(5, 5, filename_offset);
+
+    try mappings.collapse(2, 2);
+
+    try std.testing.expectEqual(@as(usize, 3), mappings.end_line);
+    const span_1 = mappings.getCorrespondingSpan(1).?;
+    try std.testing.expectEqual(@as(usize, 1), span_1.start_line);
+    try std.testing.expectEqual(@as(usize, 1), span_1.end_line);
+    const span_2 = mappings.getCorrespondingSpan(2).?;
+    try std.testing.expectEqual(@as(usize, 2), span_2.start_line);
+    try std.testing.expectEqual(@as(usize, 4), span_2.end_line);
+    const span_3 = mappings.getCorrespondingSpan(3).?;
+    try std.testing.expectEqual(@as(usize, 5), span_3.start_line);
+    try std.testing.expectEqual(@as(usize, 5), span_3.end_line);
 }
 
 /// Same thing as StringTable in Zig's src/Wasm.zig
@@ -579,10 +697,11 @@ fn testParseAndRemoveLineCommands(
             std.debug.print("{}: {s}:{}-{}\n", .{ line_num, span.filename, span.start_line, span.end_line });
         }
         std.debug.print("\nactual mappings:\n", .{});
-        for (results.mappings.mapping.items, 0..) |span, i| {
-            const line_num = i + 1;
+        var i: usize = 1;
+        while (i <= results.mappings.end_line) : (i += 1) {
+            const span = results.mappings.getCorrespondingSpan(i).?;
             const filename = results.mappings.files.get(span.filename_offset);
-            std.debug.print("{}: {s}:{}-{}\n", .{ line_num, filename, span.start_line, span.end_line });
+            std.debug.print("{}: {s}:{}-{}\n", .{ i, filename, span.start_line, span.end_line });
         }
         std.debug.print("\n", .{});
         return err;
@@ -590,10 +709,10 @@ fn testParseAndRemoveLineCommands(
 }
 
 fn expectEqualMappings(expected_spans: []const ExpectedSourceSpan, mappings: SourceMappings) !void {
-    try std.testing.expectEqual(expected_spans.len, mappings.mapping.items.len);
+    try std.testing.expectEqual(expected_spans.len, mappings.end_line);
     for (expected_spans, 0..) |expected_span, i| {
         const line_num = i + 1;
-        const span = mappings.get(line_num);
+        const span = mappings.getCorrespondingSpan(line_num) orelse return error.MissingLineNum;
         const filename = mappings.files.get(span.filename_offset);
         try std.testing.expectEqual(expected_span.start_line, span.start_line);
         try std.testing.expectEqual(expected_span.end_line, span.end_line);
@@ -685,3 +804,28 @@ test "in place" {
     defer result.mappings.deinit(std.testing.allocator);
     try std.testing.expectEqualStrings("", result.result);
 }
+
+test "line command within a multiline comment" {
+    // TODO: Enable once parseAndRemoveLineCommands is comment-aware
+    if (true) return error.SkipZigTest;
+
+    try testParseAndRemoveLineCommands(
+        \\/*
+        \\#line 1 "irrelevant.rc"
+        \\
+        \\
+        \\*/
+    , &[_]ExpectedSourceSpan{
+        .{ .start_line = 1, .end_line = 1, .filename = "blah.rc" },
+        .{ .start_line = 2, .end_line = 2, .filename = "blah.rc" },
+        .{ .start_line = 3, .end_line = 3, .filename = "blah.rc" },
+        .{ .start_line = 4, .end_line = 4, .filename = "blah.rc" },
+        .{ .start_line = 5, .end_line = 5, .filename = "blah.rc" },
+    },
+        \\/*
+        \\#line 1 "irrelevant.rc"
+        \\
+        \\
+        \\*/
+    , .{ .initial_filename = "blah.rc" });
+}
src/resinator/utils.zig → lib/compiler/resinator/utils.zig
@@ -110,3 +110,13 @@ pub fn renderErrorMessage(writer: anytype, config: std.io.tty.Config, msg_type:
     try writer.writeByte('\n');
     try config.setColor(writer, .reset);
 }
+
+pub fn isLineEndingPair(first: u8, second: u8) bool {
+    if (first != '\r' and first != '\n') return false;
+    if (second != '\r' and second != '\n') return false;
+
+    // can't be \n\n or \r\r
+    if (first == second) return false;
+
+    return true;
+}
src/resinator/windows1252.zig → lib/compiler/resinator/windows1252.zig
File renamed without changes
src/resinator/preprocess.zig
@@ -1,100 +0,0 @@
-const std = @import("std");
-const builtin = @import("builtin");
-const Allocator = std.mem.Allocator;
-const cli = @import("cli.zig");
-
-pub const IncludeArgs = struct {
-    clang_target: ?[]const u8 = null,
-    system_include_paths: []const []const u8,
-    /// Should be set to `true` when -target has the GNU abi
-    /// (either because `clang_target` has `-gnu` or `-target`
-    /// is appended via other means and it has `-gnu`)
-    needs_gnu_workaround: bool = false,
-    nostdinc: bool = false,
-
-    pub const IncludeAbi = enum {
-        msvc,
-        gnu,
-    };
-};
-
-/// `arena` is used for temporary -D argument strings and the INCLUDE environment variable.
-/// The arena should be kept alive at least as long as `argv`.
-pub fn appendClangArgs(arena: Allocator, argv: *std.ArrayList([]const u8), options: cli.Options, include_args: IncludeArgs) !void {
-    try argv.appendSlice(&[_][]const u8{
-        "-E", // preprocessor only
-        "--comments",
-        "-fuse-line-directives", // #line <num> instead of # <num>
-        // TODO: could use --trace-includes to give info about what's included from where
-        "-xc", // output c
-        // TODO: Turn this off, check the warnings, and convert the spaces back to NUL
-        "-Werror=null-character", // error on null characters instead of converting them to spaces
-        // TODO: could remove -Werror=null-character and instead parse warnings looking for 'warning: null character ignored'
-        //       since the only real problem is when clang doesn't preserve null characters
-        //"-Werror=invalid-pp-token", // will error on unfinished string literals
-        // TODO: could use -Werror instead
-        "-fms-compatibility", // Allow things like "header.h" to be resolved relative to the 'root' .rc file, among other things
-        // https://learn.microsoft.com/en-us/windows/win32/menurc/predefined-macros
-        "-DRC_INVOKED",
-    });
-    for (options.extra_include_paths.items) |extra_include_path| {
-        try argv.append("-I");
-        try argv.append(extra_include_path);
-    }
-
-    if (include_args.nostdinc) {
-        try argv.append("-nostdinc");
-    }
-    for (include_args.system_include_paths) |include_path| {
-        try argv.append("-isystem");
-        try argv.append(include_path);
-    }
-    if (include_args.clang_target) |target| {
-        try argv.append("-target");
-        try argv.append(target);
-    }
-    // Using -fms-compatibility and targeting the GNU abi interact in a strange way:
-    // - Targeting the GNU abi stops _MSC_VER from being defined
-    // - Passing -fms-compatibility stops __GNUC__ from being defined
-    // Neither being defined is a problem for things like MinGW's vadefs.h,
-    // which will fail during preprocessing if neither are defined.
-    // So, when targeting the GNU abi, we need to force __GNUC__ to be defined.
-    //
-    // TODO: This is a workaround that should be removed if possible.
-    if (include_args.needs_gnu_workaround) {
-        // This is the same default gnuc version that Clang uses:
-        // https://github.com/llvm/llvm-project/blob/4b5366c9512aa273a5272af1d833961e1ed156e7/clang/lib/Driver/ToolChains/Clang.cpp#L6738
-        try argv.append("-fgnuc-version=4.2.1");
-    }
-
-    if (!options.ignore_include_env_var) {
-        const INCLUDE = std.process.getEnvVarOwned(arena, "INCLUDE") catch "";
-
-        // The only precedence here is llvm-rc which also uses the platform-specific
-        // delimiter. There's no precedence set by `rc.exe` since it's Windows-only.
-        const delimiter = switch (builtin.os.tag) {
-            .windows => ';',
-            else => ':',
-        };
-        var it = std.mem.tokenizeScalar(u8, INCLUDE, delimiter);
-        while (it.next()) |include_path| {
-            try argv.append("-isystem");
-            try argv.append(include_path);
-        }
-    }
-
-    var symbol_it = options.symbols.iterator();
-    while (symbol_it.next()) |entry| {
-        switch (entry.value_ptr.*) {
-            .define => |value| {
-                try argv.append("-D");
-                const define_arg = try std.fmt.allocPrint(arena, "{s}={s}", .{ entry.key_ptr.*, value });
-                try argv.append(define_arg);
-            },
-            .undefine => {
-                try argv.append("-U");
-                try argv.append(entry.key_ptr.*);
-            },
-        }
-    }
-}
src/Compilation.zig
@@ -36,7 +36,6 @@ const Cache = std.Build.Cache;
 const c_codegen = @import("codegen/c.zig");
 const libtsan = @import("libtsan.zig");
 const Zir = std.zig.Zir;
-const resinator = @import("resinator.zig");
 const Builtin = @import("Builtin.zig");
 const LlvmObject = @import("codegen/llvm.zig").Object;
 
@@ -174,7 +173,7 @@ local_cache_directory: Directory,
 global_cache_directory: Directory,
 libc_include_dir_list: []const []const u8,
 libc_framework_dir_list: []const []const u8,
-rc_include_dir_list: []const []const u8,
+rc_includes: RcIncludes,
 thread_pool: *ThreadPool,
 
 /// Populated when we build the libc++ static library. A Job to build this is placed in the queue
@@ -1243,68 +1242,6 @@ pub fn create(gpa: Allocator, arena: Allocator, options: CreateOptions) !*Compil
             options.libc_installation,
         );
 
-        // The include directories used when preprocessing .rc files are separate from the
-        // target. Which include directories are used is determined by `options.rc_includes`.
-        //
-        // Note: It should be okay that the include directories used when compiling .rc
-        // files differ from the include directories used when compiling the main
-        // binary, since the .res format is not dependent on anything ABI-related. The
-        // only relevant differences would be things like `#define` constants being
-        // different in the MinGW headers vs the MSVC headers, but any such
-        // differences would likely be a MinGW bug.
-        const rc_dirs: std.zig.LibCDirs = b: {
-            // Set the includes to .none here when there are no rc files to compile
-            var includes = if (options.rc_source_files.len > 0) options.rc_includes else .none;
-            const target = options.root_mod.resolved_target.result;
-            if (!options.root_mod.resolved_target.is_native_os or target.os.tag != .windows) {
-                switch (includes) {
-                    // MSVC can't be found when the host isn't Windows, so short-circuit.
-                    .msvc => return error.WindowsSdkNotFound,
-                    // Skip straight to gnu since we won't be able to detect
-                    // MSVC on non-Windows hosts.
-                    .any => includes = .gnu,
-                    .none, .gnu => {},
-                }
-            }
-            while (true) switch (includes) {
-                .any, .msvc => break :b std.zig.LibCDirs.detect(
-                    arena,
-                    options.zig_lib_directory.path.?,
-                    .{
-                        .cpu = target.cpu,
-                        .os = target.os,
-                        .abi = .msvc,
-                        .ofmt = target.ofmt,
-                    },
-                    options.root_mod.resolved_target.is_native_abi,
-                    // The .rc preprocessor will need to know the libc include dirs even if we
-                    // are not linking libc, so force 'link_libc' to true
-                    true,
-                    options.libc_installation,
-                ) catch |err| {
-                    if (includes == .any) {
-                        // fall back to mingw
-                        includes = .gnu;
-                        continue;
-                    }
-                    return err;
-                },
-                .gnu => break :b try std.zig.LibCDirs.detectFromBuilding(arena, options.zig_lib_directory.path.?, .{
-                    .cpu = target.cpu,
-                    .os = target.os,
-                    .abi = .gnu,
-                    .ofmt = target.ofmt,
-                }),
-                .none => break :b .{
-                    .libc_include_dir_list = &[0][]u8{},
-                    .libc_installation = null,
-                    .libc_framework_dir_list = &.{},
-                    .sysroot = null,
-                    .darwin_sdk_layout = null,
-                },
-            };
-        };
-
         const sysroot = options.sysroot orelse libc_dirs.sysroot;
 
         const include_compiler_rt = options.want_compiler_rt orelse
@@ -1492,7 +1429,7 @@ pub fn create(gpa: Allocator, arena: Allocator, options: CreateOptions) !*Compil
             .self_exe_path = options.self_exe_path,
             .libc_include_dir_list = libc_dirs.libc_include_dir_list,
             .libc_framework_dir_list = libc_dirs.libc_framework_dir_list,
-            .rc_include_dir_list = rc_dirs.libc_include_dir_list,
+            .rc_includes = options.rc_includes,
             .thread_pool = options.thread_pool,
             .clang_passthrough_mode = options.clang_passthrough_mode,
             .clang_preprocessor_mode = options.clang_preprocessor_mode,
@@ -2506,7 +2443,7 @@ fn addNonIncrementalStuffToCacheManifest(
     man.hash.add(comp.link_eh_frame_hdr);
     man.hash.add(comp.skip_linker_dependencies);
     man.hash.add(comp.include_compiler_rt);
-    man.hash.addListOfBytes(comp.rc_include_dir_list);
+    man.hash.add(comp.rc_includes);
     man.hash.addListOfBytes(comp.force_undefined_symbols.keys());
     man.hash.addListOfBytes(comp.framework_dirs);
     try link.hashAddSystemLibs(man, comp.system_libs);
@@ -4172,7 +4109,7 @@ pub fn obtainCObjectCacheManifest(
 pub fn obtainWin32ResourceCacheManifest(comp: *const Compilation) Cache.Manifest {
     var man = comp.cache_parent.obtain();
 
-    man.hash.addListOfBytes(comp.rc_include_dir_list);
+    man.hash.add(comp.rc_includes);
 
     return man;
 }
@@ -4812,11 +4749,12 @@ fn updateCObject(comp: *Compilation, c_object: *CObject, c_obj_prog_node: *std.P
 }
 
 fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32_resource_prog_node: *std.Progress.Node) !void {
-    if (!build_options.have_llvm) {
-        return comp.failWin32Resource(win32_resource, "clang not available: compiler built without LLVM extensions", .{});
+    if (!std.process.can_spawn) {
+        return comp.failWin32Resource(win32_resource, "{s} does not support spawning a child process", .{@tagName(builtin.os.tag)});
     }
+
     const self_exe_path = comp.self_exe_path orelse
-        return comp.failWin32Resource(win32_resource, "clang compilation disabled", .{});
+        return comp.failWin32Resource(win32_resource, "unable to find self exe path", .{});
 
     const tracy_trace = trace(@src());
     defer tracy_trace.end();
@@ -4856,6 +4794,7 @@ fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32
     if (win32_resource.src == .manifest) {
         _ = try man.addFile(src_path, null);
 
+        const rc_basename = try std.fmt.allocPrint(arena, "{s}.rc", .{src_basename});
         const res_basename = try std.fmt.allocPrint(arena, "{s}.res", .{src_basename});
 
         const digest = if (try man.hit()) man.final() else blk: {
@@ -4867,17 +4806,12 @@ fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32
             var o_dir = try comp.local_cache_directory.handle.makeOpenPath(o_sub_path, .{});
             defer o_dir.close();
 
-            var output_file = o_dir.createFile(res_basename, .{}) catch |err| {
-                const output_file_path = try comp.local_cache_directory.join(arena, &.{ o_sub_path, res_basename });
-                return comp.failWin32Resource(win32_resource, "failed to create output file '{s}': {s}", .{ output_file_path, @errorName(err) });
-            };
-            var output_file_closed = false;
-            defer if (!output_file_closed) output_file.close();
-
-            var diagnostics = resinator.errors.Diagnostics.init(arena);
-            defer diagnostics.deinit();
-
-            var output_buffered_stream = std.io.bufferedWriter(output_file.writer());
+            const in_rc_path = try comp.local_cache_directory.join(comp.gpa, &.{
+                o_sub_path, rc_basename,
+            });
+            const out_res_path = try comp.local_cache_directory.join(comp.gpa, &.{
+                o_sub_path, res_basename,
+            });
 
             // In .rc files, a " within a quoted string is escaped as ""
             const fmtRcEscape = struct {
@@ -4899,28 +4833,47 @@ fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32
             // 1 is CREATEPROCESS_MANIFEST_RESOURCE_ID which is the default ID used for RT_MANIFEST resources
             // 24 is RT_MANIFEST
             const input = try std.fmt.allocPrint(arena, "1 24 \"{s}\"", .{fmtRcEscape(src_path)});
+            try o_dir.writeFile(rc_basename, input);
+
+            var argv = std.ArrayList([]const u8).init(comp.gpa);
+            defer argv.deinit();
+
+            try argv.appendSlice(&.{
+                self_exe_path,
+                "rc",
+                "/:no-preprocess",
+                "/x", // ignore INCLUDE environment variable
+                "/c65001", // UTF-8 codepage
+                "/:auto-includes",
+                "none",
+            });
+            try argv.appendSlice(&.{ "--", in_rc_path, out_res_path });
 
-            resinator.compile.compile(arena, input, output_buffered_stream.writer(), .{
-                .cwd = std.fs.cwd(),
-                .diagnostics = &diagnostics,
-                .ignore_include_env_var = true,
-                .default_code_page = .utf8,
-            }) catch |err| switch (err) {
-                error.ParseError, error.CompileError => {
-                    // Delete the output file on error
-                    output_file.close();
-                    output_file_closed = true;
-                    // Failing to delete is not really a big deal, so swallow any errors
-                    o_dir.deleteFile(res_basename) catch {
-                        const output_file_path = try comp.local_cache_directory.join(arena, &.{ o_sub_path, res_basename });
-                        log.warn("failed to delete '{s}': {s}", .{ output_file_path, @errorName(err) });
-                    };
-                    return comp.failWin32ResourceCompile(win32_resource, input, &diagnostics, null);
-                },
-                else => |e| return e,
+            var child = std.ChildProcess.init(argv.items, arena);
+            child.stdin_behavior = .Ignore;
+            child.stdout_behavior = .Ignore;
+            child.stderr_behavior = .Pipe;
+
+            try child.spawn();
+
+            const stderr_reader = child.stderr.?.reader();
+            const stderr = try stderr_reader.readAllAlloc(arena, 10 * 1024 * 1024);
+            const term = child.wait() catch |err| {
+                return comp.failWin32Resource(win32_resource, "unable to spawn {s}: {s}", .{ argv.items[0], @errorName(err) });
             };
 
-            try output_buffered_stream.flush();
+            switch (term) {
+                .Exited => |code| {
+                    if (code != 0) {
+                        log.err("zig rc failed with stderr:\n{s}", .{stderr});
+                        return comp.failWin32Resource(win32_resource, "zig rc exited with code {d}", .{code});
+                    }
+                },
+                else => {
+                    log.err("zig rc terminated with stderr:\n{s}", .{stderr});
+                    return comp.failWin32Resource(win32_resource, "zig rc terminated unexpectedly", .{});
+                },
+            }
 
             break :blk digest;
         };
@@ -4951,9 +4904,6 @@ fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32
     const rc_basename_noext = src_basename[0 .. src_basename.len - std.fs.path.extension(src_basename).len];
 
     const digest = if (try man.hit()) man.final() else blk: {
-        const rcpp_filename = try std.fmt.allocPrint(arena, "{s}.rcpp", .{rc_basename_noext});
-
-        const out_rcpp_path = try comp.tmpFilePath(arena, rcpp_filename);
         var zig_cache_tmp_dir = try comp.local_cache_directory.handle.makeOpenPath("tmp", .{});
         defer zig_cache_tmp_dir.close();
 
@@ -4963,193 +4913,89 @@ fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32
         // so we need a temporary filename.
         const out_res_path = try comp.tmpFilePath(arena, res_filename);
 
-        var options = options: {
-            var resinator_args = try std.ArrayListUnmanaged([]const u8).initCapacity(comp.gpa, rc_src.extra_flags.len + 4);
-            defer resinator_args.deinit(comp.gpa);
-
-            resinator_args.appendAssumeCapacity(""); // dummy 'process name' arg
-            resinator_args.appendSliceAssumeCapacity(rc_src.extra_flags);
-            resinator_args.appendSliceAssumeCapacity(&.{ "--", out_rcpp_path, out_res_path });
-
-            var cli_diagnostics = resinator.cli.Diagnostics.init(comp.gpa);
-            defer cli_diagnostics.deinit();
-            const options = resinator.cli.parse(comp.gpa, resinator_args.items, &cli_diagnostics) catch |err| switch (err) {
-                error.ParseError => {
-                    return comp.failWin32ResourceCli(win32_resource, &cli_diagnostics);
-                },
-                else => |e| return e,
-            };
-            break :options options;
-        };
-        defer options.deinit();
-
-        // We never want to read the INCLUDE environment variable, so
-        // unconditionally set `ignore_include_env_var` to true
-        options.ignore_include_env_var = true;
-
-        if (options.preprocess != .yes) {
-            return comp.failWin32Resource(win32_resource, "the '{s}' option is not supported in this context", .{switch (options.preprocess) {
-                .no => "/:no-preprocess",
-                .only => "/p",
-                .yes => unreachable,
-            }});
-        }
-
         var argv = std.ArrayList([]const u8).init(comp.gpa);
         defer argv.deinit();
 
-        try argv.appendSlice(&[_][]const u8{ self_exe_path, "clang" });
-
-        try resinator.preprocess.appendClangArgs(arena, &argv, options, .{
-            .clang_target = null, // handled by addCCArgs
-            .system_include_paths = &.{}, // handled by addCCArgs
-            .needs_gnu_workaround = comp.getTarget().isGnu(),
-            .nostdinc = false, // handled by addCCArgs
+        const depfile_filename = try std.fmt.allocPrint(arena, "{s}.d.json", .{rc_basename_noext});
+        const out_dep_path = try comp.tmpFilePath(arena, depfile_filename);
+        try argv.appendSlice(&.{
+            self_exe_path,
+            "rc",
+            "/:depfile",
+            out_dep_path,
+            "/:depfile-fmt",
+            "json",
+            "/x", // ignore INCLUDE environment variable
+            "/:auto-includes",
+            @tagName(comp.rc_includes),
         });
-
-        try argv.append(rc_src.src_path);
-        try argv.appendSlice(&[_][]const u8{
-            "-o",
-            out_rcpp_path,
-        });
-
-        const out_dep_path = try std.fmt.allocPrint(arena, "{s}.d", .{out_rcpp_path});
-        // Note: addCCArgs will implicitly add _DEBUG/NDEBUG depending on the optimization
-        // mode. While these defines are not normally present when calling rc.exe directly,
+        // While these defines are not normally present when calling rc.exe directly,
         // them being defined matches the behavior of how MSVC calls rc.exe which is the more
         // relevant behavior in this case.
-        try comp.addCCArgs(arena, &argv, .rc, out_dep_path, rc_src.owner);
-
-        if (comp.verbose_cc) {
-            dump_argv(argv.items);
+        switch (rc_src.owner.optimize_mode) {
+            .Debug => try argv.append("-D_DEBUG"),
+            .ReleaseSafe => {},
+            .ReleaseFast, .ReleaseSmall => try argv.append("-DNDEBUG"),
         }
+        try argv.appendSlice(rc_src.extra_flags);
+        try argv.appendSlice(&.{ "--", rc_src.src_path, out_res_path });
 
-        if (std.process.can_spawn) {
-            var child = std.ChildProcess.init(argv.items, arena);
-            child.stdin_behavior = .Ignore;
-            child.stdout_behavior = .Ignore;
-            child.stderr_behavior = .Pipe;
-
-            try child.spawn();
+        var child = std.ChildProcess.init(argv.items, arena);
+        child.stdin_behavior = .Ignore;
+        child.stdout_behavior = .Ignore;
+        child.stderr_behavior = .Pipe;
 
-            const stderr_reader = child.stderr.?.reader();
-
-            const stderr = try stderr_reader.readAllAlloc(arena, 10 * 1024 * 1024);
-
-            const term = child.wait() catch |err| {
-                return comp.failWin32Resource(win32_resource, "unable to spawn {s}: {s}", .{ argv.items[0], @errorName(err) });
-            };
-
-            switch (term) {
-                .Exited => |code| {
-                    if (code != 0) {
-                        // TODO parse clang stderr and turn it into an error message
-                        // and then call failCObjWithOwnedErrorMsg
-                        log.err("clang preprocessor failed with stderr:\n{s}", .{stderr});
-                        return comp.failWin32Resource(win32_resource, "clang preprocessor exited with code {d}", .{code});
-                    }
-                },
-                else => {
-                    log.err("clang preprocessor terminated with stderr:\n{s}", .{stderr});
-                    return comp.failWin32Resource(win32_resource, "clang preprocessor terminated unexpectedly", .{});
-                },
-            }
-        } else {
-            const exit_code = try clangMain(arena, argv.items);
-            if (exit_code != 0) {
-                return comp.failWin32Resource(win32_resource, "clang preprocessor exited with code {d}", .{exit_code});
-            }
-        }
+        try child.spawn();
 
-        const dep_basename = std.fs.path.basename(out_dep_path);
-        // Add the files depended on to the cache system.
-        try man.addDepFilePost(zig_cache_tmp_dir, dep_basename);
-        switch (comp.cache_use) {
-            .whole => |whole| if (whole.cache_manifest) |whole_cache_manifest| {
-                whole.cache_manifest_mutex.lock();
-                defer whole.cache_manifest_mutex.unlock();
-                try whole_cache_manifest.addDepFilePost(zig_cache_tmp_dir, dep_basename);
-            },
-            .incremental => {},
-        }
-        // Just to save disk space, we delete the file because it is never needed again.
-        zig_cache_tmp_dir.deleteFile(dep_basename) catch |err| {
-            log.warn("failed to delete '{s}': {s}", .{ out_dep_path, @errorName(err) });
+        const stderr_reader = child.stderr.?.reader();
+        const stderr = try stderr_reader.readAllAlloc(arena, 10 * 1024 * 1024);
+        const term = child.wait() catch |err| {
+            return comp.failWin32Resource(win32_resource, "unable to spawn {s}: {s}", .{ argv.items[0], @errorName(err) });
         };
 
-        const full_input = std.fs.cwd().readFileAlloc(arena, out_rcpp_path, std.math.maxInt(usize)) catch |err| switch (err) {
-            error.OutOfMemory => return error.OutOfMemory,
-            else => |e| {
-                return comp.failWin32Resource(win32_resource, "failed to read preprocessed file '{s}': {s}", .{ out_rcpp_path, @errorName(e) });
+        switch (term) {
+            .Exited => |code| {
+                if (code != 0) {
+                    log.err("zig rc failed with stderr:\n{s}", .{stderr});
+                    return comp.failWin32Resource(win32_resource, "zig rc exited with code {d}", .{code});
+                }
+            },
+            else => {
+                log.err("zig rc terminated with stderr:\n{s}", .{stderr});
+                return comp.failWin32Resource(win32_resource, "zig rc terminated unexpectedly", .{});
             },
-        };
-
-        var mapping_results = try resinator.source_mapping.parseAndRemoveLineCommands(arena, full_input, full_input, .{ .initial_filename = rc_src.src_path });
-        defer mapping_results.mappings.deinit(arena);
-
-        const final_input = resinator.comments.removeComments(mapping_results.result, mapping_results.result, &mapping_results.mappings);
-
-        var output_file = zig_cache_tmp_dir.createFile(out_res_path, .{}) catch |err| {
-            return comp.failWin32Resource(win32_resource, "failed to create output file '{s}': {s}", .{ out_res_path, @errorName(err) });
-        };
-        var output_file_closed = false;
-        defer if (!output_file_closed) output_file.close();
-
-        var diagnostics = resinator.errors.Diagnostics.init(arena);
-        defer diagnostics.deinit();
-
-        var dependencies_list = std.ArrayList([]const u8).init(comp.gpa);
-        defer {
-            for (dependencies_list.items) |item| {
-                comp.gpa.free(item);
-            }
-            dependencies_list.deinit();
         }
 
-        var output_buffered_stream = std.io.bufferedWriter(output_file.writer());
-
-        resinator.compile.compile(arena, final_input, output_buffered_stream.writer(), .{
-            .cwd = std.fs.cwd(),
-            .diagnostics = &diagnostics,
-            .source_mappings = &mapping_results.mappings,
-            .dependencies_list = &dependencies_list,
-            .system_include_paths = comp.rc_include_dir_list,
-            .ignore_include_env_var = true,
-            // options
-            .extra_include_paths = options.extra_include_paths.items,
-            .default_language_id = options.default_language_id,
-            .default_code_page = options.default_code_page orelse .windows1252,
-            .verbose = options.verbose,
-            .null_terminate_string_table_strings = options.null_terminate_string_table_strings,
-            .max_string_literal_codepoints = options.max_string_literal_codepoints,
-            .silent_duplicate_control_ids = options.silent_duplicate_control_ids,
-            .warn_instead_of_error_on_invalid_code_page = options.warn_instead_of_error_on_invalid_code_page,
-        }) catch |err| switch (err) {
-            error.ParseError, error.CompileError => {
-                // Delete the output file on error
-                output_file.close();
-                output_file_closed = true;
-                // Failing to delete is not really a big deal, so swallow any errors
-                zig_cache_tmp_dir.deleteFile(out_res_path) catch {
-                    log.warn("failed to delete '{s}': {s}", .{ out_res_path, @errorName(err) });
-                };
-                return comp.failWin32ResourceCompile(win32_resource, final_input, &diagnostics, mapping_results.mappings);
-            },
-            else => |e| return e,
-        };
+        // Read depfile and update cache manifest
+        {
+            const dep_basename = std.fs.path.basename(out_dep_path);
+            const dep_file_contents = try zig_cache_tmp_dir.readFileAlloc(arena, dep_basename, 50 * 1024 * 1024);
+            defer arena.free(dep_file_contents);
 
-        try output_buffered_stream.flush();
+            const value = try std.json.parseFromSliceLeaky(std.json.Value, arena, dep_file_contents, .{});
+            if (value != .array) {
+                return comp.failWin32Resource(win32_resource, "depfile from zig rc has unexpected format", .{});
+            }
 
-        for (dependencies_list.items) |dep_file_path| {
-            try man.addFilePost(dep_file_path);
-            switch (comp.cache_use) {
-                .whole => |whole| if (whole.cache_manifest) |whole_cache_manifest| {
-                    whole.cache_manifest_mutex.lock();
-                    defer whole.cache_manifest_mutex.unlock();
-                    try whole_cache_manifest.addFilePost(dep_file_path);
-                },
-                .incremental => {},
+            for (value.array.items) |element| {
+                if (element != .string) {
+                    return comp.failWin32Resource(win32_resource, "depfile from zig rc has unexpected format", .{});
+                }
+                const dep_file_path = element.string;
+                try man.addFilePost(dep_file_path);
+                switch (comp.cache_use) {
+                    .whole => |whole| if (whole.cache_manifest) |whole_cache_manifest| {
+                        whole.cache_manifest_mutex.lock();
+                        defer whole.cache_manifest_mutex.unlock();
+                        try whole_cache_manifest.addFilePost(dep_file_path);
+                    },
+                    .incremental => {},
+                }
             }
+            // Just to save disk space, we delete the file because it is never needed again.
+            zig_cache_tmp_dir.deleteFile(dep_basename) catch |err| {
+                log.warn("failed to delete '{s}': {s}", .{ out_dep_path, @errorName(err) });
+            };
         }
 
         // Rename into place.
@@ -5159,8 +5005,6 @@ fn updateWin32Resource(comp: *Compilation, win32_resource: *Win32Resource, win32
         defer o_dir.close();
         const tmp_basename = std.fs.path.basename(out_res_path);
         try std.fs.rename(zig_cache_tmp_dir, tmp_basename, o_dir, res_filename);
-        const tmp_rcpp_basename = std.fs.path.basename(out_rcpp_path);
-        try std.fs.rename(zig_cache_tmp_dir, tmp_rcpp_basename, o_dir, rcpp_filename);
         break :blk digest;
     };
 
@@ -5352,16 +5196,9 @@ pub fn addCCArgs(
             try argv.append("-isystem");
             try argv.append(c_headers_dir);
 
-            if (ext == .rc) {
-                for (comp.rc_include_dir_list) |include_dir| {
-                    try argv.append("-isystem");
-                    try argv.append(include_dir);
-                }
-            } else {
-                for (comp.libc_include_dir_list) |include_dir| {
-                    try argv.append("-isystem");
-                    try argv.append(include_dir);
-                }
+            for (comp.libc_include_dir_list) |include_dir| {
+                try argv.append("-isystem");
+                try argv.append(include_dir);
             }
 
             if (target.cpu.model.llvm_name) |llvm_name| {
@@ -5726,167 +5563,6 @@ fn failWin32ResourceWithOwnedBundle(
     return error.AnalysisFail;
 }
 
-fn failWin32ResourceCli(
-    comp: *Compilation,
-    win32_resource: *Win32Resource,
-    diagnostics: *resinator.cli.Diagnostics,
-) SemaError {
-    @setCold(true);
-
-    var bundle: ErrorBundle.Wip = undefined;
-    try bundle.init(comp.gpa);
-    errdefer bundle.deinit();
-
-    try bundle.addRootErrorMessage(.{
-        .msg = try bundle.addString("invalid command line option(s)"),
-        .src_loc = try bundle.addSourceLocation(.{
-            .src_path = try bundle.addString(switch (win32_resource.src) {
-                .rc => |rc_src| rc_src.src_path,
-                .manifest => |manifest_src| manifest_src,
-            }),
-            .line = 0,
-            .column = 0,
-            .span_start = 0,
-            .span_main = 0,
-            .span_end = 0,
-        }),
-    });
-
-    var cur_err: ?ErrorBundle.ErrorMessage = null;
-    var cur_notes: std.ArrayListUnmanaged(ErrorBundle.ErrorMessage) = .{};
-    defer cur_notes.deinit(comp.gpa);
-    for (diagnostics.errors.items) |err_details| {
-        switch (err_details.type) {
-            .err => {
-                if (cur_err) |err| {
-                    try win32ResourceFlushErrorMessage(&bundle, err, cur_notes.items);
-                }
-                cur_err = .{
-                    .msg = try bundle.addString(err_details.msg.items),
-                };
-                cur_notes.clearRetainingCapacity();
-            },
-            .warning => cur_err = null,
-            .note => {
-                if (cur_err == null) continue;
-                cur_err.?.notes_len += 1;
-                try cur_notes.append(comp.gpa, .{
-                    .msg = try bundle.addString(err_details.msg.items),
-                });
-            },
-        }
-    }
-    if (cur_err) |err| {
-        try win32ResourceFlushErrorMessage(&bundle, err, cur_notes.items);
-    }
-
-    const finished_bundle = try bundle.toOwnedBundle("");
-    return comp.failWin32ResourceWithOwnedBundle(win32_resource, finished_bundle);
-}
-
-fn failWin32ResourceCompile(
-    comp: *Compilation,
-    win32_resource: *Win32Resource,
-    source: []const u8,
-    diagnostics: *resinator.errors.Diagnostics,
-    opt_mappings: ?resinator.source_mapping.SourceMappings,
-) SemaError {
-    @setCold(true);
-
-    var bundle: ErrorBundle.Wip = undefined;
-    try bundle.init(comp.gpa);
-    errdefer bundle.deinit();
-
-    var msg_buf: std.ArrayListUnmanaged(u8) = .{};
-    defer msg_buf.deinit(comp.gpa);
-    var cur_err: ?ErrorBundle.ErrorMessage = null;
-    var cur_notes: std.ArrayListUnmanaged(ErrorBundle.ErrorMessage) = .{};
-    defer cur_notes.deinit(comp.gpa);
-    for (diagnostics.errors.items) |err_details| {
-        switch (err_details.type) {
-            .hint => continue,
-            // Clear the current error so that notes don't bleed into unassociated errors
-            .warning => {
-                cur_err = null;
-                continue;
-            },
-            .note => if (cur_err == null) continue,
-            .err => {},
-        }
-        const err_line, const err_filename = blk: {
-            if (opt_mappings) |mappings| {
-                const corresponding_span = mappings.get(err_details.token.line_number);
-                const corresponding_file = mappings.files.get(corresponding_span.filename_offset);
-                const err_line = corresponding_span.start_line;
-                break :blk .{ err_line, corresponding_file };
-            } else {
-                break :blk .{ err_details.token.line_number, "<generated rc>" };
-            }
-        };
-
-        const source_line_start = err_details.token.getLineStart(source);
-        const column = err_details.token.calculateColumn(source, 1, source_line_start);
-
-        msg_buf.clearRetainingCapacity();
-        try err_details.render(msg_buf.writer(comp.gpa), source, diagnostics.strings.items);
-
-        const src_loc = src_loc: {
-            var src_loc: ErrorBundle.SourceLocation = .{
-                .src_path = try bundle.addString(err_filename),
-                .line = @intCast(err_line - 1), // 1-based -> 0-based
-                .column = @intCast(column),
-                .span_start = 0,
-                .span_main = 0,
-                .span_end = 0,
-            };
-            if (err_details.print_source_line) {
-                const source_line = err_details.token.getLine(source, source_line_start);
-                const visual_info = err_details.visualTokenInfo(source_line_start, source_line_start + source_line.len);
-                src_loc.span_start = @intCast(visual_info.point_offset - visual_info.before_len);
-                src_loc.span_main = @intCast(visual_info.point_offset);
-                src_loc.span_end = @intCast(visual_info.point_offset + 1 + visual_info.after_len);
-                src_loc.source_line = try bundle.addString(source_line);
-            }
-            break :src_loc try bundle.addSourceLocation(src_loc);
-        };
-
-        switch (err_details.type) {
-            .err => {
-                if (cur_err) |err| {
-                    try win32ResourceFlushErrorMessage(&bundle, err, cur_notes.items);
-                }
-                cur_err = .{
-                    .msg = try bundle.addString(msg_buf.items),
-                    .src_loc = src_loc,
-                };
-                cur_notes.clearRetainingCapacity();
-            },
-            .note => {
-                cur_err.?.notes_len += 1;
-                try cur_notes.append(comp.gpa, .{
-                    .msg = try bundle.addString(msg_buf.items),
-                    .src_loc = src_loc,
-                });
-            },
-            .warning, .hint => unreachable,
-        }
-    }
-    if (cur_err) |err| {
-        try win32ResourceFlushErrorMessage(&bundle, err, cur_notes.items);
-    }
-
-    const finished_bundle = try bundle.toOwnedBundle("");
-    return comp.failWin32ResourceWithOwnedBundle(win32_resource, finished_bundle);
-}
-
-fn win32ResourceFlushErrorMessage(wip: *ErrorBundle.Wip, msg: ErrorBundle.ErrorMessage, notes: []const ErrorBundle.ErrorMessage) !void {
-    try wip.addRootErrorMessage(msg);
-    const notes_start = try wip.reserveNotes(@intCast(notes.len));
-    for (notes_start.., notes) |i, note| {
-        wip.extra.items[i] = @intFromEnum(wip.addErrorMessageAssumeCapacity(note));
-    }
-}
-
 pub const FileExt = enum {
     c,
     cpp,
src/main.zig
@@ -291,7 +291,12 @@ fn mainArgs(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
     } else if (mem.eql(u8, cmd, "translate-c")) {
         return buildOutputType(gpa, arena, args, .translate_c);
     } else if (mem.eql(u8, cmd, "rc")) {
-        return cmdRc(gpa, arena, args[1..]);
+        return jitCmd(gpa, arena, cmd_args, .{
+            .cmd_name = "resinator",
+            .root_src_path = "resinator/main.zig",
+            .depend_on_aro = true,
+            .prepend_zig_lib_dir_path = true,
+        });
     } else if (mem.eql(u8, cmd, "fmt")) {
         return jitCmd(gpa, arena, cmd_args, .{
             .cmd_name = "fmt",
@@ -4625,276 +4630,6 @@ fn cmdTranslateC(comp: *Compilation, arena: Allocator, fancy_output: ?*Compilati
     }
 }
 
-fn cmdRc(gpa: Allocator, arena: Allocator, args: []const []const u8) !void {
-    const resinator = @import("resinator.zig");
-
-    const stderr = std.io.getStdErr();
-    const stderr_config = std.io.tty.detectConfig(stderr);
-
-    var options = options: {
-        var cli_diagnostics = resinator.cli.Diagnostics.init(gpa);
-        defer cli_diagnostics.deinit();
-        var options = resinator.cli.parse(gpa, args, &cli_diagnostics) catch |err| switch (err) {
-            error.ParseError => {
-                cli_diagnostics.renderToStdErr(args, stderr_config);
-                process.exit(1);
-            },
-            else => |e| return e,
-        };
-        try options.maybeAppendRC(std.fs.cwd());
-
-        // print any warnings/notes
-        cli_diagnostics.renderToStdErr(args, stderr_config);
-        // If there was something printed, then add an extra newline separator
-        // so that there is a clear separation between the cli diagnostics and whatever
-        // gets printed after
-        if (cli_diagnostics.errors.items.len > 0) {
-            std.debug.print("\n", .{});
-        }
-        break :options options;
-    };
-    defer options.deinit();
-
-    if (options.print_help_and_exit) {
-        try resinator.cli.writeUsage(stderr.writer(), "zig rc");
-        return;
-    }
-
-    const stdout_writer = std.io.getStdOut().writer();
-    if (options.verbose) {
-        try options.dumpVerbose(stdout_writer);
-        try stdout_writer.writeByte('\n');
-    }
-
-    const full_input = full_input: {
-        if (options.preprocess != .no) {
-            if (!build_options.have_llvm) {
-                fatal("clang not available: compiler built without LLVM extensions", .{});
-            }
-
-            var argv = std.ArrayList([]const u8).init(gpa);
-            defer argv.deinit();
-
-            const self_exe_path = try introspect.findZigExePath(arena);
-            var zig_lib_directory = introspect.findZigLibDirFromSelfExe(arena, self_exe_path) catch |err| {
-                try resinator.utils.renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to find zig installation directory: {s}", .{@errorName(err)});
-                process.exit(1);
-            };
-            defer zig_lib_directory.handle.close();
-
-            const include_args = detectRcIncludeDirs(arena, zig_lib_directory.path.?, options.auto_includes) catch |err| {
-                try resinator.utils.renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to detect system include directories: {s}", .{@errorName(err)});
-                process.exit(1);
-            };
-
-            try argv.appendSlice(&[_][]const u8{ self_exe_path, "clang" });
-
-            const clang_target = clang_target: {
-                if (include_args.target_abi) |abi| {
-                    break :clang_target try std.fmt.allocPrint(arena, "x86_64-unknown-windows-{s}", .{abi});
-                }
-                break :clang_target "x86_64-unknown-windows";
-            };
-            try resinator.preprocess.appendClangArgs(arena, &argv, options, .{
-                .clang_target = clang_target,
-                .system_include_paths = include_args.include_paths,
-                .needs_gnu_workaround = if (include_args.target_abi) |abi| std.mem.eql(u8, abi, "gnu") else false,
-                .nostdinc = true,
-            });
-
-            try argv.append(options.input_filename);
-
-            if (options.verbose) {
-                try stdout_writer.writeAll("Preprocessor: zig clang\n");
-                for (argv.items[0 .. argv.items.len - 1]) |arg| {
-                    try stdout_writer.print("{s} ", .{arg});
-                }
-                try stdout_writer.print("{s}\n\n", .{argv.items[argv.items.len - 1]});
-            }
-
-            if (process.can_spawn) {
-                const result = std.ChildProcess.run(.{
-                    .allocator = gpa,
-                    .argv = argv.items,
-                    .max_output_bytes = std.math.maxInt(u32),
-                }) catch |err| {
-                    try resinator.utils.renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to spawn preprocessor child process: {s}", .{@errorName(err)});
-                    process.exit(1);
-                };
-                errdefer gpa.free(result.stdout);
-                defer gpa.free(result.stderr);
-
-                switch (result.term) {
-                    .Exited => |code| {
-                        if (code != 0) {
-                            try resinator.utils.renderErrorMessage(stderr.writer(), stderr_config, .err, "the preprocessor failed with exit code {}:", .{code});
-                            try stderr.writeAll(result.stderr);
-                            try stderr.writeAll("\n");
-                            process.exit(1);
-                        }
-                    },
-                    .Signal, .Stopped, .Unknown => {
-                        try resinator.utils.renderErrorMessage(stderr.writer(), stderr_config, .err, "the preprocessor terminated unexpectedly ({s}):", .{@tagName(result.term)});
-                        try stderr.writeAll(result.stderr);
-                        try stderr.writeAll("\n");
-                        process.exit(1);
-                    },
-                }
-
-                break :full_input result.stdout;
-            } else {
-                // need to use an intermediate file
-                const rand_int = std.crypto.random.int(u64);
-                const preprocessed_path = try std.fmt.allocPrint(gpa, "resinator{x}.rcpp", .{rand_int});
-                defer gpa.free(preprocessed_path);
-                defer std.fs.cwd().deleteFile(preprocessed_path) catch {};
-
-                try argv.appendSlice(&.{ "-o", preprocessed_path });
-                const exit_code = try clangMain(arena, argv.items);
-                if (exit_code != 0) {
-                    try resinator.utils.renderErrorMessage(stderr.writer(), stderr_config, .err, "the preprocessor failed with exit code {}:", .{exit_code});
-                    process.exit(1);
-                }
-                break :full_input std.fs.cwd().readFileAlloc(gpa, preprocessed_path, std.math.maxInt(usize)) catch |err| {
-                    try resinator.utils.renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to read preprocessed file path '{s}': {s}", .{ preprocessed_path, @errorName(err) });
-                    process.exit(1);
-                };
-            }
-        } else {
-            break :full_input std.fs.cwd().readFileAlloc(gpa, options.input_filename, std.math.maxInt(usize)) catch |err| {
-                try resinator.utils.renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to read input file path '{s}': {s}", .{ options.input_filename, @errorName(err) });
-                process.exit(1);
-            };
-        }
-    };
-    defer gpa.free(full_input);
-
-    if (options.preprocess == .only) {
-        std.fs.cwd().writeFile(options.output_filename, full_input) catch |err| {
-            try resinator.utils.renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to write output file '{s}': {s}", .{ options.output_filename, @errorName(err) });
-            process.exit(1);
-        };
-        return cleanExit();
-    }
-
-    var mapping_results = try resinator.source_mapping.parseAndRemoveLineCommands(gpa, full_input, full_input, .{ .initial_filename = options.input_filename });
-    defer mapping_results.mappings.deinit(gpa);
-
-    const final_input = resinator.comments.removeComments(mapping_results.result, mapping_results.result, &mapping_results.mappings);
-
-    var output_file = std.fs.cwd().createFile(options.output_filename, .{}) catch |err| {
-        try resinator.utils.renderErrorMessage(stderr.writer(), stderr_config, .err, "unable to create output file '{s}': {s}", .{ options.output_filename, @errorName(err) });
-        process.exit(1);
-    };
-    var output_file_closed = false;
-    defer if (!output_file_closed) output_file.close();
-
-    var diagnostics = resinator.errors.Diagnostics.init(gpa);
-    defer diagnostics.deinit();
-
-    var output_buffered_stream = std.io.bufferedWriter(output_file.writer());
-
-    resinator.compile.compile(gpa, final_input, output_buffered_stream.writer(), .{
-        .cwd = std.fs.cwd(),
-        .diagnostics = &diagnostics,
-        .source_mappings = &mapping_results.mappings,
-        .dependencies_list = null,
-        .ignore_include_env_var = options.ignore_include_env_var,
-        .extra_include_paths = options.extra_include_paths.items,
-        .default_language_id = options.default_language_id,
-        .default_code_page = options.default_code_page orelse .windows1252,
-        .verbose = options.verbose,
-        .null_terminate_string_table_strings = options.null_terminate_string_table_strings,
-        .max_string_literal_codepoints = options.max_string_literal_codepoints,
-        .silent_duplicate_control_ids = options.silent_duplicate_control_ids,
-        .warn_instead_of_error_on_invalid_code_page = options.warn_instead_of_error_on_invalid_code_page,
-    }) catch |err| switch (err) {
-        error.ParseError, error.CompileError => {
-            diagnostics.renderToStdErr(std.fs.cwd(), final_input, stderr_config, mapping_results.mappings);
-            // Delete the output file on error
-            output_file.close();
-            output_file_closed = true;
-            // Failing to delete is not really a big deal, so swallow any errors
-            std.fs.cwd().deleteFile(options.output_filename) catch {};
-            process.exit(1);
-        },
-        else => |e| return e,
-    };
-
-    try output_buffered_stream.flush();
-
-    // print any warnings/notes
-    diagnostics.renderToStdErr(std.fs.cwd(), final_input, stderr_config, mapping_results.mappings);
-
-    return cleanExit();
-}
-
-const RcIncludeArgs = struct {
-    include_paths: []const []const u8 = &.{},
-    target_abi: ?[]const u8 = null,
-};
-
-fn detectRcIncludeDirs(arena: Allocator, zig_lib_dir: []const u8, auto_includes: @import("resinator.zig").cli.Options.AutoIncludes) !RcIncludeArgs {
-    if (auto_includes == .none) return .{};
-    var cur_includes = auto_includes;
-    if (builtin.target.os.tag != .windows) {
-        switch (cur_includes) {
-            // MSVC can't be found when the host isn't Windows, so short-circuit.
-            .msvc => return error.WindowsSdkNotFound,
-            // Skip straight to gnu since we won't be able to detect MSVC on non-Windows hosts.
-            .any => cur_includes = .gnu,
-            .gnu => {},
-            .none => unreachable,
-        }
-    }
-    while (true) {
-        switch (cur_includes) {
-            .any, .msvc => {
-                const target_query: std.Target.Query = .{
-                    .os_tag = .windows,
-                    .abi = .msvc,
-                };
-                const target = std.zig.resolveTargetQueryOrFatal(target_query);
-                const is_native_abi = target_query.isNativeAbi();
-                const detected_libc = std.zig.LibCDirs.detect(arena, zig_lib_dir, target, is_native_abi, true, null) catch |err| {
-                    if (cur_includes == .any) {
-                        // fall back to mingw
-                        cur_includes = .gnu;
-                        continue;
-                    }
-                    return err;
-                };
-                if (detected_libc.libc_include_dir_list.len == 0) {
-                    if (cur_includes == .any) {
-                        // fall back to mingw
-                        cur_includes = .gnu;
-                        continue;
-                    }
-                    return error.WindowsSdkNotFound;
-                }
-                return .{
-                    .include_paths = detected_libc.libc_include_dir_list,
-                    .target_abi = "msvc",
-                };
-            },
-            .gnu => {
-                const target_query: std.Target.Query = .{
-                    .os_tag = .windows,
-                    .abi = .gnu,
-                };
-                const target = std.zig.resolveTargetQueryOrFatal(target_query);
-                const is_native_abi = target_query.isNativeAbi();
-                const detected_libc = try std.zig.LibCDirs.detect(arena, zig_lib_dir, target, is_native_abi, true, null);
-                return .{
-                    .include_paths = detected_libc.libc_include_dir_list,
-                    .target_abi = "gnu",
-                };
-            },
-            .none => unreachable,
-        }
-    }
-}
-
 const usage_init =
     \\Usage: zig init
     \\
src/resinator.zig
@@ -1,25 +0,0 @@
-comptime {
-    if (@import("build_options").only_core_functionality) {
-        @compileError("resinator included in only_core_functionality build");
-    }
-}
-
-pub const ani = @import("resinator/ani.zig");
-pub const ast = @import("resinator/ast.zig");
-pub const bmp = @import("resinator/bmp.zig");
-pub const cli = @import("resinator/cli.zig");
-pub const code_pages = @import("resinator/code_pages.zig");
-pub const comments = @import("resinator/comments.zig");
-pub const compile = @import("resinator/compile.zig");
-pub const errors = @import("resinator/errors.zig");
-pub const ico = @import("resinator/ico.zig");
-pub const lang = @import("resinator/lang.zig");
-pub const lex = @import("resinator/lex.zig");
-pub const literals = @import("resinator/literals.zig");
-pub const parse = @import("resinator/parse.zig");
-pub const preprocess = @import("resinator/preprocess.zig");
-pub const rc = @import("resinator/rc.zig");
-pub const res = @import("resinator/res.zig");
-pub const source_mapping = @import("resinator/source_mapping.zig");
-pub const utils = @import("resinator/utils.zig");
-pub const windows1252 = @import("resinator/windows1252.zig");