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