zig

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

errors.zig (62242B) - Raw


      1 const std = @import("std");
      2 const assert = std.debug.assert;
      3 const Token = @import("lex.zig").Token;
      4 const SourceMappings = @import("source_mapping.zig").SourceMappings;
      5 const utils = @import("utils.zig");
      6 const rc = @import("rc.zig");
      7 const res = @import("res.zig");
      8 const ico = @import("ico.zig");
      9 const bmp = @import("bmp.zig");
     10 const parse = @import("parse.zig");
     11 const lang = @import("lang.zig");
     12 const code_pages = @import("code_pages.zig");
     13 const SupportedCodePage = code_pages.SupportedCodePage;
     14 const builtin = @import("builtin");
     15 const native_endian = builtin.cpu.arch.endian();
     16 
     17 pub const Diagnostics = struct {
     18     errors: std.ArrayListUnmanaged(ErrorDetails) = .empty,
     19     /// Append-only, cannot handle removing strings.
     20     /// Expects to own all strings within the list.
     21     strings: std.ArrayListUnmanaged([]const u8) = .empty,
     22     allocator: std.mem.Allocator,
     23 
     24     pub fn init(allocator: std.mem.Allocator) Diagnostics {
     25         return .{
     26             .allocator = allocator,
     27         };
     28     }
     29 
     30     pub fn deinit(self: *Diagnostics) void {
     31         self.errors.deinit(self.allocator);
     32         for (self.strings.items) |str| {
     33             self.allocator.free(str);
     34         }
     35         self.strings.deinit(self.allocator);
     36     }
     37 
     38     pub fn append(self: *Diagnostics, error_details: ErrorDetails) !void {
     39         try self.errors.append(self.allocator, error_details);
     40     }
     41 
     42     const SmallestStringIndexType = std.meta.Int(.unsigned, @min(
     43         @bitSizeOf(ErrorDetails.FileOpenError.FilenameStringIndex),
     44         @min(
     45             @bitSizeOf(ErrorDetails.IconReadError.FilenameStringIndex),
     46             @bitSizeOf(ErrorDetails.BitmapReadError.FilenameStringIndex),
     47         ),
     48     ));
     49 
     50     /// Returns the index of the added string as the SmallestStringIndexType
     51     /// in order to avoid needing to `@intCast` it at callsites of putString.
     52     /// Instead, this function will error if the index would ever exceed the
     53     /// smallest FilenameStringIndex of an ErrorDetails type.
     54     pub fn putString(self: *Diagnostics, str: []const u8) !SmallestStringIndexType {
     55         if (self.strings.items.len >= std.math.maxInt(SmallestStringIndexType)) {
     56             return error.OutOfMemory; // ran out of string indexes
     57         }
     58         const dupe = try self.allocator.dupe(u8, str);
     59         const index = self.strings.items.len;
     60         try self.strings.append(self.allocator, dupe);
     61         return @intCast(index);
     62     }
     63 
     64     pub fn renderToStdErr(self: *Diagnostics, cwd: std.fs.Dir, source: []const u8, tty_config: std.io.tty.Config, source_mappings: ?SourceMappings) void {
     65         const stderr = std.debug.lockStderrWriter(&.{});
     66         defer std.debug.unlockStderrWriter();
     67         for (self.errors.items) |err_details| {
     68             renderErrorMessage(stderr, tty_config, cwd, err_details, source, self.strings.items, source_mappings) catch return;
     69         }
     70     }
     71 
     72     pub fn renderToStdErrDetectTTY(self: *Diagnostics, cwd: std.fs.Dir, source: []const u8, source_mappings: ?SourceMappings) void {
     73         const tty_config = std.io.tty.detectConfig(std.fs.File.stderr());
     74         return self.renderToStdErr(cwd, source, tty_config, source_mappings);
     75     }
     76 
     77     pub fn contains(self: *const Diagnostics, err: ErrorDetails.Error) bool {
     78         for (self.errors.items) |details| {
     79             if (details.err == err) return true;
     80         }
     81         return false;
     82     }
     83 
     84     pub fn containsAny(self: *const Diagnostics, errors: []const ErrorDetails.Error) bool {
     85         for (self.errors.items) |details| {
     86             for (errors) |err| {
     87                 if (details.err == err) return true;
     88             }
     89         }
     90         return false;
     91     }
     92 };
     93 
     94 /// Contains enough context to append errors/warnings/notes etc
     95 pub const DiagnosticsContext = struct {
     96     diagnostics: *Diagnostics,
     97     token: Token,
     98     /// Code page of the source file at the token location
     99     code_page: SupportedCodePage,
    100 };
    101 
    102 pub const ErrorDetails = struct {
    103     err: Error,
    104     token: Token,
    105     /// Code page of the source file at the token location
    106     code_page: SupportedCodePage,
    107     /// If non-null, should be before `token`. If null, `token` is assumed to be the start.
    108     token_span_start: ?Token = null,
    109     /// If non-null, should be after `token`. If null, `token` is assumed to be the end.
    110     token_span_end: ?Token = null,
    111     type: Type = .err,
    112     print_source_line: bool = true,
    113     extra: Extra = .{ .none = {} },
    114 
    115     pub const Type = enum {
    116         /// Fatal error, stops compilation
    117         err,
    118         /// Warning that does not affect compilation result
    119         warning,
    120         /// A note that typically provides further context for a warning/error
    121         note,
    122         /// An invisible diagnostic that is not printed to stderr but can
    123         /// provide information useful when comparing the behavior of different
    124         /// implementations. For example, a hint is emitted when a FONTDIR resource
    125         /// was included in the .RES file which is significant because rc.exe
    126         /// does something different than us, but ultimately it's not important
    127         /// enough to be a warning/note.
    128         hint,
    129     };
    130 
    131     pub const Extra = union {
    132         none: void,
    133         expected: Token.Id,
    134         number: u32,
    135         expected_types: ExpectedTypes,
    136         resource: rc.ResourceType,
    137         string_and_language: StringAndLanguage,
    138         file_open_error: FileOpenError,
    139         icon_read_error: IconReadError,
    140         icon_dir: IconDirContext,
    141         bmp_read_error: BitmapReadError,
    142         accelerator_error: AcceleratorError,
    143         statement_with_u16_param: StatementWithU16Param,
    144         menu_or_class: enum { class, menu },
    145     };
    146 
    147     comptime {
    148         // all fields in the extra union should be 32 bits or less
    149         for (std.meta.fields(Extra)) |field| {
    150             std.debug.assert(@bitSizeOf(field.type) <= 32);
    151         }
    152     }
    153 
    154     pub const StatementWithU16Param = enum(u32) {
    155         fileversion,
    156         productversion,
    157         language,
    158     };
    159 
    160     pub const StringAndLanguage = packed struct(u32) {
    161         id: u16,
    162         language: res.Language,
    163     };
    164 
    165     pub const FileOpenError = packed struct(u32) {
    166         err: FileOpenErrorEnum,
    167         filename_string_index: FilenameStringIndex,
    168 
    169         pub const FilenameStringIndex = std.meta.Int(.unsigned, 32 - @bitSizeOf(FileOpenErrorEnum));
    170         pub const FileOpenErrorEnum = std.meta.FieldEnum(std.fs.File.OpenError);
    171 
    172         pub fn enumFromError(err: std.fs.File.OpenError) FileOpenErrorEnum {
    173             return switch (err) {
    174                 inline else => |e| @field(ErrorDetails.FileOpenError.FileOpenErrorEnum, @errorName(e)),
    175             };
    176         }
    177     };
    178 
    179     pub const IconReadError = packed struct(u32) {
    180         err: IconReadErrorEnum,
    181         icon_type: enum(u1) { cursor, icon },
    182         filename_string_index: FilenameStringIndex,
    183 
    184         pub const FilenameStringIndex = std.meta.Int(.unsigned, 32 - @bitSizeOf(IconReadErrorEnum) - 1);
    185         pub const IconReadErrorEnum = std.meta.FieldEnum(ico.ReadError);
    186 
    187         pub fn enumFromError(err: ico.ReadError) IconReadErrorEnum {
    188             return switch (err) {
    189                 inline else => |e| @field(ErrorDetails.IconReadError.IconReadErrorEnum, @errorName(e)),
    190             };
    191         }
    192     };
    193 
    194     pub const IconDirContext = packed struct(u32) {
    195         icon_type: enum(u1) { cursor, icon },
    196         icon_format: ico.ImageFormat,
    197         index: u16,
    198         bitmap_version: ico.BitmapHeader.Version = .unknown,
    199         _: Padding = 0,
    200 
    201         pub const Padding = std.meta.Int(.unsigned, 15 - @bitSizeOf(ico.BitmapHeader.Version) - @bitSizeOf(ico.ImageFormat));
    202     };
    203 
    204     pub const BitmapReadError = packed struct(u32) {
    205         err: BitmapReadErrorEnum,
    206         filename_string_index: FilenameStringIndex,
    207 
    208         pub const FilenameStringIndex = std.meta.Int(.unsigned, 32 - @bitSizeOf(BitmapReadErrorEnum));
    209         pub const BitmapReadErrorEnum = std.meta.FieldEnum(bmp.ReadError);
    210 
    211         pub fn enumFromError(err: bmp.ReadError) BitmapReadErrorEnum {
    212             return switch (err) {
    213                 inline else => |e| @field(ErrorDetails.BitmapReadError.BitmapReadErrorEnum, @errorName(e)),
    214             };
    215         }
    216     };
    217 
    218     pub const BitmapUnsupportedDIB = packed struct(u32) {
    219         dib_version: ico.BitmapHeader.Version,
    220         filename_string_index: FilenameStringIndex,
    221 
    222         pub const FilenameStringIndex = std.meta.Int(.unsigned, 32 - @bitSizeOf(ico.BitmapHeader.Version));
    223     };
    224 
    225     pub const AcceleratorError = packed struct(u32) {
    226         err: AcceleratorErrorEnum,
    227         _: Padding = 0,
    228 
    229         pub const Padding = std.meta.Int(.unsigned, 32 - @bitSizeOf(AcceleratorErrorEnum));
    230         pub const AcceleratorErrorEnum = std.meta.FieldEnum(res.ParseAcceleratorKeyStringError);
    231 
    232         pub fn enumFromError(err: res.ParseAcceleratorKeyStringError) AcceleratorErrorEnum {
    233             return switch (err) {
    234                 inline else => |e| @field(ErrorDetails.AcceleratorError.AcceleratorErrorEnum, @errorName(e)),
    235             };
    236         }
    237     };
    238 
    239     pub const ExpectedTypes = packed struct(u32) {
    240         number: bool = false,
    241         number_expression: bool = false,
    242         string_literal: bool = false,
    243         accelerator_type_or_option: bool = false,
    244         control_class: bool = false,
    245         literal: bool = false,
    246         // Note: This being 0 instead of undefined is arbitrary and something of a workaround,
    247         //       see https://github.com/ziglang/zig/issues/15395
    248         _: u26 = 0,
    249 
    250         pub const strings = std.StaticStringMap([]const u8).initComptime(.{
    251             .{ "number", "number" },
    252             .{ "number_expression", "number expression" },
    253             .{ "string_literal", "quoted string literal" },
    254             .{ "accelerator_type_or_option", "accelerator type or option [ASCII, VIRTKEY, etc]" },
    255             .{ "control_class", "control class [BUTTON, EDIT, etc]" },
    256             .{ "literal", "unquoted literal" },
    257         });
    258 
    259         pub fn writeCommaSeparated(self: ExpectedTypes, writer: anytype) !void {
    260             const struct_info = @typeInfo(ExpectedTypes).@"struct";
    261             const num_real_fields = struct_info.fields.len - 1;
    262             const num_padding_bits = @bitSizeOf(ExpectedTypes) - num_real_fields;
    263             const mask = std.math.maxInt(struct_info.backing_integer.?) >> num_padding_bits;
    264             const relevant_bits_only = @as(struct_info.backing_integer.?, @bitCast(self)) & mask;
    265             const num_set_bits = @popCount(relevant_bits_only);
    266 
    267             var i: usize = 0;
    268             inline for (struct_info.fields) |field_info| {
    269                 if (field_info.type != bool) continue;
    270                 if (i == num_set_bits) return;
    271                 if (@field(self, field_info.name)) {
    272                     try writer.writeAll(strings.get(field_info.name).?);
    273                     i += 1;
    274                     if (num_set_bits > 2 and i != num_set_bits) {
    275                         try writer.writeAll(", ");
    276                     } else if (i != num_set_bits) {
    277                         try writer.writeByte(' ');
    278                     }
    279                     if (num_set_bits > 1 and i == num_set_bits - 1) {
    280                         try writer.writeAll("or ");
    281                     }
    282                 }
    283             }
    284         }
    285     };
    286 
    287     pub const Error = enum {
    288         // Lexer
    289         unfinished_string_literal,
    290         string_literal_too_long,
    291         invalid_number_with_exponent,
    292         invalid_digit_character_in_number_literal,
    293         illegal_byte,
    294         illegal_byte_outside_string_literals,
    295         illegal_codepoint_outside_string_literals,
    296         illegal_byte_order_mark,
    297         illegal_private_use_character,
    298         found_c_style_escaped_quote,
    299         code_page_pragma_missing_left_paren,
    300         code_page_pragma_missing_right_paren,
    301         code_page_pragma_invalid_code_page,
    302         code_page_pragma_not_integer,
    303         code_page_pragma_overflow,
    304         code_page_pragma_unsupported_code_page,
    305 
    306         // Parser
    307         unfinished_raw_data_block,
    308         unfinished_string_table_block,
    309         /// `expected` is populated.
    310         expected_token,
    311         /// `expected_types` is populated
    312         expected_something_else,
    313         /// `resource` is populated
    314         resource_type_cant_use_raw_data,
    315         /// `resource` is populated
    316         id_must_be_ordinal,
    317         /// `resource` is populated
    318         name_or_id_not_allowed,
    319         string_resource_as_numeric_type,
    320         ascii_character_not_equivalent_to_virtual_key_code,
    321         empty_menu_not_allowed,
    322         rc_would_miscompile_version_value_padding,
    323         rc_would_miscompile_version_value_byte_count,
    324         code_page_pragma_in_included_file,
    325         nested_resource_level_exceeds_max,
    326         too_many_dialog_controls_or_toolbar_buttons,
    327         nested_expression_level_exceeds_max,
    328         close_paren_expression,
    329         unary_plus_expression,
    330         rc_could_miscompile_control_params,
    331         dangling_literal_at_eof,
    332         disjoint_code_page,
    333 
    334         // Compiler
    335         /// `string_and_language` is populated
    336         string_already_defined,
    337         font_id_already_defined,
    338         /// `file_open_error` is populated
    339         file_open_error,
    340         /// `accelerator_error` is populated
    341         invalid_accelerator_key,
    342         accelerator_type_required,
    343         accelerator_shift_or_control_without_virtkey,
    344         rc_would_miscompile_control_padding,
    345         rc_would_miscompile_control_class_ordinal,
    346         /// `icon_dir` is populated
    347         rc_would_error_on_icon_dir,
    348         /// `icon_dir` is populated
    349         format_not_supported_in_icon_dir,
    350         /// `resource` is populated and contains the expected type
    351         icon_dir_and_resource_type_mismatch,
    352         /// `icon_read_error` is populated
    353         icon_read_error,
    354         /// `icon_dir` is populated
    355         rc_would_error_on_bitmap_version,
    356         /// `icon_dir` is populated
    357         max_icon_ids_exhausted,
    358         /// `bmp_read_error` is populated
    359         bmp_read_error,
    360         /// `number` is populated and contains a string index for which the string contains
    361         /// the bytes of a `u64` (native endian). The `u64` contains the number of ignored bytes.
    362         bmp_ignored_palette_bytes,
    363         /// `number` is populated and contains a string index for which the string contains
    364         /// the bytes of a `u64` (native endian). The `u64` contains the number of missing bytes.
    365         bmp_missing_palette_bytes,
    366         /// `number` is populated and contains a string index for which the string contains
    367         /// the bytes of a `u64` (native endian). The `u64` contains the number of miscompiled bytes.
    368         rc_would_miscompile_bmp_palette_padding,
    369         resource_header_size_exceeds_max,
    370         resource_data_size_exceeds_max,
    371         control_extra_data_size_exceeds_max,
    372         version_node_size_exceeds_max,
    373         fontdir_size_exceeds_max,
    374         /// `number` is populated and contains a string index for the filename
    375         number_expression_as_filename,
    376         /// `number` is populated and contains the control ID that is a duplicate
    377         control_id_already_defined,
    378         /// `number` is populated and contains the disallowed codepoint
    379         invalid_filename,
    380         /// `statement_with_u16_param` is populated
    381         rc_would_error_u16_with_l_suffix,
    382         result_contains_fontdir,
    383         /// `number` is populated and contains the ordinal value that the id would be miscompiled to
    384         rc_would_miscompile_dialog_menu_id,
    385         /// `number` is populated and contains the ordinal value that the value would be miscompiled to
    386         rc_would_miscompile_dialog_class,
    387         /// `menu_or_class` is populated and contains the type of the parameter statement
    388         rc_would_miscompile_dialog_menu_or_class_id_forced_ordinal,
    389         rc_would_miscompile_dialog_menu_id_starts_with_digit,
    390         dialog_menu_id_was_uppercased,
    391         duplicate_optional_statement_skipped,
    392         invalid_digit_character_in_ordinal,
    393 
    394         // Literals
    395         /// `number` is populated
    396         rc_would_miscompile_codepoint_whitespace,
    397         /// `number` is populated
    398         rc_would_miscompile_codepoint_skip,
    399         /// `number` is populated
    400         rc_would_miscompile_codepoint_bom,
    401         tab_converted_to_spaces,
    402 
    403         // General (used in various places)
    404         /// `number` is populated and contains the value that the ordinal would have in the Win32 RC compiler implementation
    405         win32_non_ascii_ordinal,
    406 
    407         // Initialization
    408         /// `file_open_error` is populated, but `filename_string_index` is not
    409         failed_to_open_cwd,
    410     };
    411 
    412     fn formatToken(ctx: TokenFormatContext, writer: *std.io.Writer) std.io.Writer.Error!void {
    413         switch (ctx.token.id) {
    414             .eof => return writer.writeAll(ctx.token.id.nameForErrorDisplay()),
    415             else => {},
    416         }
    417 
    418         const slice = ctx.token.slice(ctx.source);
    419         var src_i: usize = 0;
    420         while (src_i < slice.len) {
    421             const codepoint = ctx.code_page.codepointAt(src_i, slice) orelse break;
    422             defer src_i += codepoint.byte_len;
    423             const display_codepoint = codepointForDisplay(codepoint) orelse continue;
    424             var buf: [4]u8 = undefined;
    425             const utf8_len = std.unicode.utf8Encode(display_codepoint, &buf) catch unreachable;
    426             try writer.writeAll(buf[0..utf8_len]);
    427         }
    428     }
    429 
    430     const TokenFormatContext = struct {
    431         token: Token,
    432         source: []const u8,
    433         code_page: SupportedCodePage,
    434     };
    435 
    436     fn fmtToken(self: ErrorDetails, source: []const u8) std.fmt.Formatter(TokenFormatContext, formatToken) {
    437         return .{ .data = .{
    438             .token = self.token,
    439             .code_page = self.code_page,
    440             .source = source,
    441         } };
    442     }
    443 
    444     pub fn render(self: ErrorDetails, writer: anytype, source: []const u8, strings: []const []const u8) !void {
    445         switch (self.err) {
    446             .unfinished_string_literal => {
    447                 return writer.print("unfinished string literal at '{f}', expected closing '\"'", .{self.fmtToken(source)});
    448             },
    449             .string_literal_too_long => {
    450                 return writer.print("string literal too long (max is currently {} characters)", .{self.extra.number});
    451             },
    452             .invalid_number_with_exponent => {
    453                 return writer.print("base 10 number literal with exponent is not allowed: {s}", .{self.token.slice(source)});
    454             },
    455             .invalid_digit_character_in_number_literal => switch (self.type) {
    456                 .err, .warning => return writer.writeAll("non-ASCII digit characters are not allowed in number literals"),
    457                 .note => return writer.writeAll("the Win32 RC compiler allows non-ASCII digit characters, but will miscompile them"),
    458                 .hint => return,
    459             },
    460             .illegal_byte => {
    461                 return writer.print("character '{f}' is not allowed", .{
    462                     std.ascii.hexEscape(self.token.slice(source), .upper),
    463                 });
    464             },
    465             .illegal_byte_outside_string_literals => {
    466                 return writer.print("character '{f}' is not allowed outside of string literals", .{
    467                     std.ascii.hexEscape(self.token.slice(source), .upper),
    468                 });
    469             },
    470             .illegal_codepoint_outside_string_literals => {
    471                 // This is somewhat hacky, but we know that:
    472                 //  - This error is only possible with codepoints outside of the Windows-1252 character range
    473                 //  - So, the only supported code page that could generate this error is UTF-8
    474                 // Therefore, we just assume the token bytes are UTF-8 and decode them to get the illegal
    475                 // codepoint.
    476                 //
    477                 // FIXME: Support other code pages if they become relevant
    478                 const bytes = self.token.slice(source);
    479                 const codepoint = std.unicode.utf8Decode(bytes) catch unreachable;
    480                 return writer.print("codepoint <U+{X:0>4}> is not allowed outside of string literals", .{codepoint});
    481             },
    482             .illegal_byte_order_mark => {
    483                 return writer.writeAll("byte order mark <U+FEFF> is not allowed");
    484             },
    485             .illegal_private_use_character => {
    486                 return writer.writeAll("private use character <U+E000> is not allowed");
    487             },
    488             .found_c_style_escaped_quote => {
    489                 return writer.writeAll("escaping quotes with \\\" is not allowed (use \"\" instead)");
    490             },
    491             .code_page_pragma_missing_left_paren => {
    492                 return writer.writeAll("expected left parenthesis after 'code_page' in #pragma code_page");
    493             },
    494             .code_page_pragma_missing_right_paren => {
    495                 return writer.writeAll("expected right parenthesis after '<number>' in #pragma code_page");
    496             },
    497             .code_page_pragma_invalid_code_page => {
    498                 return writer.writeAll("invalid or unknown code page in #pragma code_page");
    499             },
    500             .code_page_pragma_not_integer => {
    501                 return writer.writeAll("code page is not a valid integer in #pragma code_page");
    502             },
    503             .code_page_pragma_overflow => {
    504                 return writer.writeAll("code page too large in #pragma code_page");
    505             },
    506             .code_page_pragma_unsupported_code_page => {
    507                 // We know that the token slice is a well-formed #pragma code_page(N), so
    508                 // we can skip to the first ( and then get the number that follows
    509                 const token_slice = self.token.slice(source);
    510                 var number_start = std.mem.indexOfScalar(u8, token_slice, '(').? + 1;
    511                 while (std.ascii.isWhitespace(token_slice[number_start])) {
    512                     number_start += 1;
    513                 }
    514                 var number_slice = token_slice[number_start..number_start];
    515                 while (std.ascii.isDigit(token_slice[number_start + number_slice.len])) {
    516                     number_slice.len += 1;
    517                 }
    518                 const number = std.fmt.parseUnsigned(u16, number_slice, 10) catch unreachable;
    519                 const code_page = code_pages.getByIdentifier(number) catch unreachable;
    520                 // TODO: Improve or maybe add a note making it more clear that the code page
    521                 //       is valid and that the code page is unsupported purely due to a limitation
    522                 //       in this compiler.
    523                 return writer.print("unsupported code page '{s} (id={})' in #pragma code_page", .{ @tagName(code_page), number });
    524             },
    525             .unfinished_raw_data_block => {
    526                 return writer.print("unfinished raw data block at '{f}', expected closing '}}' or 'END'", .{self.fmtToken(source)});
    527             },
    528             .unfinished_string_table_block => {
    529                 return writer.print("unfinished STRINGTABLE block at '{f}', expected closing '}}' or 'END'", .{self.fmtToken(source)});
    530             },
    531             .expected_token => {
    532                 return writer.print("expected '{s}', got '{f}'", .{ self.extra.expected.nameForErrorDisplay(), self.fmtToken(source) });
    533             },
    534             .expected_something_else => {
    535                 try writer.writeAll("expected ");
    536                 try self.extra.expected_types.writeCommaSeparated(writer);
    537                 return writer.print("; got '{f}'", .{self.fmtToken(source)});
    538             },
    539             .resource_type_cant_use_raw_data => switch (self.type) {
    540                 .err, .warning => try writer.print("expected '<filename>', found '{f}' (resource type '{s}' can't use raw data)", .{ self.fmtToken(source), self.extra.resource.nameForErrorDisplay() }),
    541                 .note => try writer.print("if '{f}' is intended to be a filename, it must be specified as a quoted string literal", .{self.fmtToken(source)}),
    542                 .hint => return,
    543             },
    544             .id_must_be_ordinal => {
    545                 try writer.print("id of resource type '{s}' must be an ordinal (u16), got '{f}'", .{ self.extra.resource.nameForErrorDisplay(), self.fmtToken(source) });
    546             },
    547             .name_or_id_not_allowed => {
    548                 try writer.print("name or id is not allowed for resource type '{s}'", .{self.extra.resource.nameForErrorDisplay()});
    549             },
    550             .string_resource_as_numeric_type => switch (self.type) {
    551                 .err, .warning => try writer.writeAll("the number 6 (RT_STRING) cannot be used as a resource type"),
    552                 .note => try writer.writeAll("using RT_STRING directly likely results in an invalid .res file, use a STRINGTABLE instead"),
    553                 .hint => return,
    554             },
    555             .ascii_character_not_equivalent_to_virtual_key_code => {
    556                 // TODO: Better wording? This is what the Win32 RC compiler emits.
    557                 //       This occurs when VIRTKEY and a control code is specified ("^c", etc)
    558                 try writer.writeAll("ASCII character not equivalent to virtual key code");
    559             },
    560             .empty_menu_not_allowed => {
    561                 try writer.print("empty menu of type '{f}' not allowed", .{self.fmtToken(source)});
    562             },
    563             .rc_would_miscompile_version_value_padding => switch (self.type) {
    564                 .err, .warning => return writer.print("the padding before this quoted string value would be miscompiled by the Win32 RC compiler", .{}),
    565                 .note => return writer.print("to avoid the potential miscompilation, consider adding a comma between the key and the quoted string", .{}),
    566                 .hint => return,
    567             },
    568             .rc_would_miscompile_version_value_byte_count => switch (self.type) {
    569                 .err, .warning => return writer.print("the byte count of this value would be miscompiled by the Win32 RC compiler", .{}),
    570                 .note => return writer.print("to avoid the potential miscompilation, do not mix numbers and strings within a value", .{}),
    571                 .hint => return,
    572             },
    573             .code_page_pragma_in_included_file => {
    574                 try writer.print("#pragma code_page is not supported in an included resource file", .{});
    575             },
    576             .nested_resource_level_exceeds_max => switch (self.type) {
    577                 .err, .warning => {
    578                     const max = switch (self.extra.resource) {
    579                         .versioninfo => parse.max_nested_version_level,
    580                         .menu, .menuex => parse.max_nested_menu_level,
    581                         else => unreachable,
    582                     };
    583                     return writer.print("{s} contains too many nested children (max is {})", .{ self.extra.resource.nameForErrorDisplay(), max });
    584                 },
    585                 .note => return writer.print("max {s} nesting level exceeded here", .{self.extra.resource.nameForErrorDisplay()}),
    586                 .hint => return,
    587             },
    588             .too_many_dialog_controls_or_toolbar_buttons => switch (self.type) {
    589                 .err, .warning => return writer.print("{s} contains too many {s} (max is {})", .{ self.extra.resource.nameForErrorDisplay(), switch (self.extra.resource) {
    590                     .toolbar => "buttons",
    591                     else => "controls",
    592                 }, std.math.maxInt(u16) }),
    593                 .note => return writer.print("maximum number of {s} exceeded here", .{switch (self.extra.resource) {
    594                     .toolbar => "buttons",
    595                     else => "controls",
    596                 }}),
    597                 .hint => return,
    598             },
    599             .nested_expression_level_exceeds_max => switch (self.type) {
    600                 .err, .warning => return writer.print("expression contains too many syntax levels (max is {})", .{parse.max_nested_expression_level}),
    601                 .note => return writer.print("maximum expression level exceeded here", .{}),
    602                 .hint => return,
    603             },
    604             .close_paren_expression => {
    605                 try writer.writeAll("the Win32 RC compiler would accept ')' as a valid expression, but it would be skipped over and potentially lead to unexpected outcomes");
    606             },
    607             .unary_plus_expression => {
    608                 try writer.writeAll("the Win32 RC compiler may accept '+' as a unary operator here, but it is not supported in this implementation; consider omitting the unary +");
    609             },
    610             .rc_could_miscompile_control_params => switch (self.type) {
    611                 .err, .warning => return writer.print("this token could be erroneously skipped over by the Win32 RC compiler", .{}),
    612                 .note => return writer.print("to avoid the potential miscompilation, consider adding a comma after the style parameter", .{}),
    613                 .hint => return,
    614             },
    615             .dangling_literal_at_eof => {
    616                 try writer.writeAll("dangling literal at end-of-file; this is not a problem, but it is likely a mistake");
    617             },
    618             .disjoint_code_page => switch (self.type) {
    619                 .err, .warning => return writer.print("#pragma code_page as the first thing in the .rc script can cause the input and output code pages to become out-of-sync", .{}),
    620                 .note => return writer.print("to avoid unexpected behavior, add a comment (or anything else) above the #pragma code_page line", .{}),
    621                 .hint => return,
    622             },
    623             .string_already_defined => switch (self.type) {
    624                 .err, .warning => {
    625                     const language = self.extra.string_and_language.language;
    626                     return writer.print("string with id {d} (0x{X}) already defined for language {f}", .{ self.extra.string_and_language.id, self.extra.string_and_language.id, language });
    627                 },
    628                 .note => return writer.print("previous definition of string with id {d} (0x{X}) here", .{ self.extra.string_and_language.id, self.extra.string_and_language.id }),
    629                 .hint => return,
    630             },
    631             .font_id_already_defined => switch (self.type) {
    632                 .err => return writer.print("font with id {d} already defined", .{self.extra.number}),
    633                 .warning => return writer.print("skipped duplicate font with id {d}", .{self.extra.number}),
    634                 .note => return writer.print("previous definition of font with id {d} here", .{self.extra.number}),
    635                 .hint => return,
    636             },
    637             .file_open_error => {
    638                 try writer.print("unable to open file '{s}': {s}", .{ strings[self.extra.file_open_error.filename_string_index], @tagName(self.extra.file_open_error.err) });
    639             },
    640             .invalid_accelerator_key => {
    641                 try writer.print("invalid accelerator key '{f}': {s}", .{ self.fmtToken(source), @tagName(self.extra.accelerator_error.err) });
    642             },
    643             .accelerator_type_required => {
    644                 try writer.writeAll("accelerator type [ASCII or VIRTKEY] required when key is an integer");
    645             },
    646             .accelerator_shift_or_control_without_virtkey => {
    647                 try writer.writeAll("SHIFT or CONTROL used without VIRTKEY");
    648             },
    649             .rc_would_miscompile_control_padding => switch (self.type) {
    650                 .err, .warning => return writer.print("the padding before this control would be miscompiled by the Win32 RC compiler (it would insert 2 extra bytes of padding)", .{}),
    651                 .note => return writer.print("to avoid the potential miscompilation, consider adding one more byte to the control data of the control preceding this one", .{}),
    652                 .hint => return,
    653             },
    654             .rc_would_miscompile_control_class_ordinal => switch (self.type) {
    655                 .err, .warning => return writer.print("the control class of this CONTROL would be miscompiled by the Win32 RC compiler", .{}),
    656                 .note => return writer.print("to avoid the potential miscompilation, consider specifying the control class using a string (BUTTON, EDIT, etc) instead of a number", .{}),
    657                 .hint => return,
    658             },
    659             .rc_would_error_on_icon_dir => switch (self.type) {
    660                 .err, .warning => return writer.print("the resource at index {} of this {s} has the format '{s}'; this would be an error in the Win32 RC compiler", .{ self.extra.icon_dir.index, @tagName(self.extra.icon_dir.icon_type), @tagName(self.extra.icon_dir.icon_format) }),
    661                 .note => {
    662                     // The only note supported is one specific to exactly this combination
    663                     if (!(self.extra.icon_dir.icon_type == .icon and self.extra.icon_dir.icon_format == .riff)) unreachable;
    664                     try writer.print("animated RIFF icons within resource groups may not be well supported, consider using an animated icon file (.ani) instead", .{});
    665                 },
    666                 .hint => return,
    667             },
    668             .format_not_supported_in_icon_dir => {
    669                 try writer.print("resource with format '{s}' (at index {}) is not allowed in {s} resource groups", .{ @tagName(self.extra.icon_dir.icon_format), self.extra.icon_dir.index, @tagName(self.extra.icon_dir.icon_type) });
    670             },
    671             .icon_dir_and_resource_type_mismatch => {
    672                 const unexpected_type: rc.ResourceType = if (self.extra.resource == .icon) .cursor else .icon;
    673                 // TODO: Better wording
    674                 try writer.print("resource type '{s}' does not match type '{s}' specified in the file", .{ self.extra.resource.nameForErrorDisplay(), unexpected_type.nameForErrorDisplay() });
    675             },
    676             .icon_read_error => {
    677                 try writer.print("unable to read {s} file '{s}': {s}", .{ @tagName(self.extra.icon_read_error.icon_type), strings[self.extra.icon_read_error.filename_string_index], @tagName(self.extra.icon_read_error.err) });
    678             },
    679             .rc_would_error_on_bitmap_version => switch (self.type) {
    680                 .err => try writer.print("the DIB at index {} of this {s} is of version '{s}'; this version is no longer allowed and should be upgraded to '{s}'", .{
    681                     self.extra.icon_dir.index,
    682                     @tagName(self.extra.icon_dir.icon_type),
    683                     self.extra.icon_dir.bitmap_version.nameForErrorDisplay(),
    684                     ico.BitmapHeader.Version.@"nt3.1".nameForErrorDisplay(),
    685                 }),
    686                 .warning => try writer.print("the DIB at index {} of this {s} is of version '{s}'; this would be an error in the Win32 RC compiler", .{
    687                     self.extra.icon_dir.index,
    688                     @tagName(self.extra.icon_dir.icon_type),
    689                     self.extra.icon_dir.bitmap_version.nameForErrorDisplay(),
    690                 }),
    691                 .note => unreachable,
    692                 .hint => return,
    693             },
    694             .max_icon_ids_exhausted => switch (self.type) {
    695                 .err, .warning => try writer.print("maximum global icon/cursor ids exhausted (max is {})", .{std.math.maxInt(u16) - 1}),
    696                 .note => try writer.print("maximum icon/cursor id exceeded at index {} of this {s}", .{ self.extra.icon_dir.index, @tagName(self.extra.icon_dir.icon_type) }),
    697                 .hint => return,
    698             },
    699             .bmp_read_error => {
    700                 try writer.print("invalid bitmap file '{s}': {s}", .{ strings[self.extra.bmp_read_error.filename_string_index], @tagName(self.extra.bmp_read_error.err) });
    701             },
    702             .bmp_ignored_palette_bytes => {
    703                 const bytes = strings[self.extra.number];
    704                 const ignored_bytes = std.mem.readInt(u64, bytes[0..8], native_endian);
    705                 try writer.print("bitmap has {d} extra bytes preceding the pixel data which will be ignored", .{ignored_bytes});
    706             },
    707             .bmp_missing_palette_bytes => {
    708                 const bytes = strings[self.extra.number];
    709                 const missing_bytes = std.mem.readInt(u64, bytes[0..8], native_endian);
    710                 try writer.print("bitmap has {d} missing color palette bytes", .{missing_bytes});
    711             },
    712             .rc_would_miscompile_bmp_palette_padding => {
    713                 try writer.writeAll("the Win32 RC compiler would erroneously pad out the missing bytes");
    714                 if (self.extra.number != 0) {
    715                     const bytes = strings[self.extra.number];
    716                     const miscompiled_bytes = std.mem.readInt(u64, bytes[0..8], native_endian);
    717                     try writer.print(" (and the added padding bytes would include {d} bytes of the pixel data)", .{miscompiled_bytes});
    718                 }
    719             },
    720             .resource_header_size_exceeds_max => {
    721                 try writer.print("resource's header length exceeds maximum of {} bytes", .{std.math.maxInt(u32)});
    722             },
    723             .resource_data_size_exceeds_max => switch (self.type) {
    724                 .err, .warning => return writer.print("resource's data length exceeds maximum of {} bytes", .{std.math.maxInt(u32)}),
    725                 .note => return writer.print("maximum data length exceeded here", .{}),
    726                 .hint => return,
    727             },
    728             .control_extra_data_size_exceeds_max => switch (self.type) {
    729                 .err, .warning => try writer.print("control data length exceeds maximum of {} bytes", .{std.math.maxInt(u16)}),
    730                 .note => return writer.print("maximum control data length exceeded here", .{}),
    731                 .hint => return,
    732             },
    733             .version_node_size_exceeds_max => switch (self.type) {
    734                 .err, .warning => return writer.print("version node tree size exceeds maximum of {} bytes", .{std.math.maxInt(u16)}),
    735                 .note => return writer.print("maximum tree size exceeded while writing this child", .{}),
    736                 .hint => return,
    737             },
    738             .fontdir_size_exceeds_max => switch (self.type) {
    739                 .err, .warning => return writer.print("FONTDIR data length exceeds maximum of {} bytes", .{std.math.maxInt(u32)}),
    740                 .note => return writer.writeAll("this is likely due to the size of the combined lengths of the device/face names of all FONT resources"),
    741                 .hint => return,
    742             },
    743             .number_expression_as_filename => switch (self.type) {
    744                 .err, .warning => return writer.writeAll("filename cannot be specified using a number expression, consider using a quoted string instead"),
    745                 .note => return writer.print("the Win32 RC compiler would evaluate this number expression as the filename '{s}'", .{strings[self.extra.number]}),
    746                 .hint => return,
    747             },
    748             .control_id_already_defined => switch (self.type) {
    749                 .err, .warning => return writer.print("control with id {d} already defined for this dialog", .{self.extra.number}),
    750                 .note => return writer.print("previous definition of control with id {d} here", .{self.extra.number}),
    751                 .hint => return,
    752             },
    753             .invalid_filename => {
    754                 const disallowed_codepoint = self.extra.number;
    755                 if (disallowed_codepoint < 128 and std.ascii.isPrint(@intCast(disallowed_codepoint))) {
    756                     try writer.print("evaluated filename contains a disallowed character: '{c}'", .{@as(u8, @intCast(disallowed_codepoint))});
    757                 } else {
    758                     try writer.print("evaluated filename contains a disallowed codepoint: <U+{X:0>4}>", .{disallowed_codepoint});
    759                 }
    760             },
    761             .rc_would_error_u16_with_l_suffix => switch (self.type) {
    762                 .err, .warning => return writer.print("this {s} parameter would be an error in the Win32 RC compiler", .{@tagName(self.extra.statement_with_u16_param)}),
    763                 .note => return writer.writeAll("to avoid the error, remove any L suffixes from numbers within the parameter"),
    764                 .hint => return,
    765             },
    766             .result_contains_fontdir => return,
    767             .rc_would_miscompile_dialog_menu_id => switch (self.type) {
    768                 .err, .warning => return writer.print("the id of this menu would be miscompiled by the Win32 RC compiler", .{}),
    769                 .note => return writer.print("the Win32 RC compiler would evaluate the id as the ordinal/number value {d}", .{self.extra.number}),
    770                 .hint => return,
    771             },
    772             .rc_would_miscompile_dialog_class => switch (self.type) {
    773                 .err, .warning => return writer.print("this class would be miscompiled by the Win32 RC compiler", .{}),
    774                 .note => return writer.print("the Win32 RC compiler would evaluate it as the ordinal/number value {d}", .{self.extra.number}),
    775                 .hint => return,
    776             },
    777             .rc_would_miscompile_dialog_menu_or_class_id_forced_ordinal => switch (self.type) {
    778                 .err, .warning => return,
    779                 .note => return writer.print("to avoid the potential miscompilation, only specify one {s} per dialog resource", .{@tagName(self.extra.menu_or_class)}),
    780                 .hint => return,
    781             },
    782             .rc_would_miscompile_dialog_menu_id_starts_with_digit => switch (self.type) {
    783                 .err, .warning => return,
    784                 .note => return writer.writeAll("to avoid the potential miscompilation, the first character of the id should not be a digit"),
    785                 .hint => return,
    786             },
    787             .dialog_menu_id_was_uppercased => return,
    788             .duplicate_optional_statement_skipped => {
    789                 return writer.writeAll("this statement was ignored; when multiple statements of the same type are specified, only the last takes precedence");
    790             },
    791             .invalid_digit_character_in_ordinal => {
    792                 return writer.writeAll("non-ASCII digit characters are not allowed in ordinal (number) values");
    793             },
    794             .rc_would_miscompile_codepoint_whitespace => {
    795                 const treated_as = self.extra.number >> 8;
    796                 return writer.print("codepoint U+{X:0>4} within a string literal would be miscompiled by the Win32 RC compiler (it would get treated as U+{X:0>4})", .{ self.extra.number, treated_as });
    797             },
    798             .rc_would_miscompile_codepoint_skip => {
    799                 return writer.print("codepoint U+{X:0>4} within a string literal would be miscompiled by the Win32 RC compiler (the codepoint would be missing from the compiled resource)", .{self.extra.number});
    800             },
    801             .rc_would_miscompile_codepoint_bom => switch (self.type) {
    802                 .err, .warning => return writer.print("codepoint U+{X:0>4} within a string literal would cause the entire file to be miscompiled by the Win32 RC compiler", .{self.extra.number}),
    803                 .note => return writer.writeAll("the presence of this codepoint causes all non-ASCII codepoints to be byteswapped by the Win32 RC preprocessor"),
    804                 .hint => return,
    805             },
    806             .tab_converted_to_spaces => switch (self.type) {
    807                 .err, .warning => return writer.writeAll("the tab character(s) in this string will be converted into a variable number of spaces (determined by the column of the tab character in the .rc file)"),
    808                 .note => return writer.writeAll("to include the tab character itself in a string, the escape sequence \\t should be used"),
    809                 .hint => return,
    810             },
    811             .win32_non_ascii_ordinal => switch (self.type) {
    812                 .err, .warning => unreachable,
    813                 .note => return writer.print("the Win32 RC compiler would accept this as an ordinal but its value would be {}", .{self.extra.number}),
    814                 .hint => return,
    815             },
    816             .failed_to_open_cwd => {
    817                 try writer.print("failed to open CWD for compilation: {s}", .{@tagName(self.extra.file_open_error.err)});
    818             },
    819         }
    820     }
    821 
    822     pub const VisualTokenInfo = struct {
    823         before_len: usize,
    824         point_offset: usize,
    825         after_len: usize,
    826     };
    827 
    828     pub fn visualTokenInfo(self: ErrorDetails, source_line_start: usize, source_line_end: usize, source: []const u8) VisualTokenInfo {
    829         return switch (self.err) {
    830             // These can technically be more than 1 byte depending on encoding,
    831             // but they always refer to one visual character/grapheme.
    832             .illegal_byte,
    833             .illegal_byte_outside_string_literals,
    834             .illegal_codepoint_outside_string_literals,
    835             .illegal_byte_order_mark,
    836             .illegal_private_use_character,
    837             => .{
    838                 .before_len = 0,
    839                 .point_offset = cellCount(self.code_page, source, source_line_start, self.token.start),
    840                 .after_len = 0,
    841             },
    842             else => .{
    843                 .before_len = before: {
    844                     const start = @max(source_line_start, if (self.token_span_start) |span_start| span_start.start else self.token.start);
    845                     break :before cellCount(self.code_page, source, start, self.token.start);
    846                 },
    847                 .point_offset = cellCount(self.code_page, source, source_line_start, self.token.start),
    848                 .after_len = after: {
    849                     const end = @min(source_line_end, if (self.token_span_end) |span_end| span_end.end else self.token.end);
    850                     // end may be less than start when pointing to EOF
    851                     if (end <= self.token.start) break :after 0;
    852                     break :after cellCount(self.code_page, source, self.token.start, end) - 1;
    853                 },
    854             },
    855         };
    856     }
    857 };
    858 
    859 /// Convenience struct only useful when the code page can be inferred from the token
    860 pub const ErrorDetailsWithoutCodePage = blk: {
    861     const details_info = @typeInfo(ErrorDetails);
    862     const fields = details_info.@"struct".fields;
    863     var fields_without_codepage: [fields.len - 1]std.builtin.Type.StructField = undefined;
    864     var i: usize = 0;
    865     for (fields) |field| {
    866         if (std.mem.eql(u8, field.name, "code_page")) continue;
    867         fields_without_codepage[i] = field;
    868         i += 1;
    869     }
    870     std.debug.assert(i == fields_without_codepage.len);
    871     break :blk @Type(.{ .@"struct" = .{
    872         .layout = .auto,
    873         .fields = &fields_without_codepage,
    874         .decls = &.{},
    875         .is_tuple = false,
    876     } });
    877 };
    878 
    879 fn cellCount(code_page: SupportedCodePage, source: []const u8, start_index: usize, end_index: usize) usize {
    880     // Note: This is an imperfect solution. A proper implementation here would
    881     //       involve full grapheme cluster awareness + grapheme width data, but oh well.
    882     var codepoint_count: usize = 0;
    883     var index: usize = start_index;
    884     while (index < end_index) {
    885         const codepoint = code_page.codepointAt(index, source) orelse break;
    886         defer index += codepoint.byte_len;
    887         _ = codepointForDisplay(codepoint) orelse continue;
    888         codepoint_count += 1;
    889         // no need to count more than we will display
    890         if (codepoint_count >= max_source_line_codepoints + truncated_str.len) break;
    891     }
    892     return codepoint_count;
    893 }
    894 
    895 const truncated_str = "<...truncated...>";
    896 
    897 pub fn renderErrorMessage(writer: *std.io.Writer, tty_config: std.io.tty.Config, cwd: std.fs.Dir, err_details: ErrorDetails, source: []const u8, strings: []const []const u8, source_mappings: ?SourceMappings) !void {
    898     if (err_details.type == .hint) return;
    899 
    900     const source_line_start = err_details.token.getLineStartForErrorDisplay(source);
    901     // Treat tab stops as 1 column wide for error display purposes,
    902     // and add one to get a 1-based column
    903     const column = err_details.token.calculateColumn(source, 1, source_line_start) + 1;
    904 
    905     const corresponding_span: ?SourceMappings.CorrespondingSpan = if (source_mappings) |mappings|
    906         mappings.getCorrespondingSpan(err_details.token.line_number)
    907     else
    908         null;
    909     const corresponding_file: ?[]const u8 = if (source_mappings != null and corresponding_span != null)
    910         source_mappings.?.files.get(corresponding_span.?.filename_offset)
    911     else
    912         null;
    913 
    914     const err_line = if (corresponding_span) |span| span.start_line else err_details.token.line_number;
    915 
    916     try tty_config.setColor(writer, .bold);
    917     if (corresponding_file) |file| {
    918         try writer.writeAll(file);
    919     } else {
    920         try tty_config.setColor(writer, .dim);
    921         try writer.writeAll("<after preprocessor>");
    922         try tty_config.setColor(writer, .reset);
    923         try tty_config.setColor(writer, .bold);
    924     }
    925     try writer.print(":{d}:{d}: ", .{ err_line, column });
    926     switch (err_details.type) {
    927         .err => {
    928             try tty_config.setColor(writer, .red);
    929             try writer.writeAll("error: ");
    930         },
    931         .warning => {
    932             try tty_config.setColor(writer, .yellow);
    933             try writer.writeAll("warning: ");
    934         },
    935         .note => {
    936             try tty_config.setColor(writer, .cyan);
    937             try writer.writeAll("note: ");
    938         },
    939         .hint => unreachable,
    940     }
    941     try tty_config.setColor(writer, .reset);
    942     try tty_config.setColor(writer, .bold);
    943     try err_details.render(writer, source, strings);
    944     try writer.writeByte('\n');
    945     try tty_config.setColor(writer, .reset);
    946 
    947     if (!err_details.print_source_line) {
    948         try writer.writeByte('\n');
    949         return;
    950     }
    951 
    952     const source_line = err_details.token.getLineForErrorDisplay(source, source_line_start);
    953     const visual_info = err_details.visualTokenInfo(source_line_start, source_line_start + source_line.len, source);
    954     const truncated_visual_info = ErrorDetails.VisualTokenInfo{
    955         .before_len = if (visual_info.point_offset > max_source_line_codepoints and visual_info.before_len > 0)
    956             (visual_info.before_len + 1) -| (visual_info.point_offset - max_source_line_codepoints)
    957         else
    958             visual_info.before_len,
    959         .point_offset = @min(max_source_line_codepoints + 1, visual_info.point_offset),
    960         .after_len = if (visual_info.point_offset > max_source_line_codepoints)
    961             @min(truncated_str.len - 3, visual_info.after_len)
    962         else
    963             @min(max_source_line_codepoints - visual_info.point_offset + (truncated_str.len - 2), visual_info.after_len),
    964     };
    965 
    966     // Need this to determine if the 'line originated from' note is worth printing
    967     var source_line_for_display_buf: [max_source_line_bytes]u8 = undefined;
    968     const source_line_for_display = writeSourceSlice(&source_line_for_display_buf, source_line, err_details.code_page);
    969 
    970     try writer.writeAll(source_line_for_display.line);
    971     if (source_line_for_display.truncated) {
    972         try tty_config.setColor(writer, .dim);
    973         try writer.writeAll(truncated_str);
    974         try tty_config.setColor(writer, .reset);
    975     }
    976     try writer.writeByte('\n');
    977 
    978     try tty_config.setColor(writer, .green);
    979     const num_spaces = truncated_visual_info.point_offset - truncated_visual_info.before_len;
    980     try writer.splatByteAll(' ', num_spaces);
    981     try writer.splatByteAll('~', truncated_visual_info.before_len);
    982     try writer.writeByte('^');
    983     try writer.splatByteAll('~', truncated_visual_info.after_len);
    984     try writer.writeByte('\n');
    985     try tty_config.setColor(writer, .reset);
    986 
    987     if (corresponding_span != null and corresponding_file != null) {
    988         var worth_printing_lines: bool = true;
    989         var initial_lines_err: ?anyerror = null;
    990         var corresponding_lines: ?CorrespondingLines = CorrespondingLines.init(
    991             cwd,
    992             err_details,
    993             source_line_for_display.line,
    994             corresponding_span.?,
    995             corresponding_file.?,
    996         ) catch |err| switch (err) {
    997             error.NotWorthPrintingLines => blk: {
    998                 worth_printing_lines = false;
    999                 break :blk null;
   1000             },
   1001             error.NotWorthPrintingNote => return,
   1002             else => |e| blk: {
   1003                 initial_lines_err = e;
   1004                 break :blk null;
   1005             },
   1006         };
   1007         defer if (corresponding_lines) |*cl| cl.deinit();
   1008 
   1009         try tty_config.setColor(writer, .bold);
   1010         if (corresponding_file) |file| {
   1011             try writer.writeAll(file);
   1012         } else {
   1013             try tty_config.setColor(writer, .dim);
   1014             try writer.writeAll("<after preprocessor>");
   1015             try tty_config.setColor(writer, .reset);
   1016             try tty_config.setColor(writer, .bold);
   1017         }
   1018         try writer.print(":{d}:{d}: ", .{ err_line, column });
   1019         try tty_config.setColor(writer, .cyan);
   1020         try writer.writeAll("note: ");
   1021         try tty_config.setColor(writer, .reset);
   1022         try tty_config.setColor(writer, .bold);
   1023         try writer.writeAll("this line originated from line");
   1024         if (corresponding_span.?.start_line != corresponding_span.?.end_line) {
   1025             try writer.print("s {}-{}", .{ corresponding_span.?.start_line, corresponding_span.?.end_line });
   1026         } else {
   1027             try writer.print(" {}", .{corresponding_span.?.start_line});
   1028         }
   1029         try writer.print(" of file '{s}'\n", .{corresponding_file.?});
   1030         try tty_config.setColor(writer, .reset);
   1031 
   1032         if (!worth_printing_lines) return;
   1033 
   1034         const write_lines_err: ?anyerror = write_lines: {
   1035             if (initial_lines_err) |err| break :write_lines err;
   1036             while (corresponding_lines.?.next() catch |err| {
   1037                 break :write_lines err;
   1038             }) |display_line| {
   1039                 try writer.writeAll(display_line.line);
   1040                 if (display_line.truncated) {
   1041                     try tty_config.setColor(writer, .dim);
   1042                     try writer.writeAll(truncated_str);
   1043                     try tty_config.setColor(writer, .reset);
   1044                 }
   1045                 try writer.writeByte('\n');
   1046             }
   1047             break :write_lines null;
   1048         };
   1049         if (write_lines_err) |err| {
   1050             try tty_config.setColor(writer, .red);
   1051             try writer.writeAll(" | ");
   1052             try tty_config.setColor(writer, .reset);
   1053             try tty_config.setColor(writer, .dim);
   1054             try writer.print("unable to print line(s) from file: {s}\n", .{@errorName(err)});
   1055             try tty_config.setColor(writer, .reset);
   1056         }
   1057         try writer.writeByte('\n');
   1058     }
   1059 }
   1060 
   1061 const VisualLine = struct {
   1062     line: []u8,
   1063     truncated: bool,
   1064 };
   1065 
   1066 const CorrespondingLines = struct {
   1067     // enough room for one more codepoint, just so that we don't have to keep
   1068     // track of this being truncated, since the extra codepoint will ensure
   1069     // the visual line will need to truncate in that case.
   1070     line_buf: [max_source_line_bytes + 4]u8 = undefined,
   1071     line_len: usize = 0,
   1072     visual_line_buf: [max_source_line_bytes]u8 = undefined,
   1073     visual_line_len: usize = 0,
   1074     truncated: bool = false,
   1075     line_num: usize = 1,
   1076     initial_line: bool = true,
   1077     last_byte: u8 = 0,
   1078     at_eof: bool = false,
   1079     span: SourceMappings.CorrespondingSpan,
   1080     file: std.fs.File,
   1081     buffered_reader: std.fs.File.Reader,
   1082     code_page: SupportedCodePage,
   1083 
   1084     pub fn init(cwd: std.fs.Dir, err_details: ErrorDetails, line_for_comparison: []const u8, corresponding_span: SourceMappings.CorrespondingSpan, corresponding_file: []const u8) !CorrespondingLines {
   1085         // We don't do line comparison for this error, so don't print the note if the line
   1086         // number is different
   1087         if (err_details.err == .string_literal_too_long and err_details.token.line_number != corresponding_span.start_line) {
   1088             return error.NotWorthPrintingNote;
   1089         }
   1090 
   1091         // Don't print the originating line for this error, we know it's really long
   1092         if (err_details.err == .string_literal_too_long) {
   1093             return error.NotWorthPrintingLines;
   1094         }
   1095 
   1096         var corresponding_lines = CorrespondingLines{
   1097             .span = corresponding_span,
   1098             .file = try utils.openFileNotDir(cwd, corresponding_file, .{}),
   1099             .buffered_reader = undefined,
   1100             .code_page = err_details.code_page,
   1101         };
   1102         corresponding_lines.buffered_reader = corresponding_lines.file.reader(&.{});
   1103         errdefer corresponding_lines.deinit();
   1104 
   1105         var fbs = std.io.fixedBufferStream(&corresponding_lines.line_buf);
   1106         const writer = fbs.writer();
   1107 
   1108         try corresponding_lines.writeLineFromStreamVerbatim(
   1109             writer,
   1110             corresponding_lines.buffered_reader.interface.adaptToOldInterface(),
   1111             corresponding_span.start_line,
   1112         );
   1113 
   1114         const visual_line = writeSourceSlice(
   1115             &corresponding_lines.visual_line_buf,
   1116             corresponding_lines.line_buf[0..corresponding_lines.line_len],
   1117             err_details.code_page,
   1118         );
   1119         corresponding_lines.visual_line_len = visual_line.line.len;
   1120         corresponding_lines.truncated = visual_line.truncated;
   1121 
   1122         // If the lines are the same as they were before preprocessing, skip printing the note entirely
   1123         if (corresponding_span.start_line == corresponding_span.end_line and std.mem.eql(
   1124             u8,
   1125             line_for_comparison,
   1126             corresponding_lines.visual_line_buf[0..corresponding_lines.visual_line_len],
   1127         )) {
   1128             return error.NotWorthPrintingNote;
   1129         }
   1130 
   1131         return corresponding_lines;
   1132     }
   1133 
   1134     pub fn next(self: *CorrespondingLines) !?VisualLine {
   1135         if (self.initial_line) {
   1136             self.initial_line = false;
   1137             return .{
   1138                 .line = self.visual_line_buf[0..self.visual_line_len],
   1139                 .truncated = self.truncated,
   1140             };
   1141         }
   1142         if (self.line_num > self.span.end_line) return null;
   1143         if (self.at_eof) return error.LinesNotFound;
   1144 
   1145         self.line_len = 0;
   1146         self.visual_line_len = 0;
   1147 
   1148         var fbs = std.io.fixedBufferStream(&self.line_buf);
   1149         const writer = fbs.writer();
   1150 
   1151         try self.writeLineFromStreamVerbatim(
   1152             writer,
   1153             self.buffered_reader.interface.adaptToOldInterface(),
   1154             self.line_num,
   1155         );
   1156 
   1157         const visual_line = writeSourceSlice(
   1158             &self.visual_line_buf,
   1159             self.line_buf[0..self.line_len],
   1160             self.code_page,
   1161         );
   1162         self.visual_line_len = visual_line.line.len;
   1163 
   1164         return visual_line;
   1165     }
   1166 
   1167     fn writeLineFromStreamVerbatim(self: *CorrespondingLines, writer: anytype, input: anytype, line_num: usize) !void {
   1168         while (try readByteOrEof(input)) |byte| {
   1169             switch (byte) {
   1170                 '\n', '\r' => {
   1171                     if (!utils.isLineEndingPair(self.last_byte, byte)) {
   1172                         const line_complete = self.line_num == line_num;
   1173                         self.line_num += 1;
   1174                         if (line_complete) {
   1175                             self.last_byte = byte;
   1176                             return;
   1177                         }
   1178                     } else {
   1179                         // reset last_byte to a non-line ending so that
   1180                         // consecutive CRLF pairs don't get treated as one
   1181                         // long line ending 'pair'
   1182                         self.last_byte = 0;
   1183                         continue;
   1184                     }
   1185                 },
   1186                 else => {
   1187                     if (self.line_num == line_num) {
   1188                         if (writer.writeByte(byte)) {
   1189                             self.line_len += 1;
   1190                         } else |err| switch (err) {
   1191                             error.NoSpaceLeft => {},
   1192                             else => |e| return e,
   1193                         }
   1194                     }
   1195                 },
   1196             }
   1197             self.last_byte = byte;
   1198         }
   1199         self.at_eof = true;
   1200         // hacky way to get next to return null
   1201         self.line_num += 1;
   1202     }
   1203 
   1204     fn readByteOrEof(reader: anytype) !?u8 {
   1205         return reader.readByte() catch |err| switch (err) {
   1206             error.EndOfStream => return null,
   1207             else => |e| return e,
   1208         };
   1209     }
   1210 
   1211     pub fn deinit(self: *CorrespondingLines) void {
   1212         self.file.close();
   1213     }
   1214 };
   1215 
   1216 const max_source_line_codepoints = 120;
   1217 const max_source_line_bytes = max_source_line_codepoints * 4;
   1218 
   1219 fn writeSourceSlice(buf: []u8, slice: []const u8, code_page: SupportedCodePage) VisualLine {
   1220     var src_i: usize = 0;
   1221     var dest_i: usize = 0;
   1222     var codepoint_count: usize = 0;
   1223     while (src_i < slice.len) {
   1224         const codepoint = code_page.codepointAt(src_i, slice) orelse break;
   1225         defer src_i += codepoint.byte_len;
   1226         const display_codepoint = codepointForDisplay(codepoint) orelse continue;
   1227         codepoint_count += 1;
   1228         if (codepoint_count > max_source_line_codepoints) {
   1229             return .{ .line = buf[0..dest_i], .truncated = true };
   1230         }
   1231         const utf8_len = std.unicode.utf8Encode(display_codepoint, buf[dest_i..]) catch unreachable;
   1232         dest_i += utf8_len;
   1233     }
   1234     return .{ .line = buf[0..dest_i], .truncated = false };
   1235 }
   1236 
   1237 fn codepointForDisplay(codepoint: code_pages.Codepoint) ?u21 {
   1238     return switch (codepoint.value) {
   1239         '\x00'...'\x08',
   1240         '\x0E'...'\x1F',
   1241         '\x7F',
   1242         code_pages.Codepoint.invalid,
   1243         => '�',
   1244         // \r is seemingly ignored by the RC compiler so skipping it when printing source lines
   1245         // could help avoid confusing output (e.g. RC\rDATA if printed verbatim would show up
   1246         // in the console as DATA but the compiler reads it as RCDATA)
   1247         //
   1248         // NOTE: This is irrelevant when using the clang preprocessor, because unpaired \r
   1249         //       characters get converted to \n, but may become relevant if another
   1250         //       preprocessor is used instead.
   1251         '\r' => null,
   1252         '\t', '\x0B', '\x0C' => ' ',
   1253         else => |v| v,
   1254     };
   1255 }