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