2025-10-20 23:25:57 -07:00
const builtin = @import ( " builtin " ) ;
const native_endian = builtin . cpu . arch . endian ( ) ;
2023-06-30 15:02:32 -07:00
const std = @import ( " std " ) ;
2025-10-20 23:25:57 -07:00
const Io = std . Io ;
2025-06-27 20:05:22 -07:00
const assert = std . debug . assert ;
2025-10-20 23:25:57 -07:00
const Allocator = std . mem . Allocator ;
2023-06-30 15:02:32 -07:00
const Token = @import ( " lex.zig " ) . Token ;
const SourceMappings = @import ( " source_mapping.zig " ) . SourceMappings ;
const utils = @import ( " utils.zig " ) ;
const rc = @import ( " rc.zig " ) ;
const res = @import ( " res.zig " ) ;
const ico = @import ( " ico.zig " ) ;
const bmp = @import ( " bmp.zig " ) ;
const parse = @import ( " parse.zig " ) ;
2023-09-27 14:00:31 -07:00
const lang = @import ( " lang.zig " ) ;
2025-01-17 19:07:46 -08:00
const code_pages = @import ( " code_pages.zig " ) ;
const SupportedCodePage = code_pages . SupportedCodePage ;
2023-06-30 15:02:32 -07:00
pub const Diagnostics = struct {
2025-08-27 20:33:36 -07:00
errors : std . ArrayList ( ErrorDetails ) = . empty ,
2023-06-30 15:02:32 -07:00
/// Append-only, cannot handle removing strings.
/// Expects to own all strings within the list.
2025-08-27 20:33:36 -07:00
strings : std . ArrayList ( [ ] const u8 ) = . empty ,
2025-10-20 23:25:57 -07:00
allocator : Allocator ,
io : Io ,
2023-06-30 15:02:32 -07:00
2025-10-20 23:25:57 -07:00
pub fn init ( allocator : Allocator , io : Io ) Diagnostics {
2023-06-30 15:02:32 -07:00
return . {
. allocator = allocator ,
2025-10-20 23:25:57 -07:00
. io = io ,
2023-06-30 15:02:32 -07:00
} ;
}
pub fn deinit ( self : * Diagnostics ) void {
self . errors . deinit ( self . allocator ) ;
for ( self . strings . items ) | str | {
self . allocator . free ( str ) ;
}
self . strings . deinit ( self . allocator ) ;
}
pub fn append ( self : * Diagnostics , error_details : ErrorDetails ) ! void {
try self . errors . append ( self . allocator , error_details ) ;
}
const SmallestStringIndexType = std . meta . Int ( . unsigned , @min (
@bitSizeOf ( ErrorDetails . FileOpenError . FilenameStringIndex ) ,
@min (
@bitSizeOf ( ErrorDetails . IconReadError . FilenameStringIndex ) ,
@bitSizeOf ( ErrorDetails . BitmapReadError . FilenameStringIndex ) ,
) ,
) ) ;
/// Returns the index of the added string as the SmallestStringIndexType
/// in order to avoid needing to `@intCast` it at callsites of putString.
/// Instead, this function will error if the index would ever exceed the
/// smallest FilenameStringIndex of an ErrorDetails type.
pub fn putString ( self : * Diagnostics , str : [ ] const u8 ) ! SmallestStringIndexType {
if ( self . strings . items . len >= std . math . maxInt ( SmallestStringIndexType ) ) {
return error . OutOfMemory ; // ran out of string indexes
}
const dupe = try self . allocator . dupe ( u8 , str ) ;
const index = self . strings . items . len ;
try self . strings . append ( self . allocator , dupe ) ;
return @intCast ( index ) ;
}
2025-10-28 12:42:05 +00:00
pub fn renderToStdErr ( self : * Diagnostics , cwd : std . fs . Dir , source : [ ] const u8 , source_mappings : ? SourceMappings ) void {
2025-10-20 23:25:57 -07:00
const io = self . io ;
2025-10-28 12:42:05 +00:00
const stderr , const ttyconf = std . debug . lockStderrWriter ( & . { } ) ;
2025-07-07 19:33:20 -07:00
defer std . debug . unlockStderrWriter ( ) ;
2023-06-30 15:02:32 -07:00
for ( self . errors . items ) | err_details | {
2025-10-28 12:42:05 +00:00
renderErrorMessage ( io , stderr , ttyconf , cwd , err_details , source , self . strings . items , source_mappings ) catch return ;
2023-06-30 15:02:32 -07:00
}
}
pub fn contains ( self : * const Diagnostics , err : ErrorDetails . Error ) bool {
for ( self . errors . items ) | details | {
if ( details . err == err ) return true ;
}
return false ;
}
pub fn containsAny ( self : * const Diagnostics , errors : [ ] const ErrorDetails . Error ) bool {
for ( self . errors . items ) | details | {
for ( errors ) | err | {
if ( details . err == err ) return true ;
}
}
return false ;
}
} ;
/// Contains enough context to append errors/warnings/notes etc
pub const DiagnosticsContext = struct {
diagnostics : * Diagnostics ,
token : Token ,
2025-01-17 19:07:46 -08:00
/// Code page of the source file at the token location
code_page : SupportedCodePage ,
2023-06-30 15:02:32 -07:00
} ;
pub const ErrorDetails = struct {
err : Error ,
token : Token ,
2025-01-17 19:07:46 -08:00
/// Code page of the source file at the token location
code_page : SupportedCodePage ,
2023-06-30 15:02:32 -07:00
/// If non-null, should be before `token`. If null, `token` is assumed to be the start.
token_span_start : ? Token = null ,
/// If non-null, should be after `token`. If null, `token` is assumed to be the end.
token_span_end : ? Token = null ,
type : Type = . err ,
print_source_line : bool = true ,
2025-01-17 19:07:46 -08:00
extra : Extra = . { . none = { } } ,
2023-06-30 15:02:32 -07:00
pub const Type = enum {
/// Fatal error, stops compilation
err ,
/// Warning that does not affect compilation result
warning ,
/// A note that typically provides further context for a warning/error
note ,
/// An invisible diagnostic that is not printed to stderr but can
/// provide information useful when comparing the behavior of different
/// implementations. For example, a hint is emitted when a FONTDIR resource
/// was included in the .RES file which is significant because rc.exe
/// does something different than us, but ultimately it's not important
/// enough to be a warning/note.
hint ,
} ;
2025-01-17 19:07:46 -08:00
pub const Extra = union {
none : void ,
expected : Token . Id ,
number : u32 ,
expected_types : ExpectedTypes ,
resource : rc . ResourceType ,
string_and_language : StringAndLanguage ,
file_open_error : FileOpenError ,
icon_read_error : IconReadError ,
icon_dir : IconDirContext ,
bmp_read_error : BitmapReadError ,
accelerator_error : AcceleratorError ,
statement_with_u16_param : StatementWithU16Param ,
menu_or_class : enum { class , menu } ,
} ;
2023-06-30 15:02:32 -07:00
comptime {
// all fields in the extra union should be 32 bits or less
2025-01-17 19:07:46 -08:00
for ( std . meta . fields ( Extra ) ) | field | {
2023-06-30 15:02:32 -07:00
std . debug . assert ( @bitSizeOf ( field . type ) <= 32 ) ;
}
}
pub const StatementWithU16Param = enum ( u32 ) {
fileversion ,
productversion ,
language ,
} ;
pub const StringAndLanguage = packed struct ( u32 ) {
id : u16 ,
language : res . Language ,
} ;
pub const FileOpenError = packed struct ( u32 ) {
err : FileOpenErrorEnum ,
filename_string_index : FilenameStringIndex ,
pub const FilenameStringIndex = std . meta . Int ( . unsigned , 32 - @bitSizeOf ( FileOpenErrorEnum ) ) ;
2025-10-20 23:25:57 -07:00
pub const FileOpenErrorEnum = std . meta . FieldEnum ( std . fs . File . OpenError || std . fs . File . StatError ) ;
2023-06-30 15:02:32 -07:00
2025-10-20 23:25:57 -07:00
pub fn enumFromError ( err : ( std . fs . File . OpenError || std . fs . File . StatError ) ) FileOpenErrorEnum {
2023-06-30 15:02:32 -07:00
return switch ( err ) {
inline else = > | e | @field ( ErrorDetails . FileOpenError . FileOpenErrorEnum , @errorName ( e ) ) ,
} ;
}
} ;
pub const IconReadError = packed struct ( u32 ) {
err : IconReadErrorEnum ,
icon_type : enum ( u1 ) { cursor , icon } ,
filename_string_index : FilenameStringIndex ,
pub const FilenameStringIndex = std . meta . Int ( . unsigned , 32 - @bitSizeOf ( IconReadErrorEnum ) - 1 ) ;
pub const IconReadErrorEnum = std . meta . FieldEnum ( ico . ReadError ) ;
pub fn enumFromError ( err : ico . ReadError ) IconReadErrorEnum {
return switch ( err ) {
inline else = > | e | @field ( ErrorDetails . IconReadError . IconReadErrorEnum , @errorName ( e ) ) ,
} ;
}
} ;
pub const IconDirContext = packed struct ( u32 ) {
icon_type : enum ( u1 ) { cursor , icon } ,
icon_format : ico . ImageFormat ,
index : u16 ,
bitmap_version : ico . BitmapHeader . Version = . unknown ,
_ : Padding = 0 ,
pub const Padding = std . meta . Int ( . unsigned , 15 - @bitSizeOf ( ico . BitmapHeader . Version ) - @bitSizeOf ( ico . ImageFormat ) ) ;
} ;
pub const BitmapReadError = packed struct ( u32 ) {
err : BitmapReadErrorEnum ,
filename_string_index : FilenameStringIndex ,
pub const FilenameStringIndex = std . meta . Int ( . unsigned , 32 - @bitSizeOf ( BitmapReadErrorEnum ) ) ;
pub const BitmapReadErrorEnum = std . meta . FieldEnum ( bmp . ReadError ) ;
pub fn enumFromError ( err : bmp . ReadError ) BitmapReadErrorEnum {
return switch ( err ) {
inline else = > | e | @field ( ErrorDetails . BitmapReadError . BitmapReadErrorEnum , @errorName ( e ) ) ,
} ;
}
} ;
pub const BitmapUnsupportedDIB = packed struct ( u32 ) {
dib_version : ico . BitmapHeader . Version ,
filename_string_index : FilenameStringIndex ,
pub const FilenameStringIndex = std . meta . Int ( . unsigned , 32 - @bitSizeOf ( ico . BitmapHeader . Version ) ) ;
} ;
pub const AcceleratorError = packed struct ( u32 ) {
err : AcceleratorErrorEnum ,
_ : Padding = 0 ,
pub const Padding = std . meta . Int ( . unsigned , 32 - @bitSizeOf ( AcceleratorErrorEnum ) ) ;
pub const AcceleratorErrorEnum = std . meta . FieldEnum ( res . ParseAcceleratorKeyStringError ) ;
pub fn enumFromError ( err : res . ParseAcceleratorKeyStringError ) AcceleratorErrorEnum {
return switch ( err ) {
inline else = > | e | @field ( ErrorDetails . AcceleratorError . AcceleratorErrorEnum , @errorName ( e ) ) ,
} ;
}
} ;
pub const ExpectedTypes = packed struct ( u32 ) {
number : bool = false ,
number_expression : bool = false ,
string_literal : bool = false ,
accelerator_type_or_option : bool = false ,
control_class : bool = false ,
literal : bool = false ,
// Note: This being 0 instead of undefined is arbitrary and something of a workaround,
// see https://github.com/ziglang/zig/issues/15395
_ : u26 = 0 ,
2024-04-20 23:14:39 -07:00
pub const strings = std . StaticStringMap ( [ ] const u8 ) . initComptime ( . {
2023-06-30 15:02:32 -07:00
. { " number " , " number " } ,
. { " number_expression " , " number expression " } ,
. { " string_literal " , " quoted string literal " } ,
. { " accelerator_type_or_option " , " accelerator type or option [ASCII, VIRTKEY, etc] " } ,
. { " control_class " , " control class [BUTTON, EDIT, etc] " } ,
. { " literal " , " unquoted literal " } ,
} ) ;
2025-08-27 20:33:36 -07:00
pub fn writeCommaSeparated ( self : ExpectedTypes , writer : * std . Io . Writer ) ! void {
2024-08-28 02:35:53 +01:00
const struct_info = @typeInfo ( ExpectedTypes ) . @" struct " ;
2023-06-30 15:02:32 -07:00
const num_real_fields = struct_info . fields . len - 1 ;
const num_padding_bits = @bitSizeOf ( ExpectedTypes ) - num_real_fields ;
const mask = std . math . maxInt ( struct_info . backing_integer .? ) >> num_padding_bits ;
const relevant_bits_only = @as ( struct_info . backing_integer .? , @bitCast ( self ) ) & mask ;
const num_set_bits = @popCount ( relevant_bits_only ) ;
var i : usize = 0 ;
inline for ( struct_info . fields ) | field_info | {
if ( field_info . type != bool ) continue ;
if ( i == num_set_bits ) return ;
if ( @field ( self , field_info . name ) ) {
try writer . writeAll ( strings . get ( field_info . name ) .? ) ;
i += 1 ;
if ( num_set_bits > 2 and i != num_set_bits ) {
try writer . writeAll ( " , " ) ;
} else if ( i != num_set_bits ) {
try writer . writeByte ( ' ' ) ;
}
if ( num_set_bits > 1 and i == num_set_bits - 1 ) {
try writer . writeAll ( " or " ) ;
}
}
}
}
} ;
pub const Error = enum {
// Lexer
unfinished_string_literal ,
string_literal_too_long ,
invalid_number_with_exponent ,
invalid_digit_character_in_number_literal ,
illegal_byte ,
illegal_byte_outside_string_literals ,
illegal_codepoint_outside_string_literals ,
illegal_byte_order_mark ,
illegal_private_use_character ,
found_c_style_escaped_quote ,
code_page_pragma_missing_left_paren ,
code_page_pragma_missing_right_paren ,
code_page_pragma_invalid_code_page ,
code_page_pragma_not_integer ,
code_page_pragma_overflow ,
code_page_pragma_unsupported_code_page ,
// Parser
unfinished_raw_data_block ,
unfinished_string_table_block ,
/// `expected` is populated.
expected_token ,
/// `expected_types` is populated
expected_something_else ,
/// `resource` is populated
resource_type_cant_use_raw_data ,
/// `resource` is populated
id_must_be_ordinal ,
/// `resource` is populated
name_or_id_not_allowed ,
string_resource_as_numeric_type ,
ascii_character_not_equivalent_to_virtual_key_code ,
empty_menu_not_allowed ,
rc_would_miscompile_version_value_padding ,
rc_would_miscompile_version_value_byte_count ,
code_page_pragma_in_included_file ,
nested_resource_level_exceeds_max ,
2024-03-02 00:50:53 -08:00
too_many_dialog_controls_or_toolbar_buttons ,
2023-06-30 15:02:32 -07:00
nested_expression_level_exceeds_max ,
close_paren_expression ,
unary_plus_expression ,
rc_could_miscompile_control_params ,
2025-01-17 19:07:46 -08:00
dangling_literal_at_eof ,
disjoint_code_page ,
2023-06-30 15:02:32 -07:00
// Compiler
/// `string_and_language` is populated
string_already_defined ,
font_id_already_defined ,
/// `file_open_error` is populated
file_open_error ,
/// `accelerator_error` is populated
invalid_accelerator_key ,
accelerator_type_required ,
2025-01-17 19:07:46 -08:00
accelerator_shift_or_control_without_virtkey ,
2023-06-30 15:02:32 -07:00
rc_would_miscompile_control_padding ,
rc_would_miscompile_control_class_ordinal ,
/// `icon_dir` is populated
rc_would_error_on_icon_dir ,
/// `icon_dir` is populated
format_not_supported_in_icon_dir ,
/// `resource` is populated and contains the expected type
icon_dir_and_resource_type_mismatch ,
/// `icon_read_error` is populated
icon_read_error ,
/// `icon_dir` is populated
rc_would_error_on_bitmap_version ,
/// `icon_dir` is populated
max_icon_ids_exhausted ,
/// `bmp_read_error` is populated
bmp_read_error ,
/// `number` is populated and contains a string index for which the string contains
/// the bytes of a `u64` (native endian). The `u64` contains the number of ignored bytes.
bmp_ignored_palette_bytes ,
/// `number` is populated and contains a string index for which the string contains
/// the bytes of a `u64` (native endian). The `u64` contains the number of missing bytes.
bmp_missing_palette_bytes ,
/// `number` is populated and contains a string index for which the string contains
/// the bytes of a `u64` (native endian). The `u64` contains the number of miscompiled bytes.
rc_would_miscompile_bmp_palette_padding ,
resource_header_size_exceeds_max ,
resource_data_size_exceeds_max ,
control_extra_data_size_exceeds_max ,
version_node_size_exceeds_max ,
fontdir_size_exceeds_max ,
/// `number` is populated and contains a string index for the filename
number_expression_as_filename ,
/// `number` is populated and contains the control ID that is a duplicate
control_id_already_defined ,
/// `number` is populated and contains the disallowed codepoint
invalid_filename ,
/// `statement_with_u16_param` is populated
rc_would_error_u16_with_l_suffix ,
result_contains_fontdir ,
/// `number` is populated and contains the ordinal value that the id would be miscompiled to
rc_would_miscompile_dialog_menu_id ,
/// `number` is populated and contains the ordinal value that the value would be miscompiled to
rc_would_miscompile_dialog_class ,
/// `menu_or_class` is populated and contains the type of the parameter statement
rc_would_miscompile_dialog_menu_or_class_id_forced_ordinal ,
rc_would_miscompile_dialog_menu_id_starts_with_digit ,
dialog_menu_id_was_uppercased ,
2025-01-17 19:07:46 -08:00
duplicate_optional_statement_skipped ,
2023-06-30 15:02:32 -07:00
invalid_digit_character_in_ordinal ,
// Literals
/// `number` is populated
2025-01-17 19:07:46 -08:00
rc_would_miscompile_codepoint_whitespace ,
2023-06-30 15:02:32 -07:00
/// `number` is populated
rc_would_miscompile_codepoint_skip ,
2025-01-17 19:07:46 -08:00
/// `number` is populated
rc_would_miscompile_codepoint_bom ,
2023-06-30 15:02:32 -07:00
tab_converted_to_spaces ,
// General (used in various places)
/// `number` is populated and contains the value that the ordinal would have in the Win32 RC compiler implementation
win32_non_ascii_ordinal ,
2023-10-18 18:00:50 -07:00
// Initialization
/// `file_open_error` is populated, but `filename_string_index` is not
failed_to_open_cwd ,
2023-06-30 15:02:32 -07:00
} ;
2025-08-27 21:20:18 -07:00
fn formatToken ( ctx : TokenFormatContext , writer : * std . Io . Writer ) std . Io . Writer . Error ! void {
2025-01-17 19:07:46 -08:00
switch ( ctx . token . id ) {
. eof = > return writer . writeAll ( ctx . token . id . nameForErrorDisplay ( ) ) ,
else = > { } ,
}
const slice = ctx . token . slice ( ctx . source ) ;
var src_i : usize = 0 ;
while ( src_i < slice . len ) {
const codepoint = ctx . code_page . codepointAt ( src_i , slice ) orelse break ;
defer src_i += codepoint . byte_len ;
const display_codepoint = codepointForDisplay ( codepoint ) orelse continue ;
var buf : [ 4 ] u8 = undefined ;
const utf8_len = std . unicode . utf8Encode ( display_codepoint , & buf ) catch unreachable ;
try writer . writeAll ( buf [ 0 .. utf8_len ] ) ;
}
}
const TokenFormatContext = struct {
token : Token ,
source : [ ] const u8 ,
code_page : SupportedCodePage ,
} ;
2025-08-30 14:50:33 -07:00
fn fmtToken ( self : ErrorDetails , source : [ ] const u8 ) std . fmt . Alt ( TokenFormatContext , formatToken ) {
2025-01-17 19:07:46 -08:00
return . { . data = . {
. token = self . token ,
. code_page = self . code_page ,
. source = source ,
} } ;
}
2025-08-27 20:33:36 -07:00
pub fn render ( self : ErrorDetails , writer : * std . Io . Writer , source : [ ] const u8 , strings : [ ] const [ ] const u8 ) ! void {
2023-06-30 15:02:32 -07:00
switch ( self . err ) {
. unfinished_string_literal = > {
2025-07-07 19:33:20 -07:00
return writer . print ( " unfinished string literal at '{f}', expected closing ' \" ' " , . { self . fmtToken ( source ) } ) ;
2023-06-30 15:02:32 -07:00
} ,
. string_literal_too_long = > {
return writer . print ( " string literal too long (max is currently {} characters) " , . { self . extra . number } ) ;
} ,
. invalid_number_with_exponent = > {
return writer . print ( " base 10 number literal with exponent is not allowed: {s} " , . { self . token . slice ( source ) } ) ;
} ,
. invalid_digit_character_in_number_literal = > switch ( self . type ) {
. err , . warning = > return writer . writeAll ( " non-ASCII digit characters are not allowed in number literals " ) ,
. note = > return writer . writeAll ( " the Win32 RC compiler allows non-ASCII digit characters, but will miscompile them " ) ,
. hint = > return ,
} ,
. illegal_byte = > {
2025-06-27 20:05:22 -07:00
return writer . print ( " character '{f}' is not allowed " , . {
std . ascii . hexEscape ( self . token . slice ( source ) , . upper ) ,
} ) ;
2023-06-30 15:02:32 -07:00
} ,
. illegal_byte_outside_string_literals = > {
2025-06-27 20:05:22 -07:00
return writer . print ( " character '{f}' is not allowed outside of string literals " , . {
std . ascii . hexEscape ( self . token . slice ( source ) , . upper ) ,
} ) ;
2023-06-30 15:02:32 -07:00
} ,
. illegal_codepoint_outside_string_literals = > {
// This is somewhat hacky, but we know that:
// - This error is only possible with codepoints outside of the Windows-1252 character range
// - So, the only supported code page that could generate this error is UTF-8
// Therefore, we just assume the token bytes are UTF-8 and decode them to get the illegal
// codepoint.
//
// FIXME: Support other code pages if they become relevant
const bytes = self . token . slice ( source ) ;
const codepoint = std . unicode . utf8Decode ( bytes ) catch unreachable ;
return writer . print ( " codepoint <U+{X:0>4}> is not allowed outside of string literals " , . { codepoint } ) ;
} ,
. illegal_byte_order_mark = > {
return writer . writeAll ( " byte order mark <U+FEFF> is not allowed " ) ;
} ,
. illegal_private_use_character = > {
return writer . writeAll ( " private use character <U+E000> is not allowed " ) ;
} ,
. found_c_style_escaped_quote = > {
return writer . writeAll ( " escaping quotes with \\ \" is not allowed (use \" \" instead) " ) ;
} ,
. code_page_pragma_missing_left_paren = > {
return writer . writeAll ( " expected left parenthesis after 'code_page' in #pragma code_page " ) ;
} ,
. code_page_pragma_missing_right_paren = > {
return writer . writeAll ( " expected right parenthesis after '<number>' in #pragma code_page " ) ;
} ,
. code_page_pragma_invalid_code_page = > {
return writer . writeAll ( " invalid or unknown code page in #pragma code_page " ) ;
} ,
. code_page_pragma_not_integer = > {
return writer . writeAll ( " code page is not a valid integer in #pragma code_page " ) ;
} ,
. code_page_pragma_overflow = > {
return writer . writeAll ( " code page too large in #pragma code_page " ) ;
} ,
. code_page_pragma_unsupported_code_page = > {
// We know that the token slice is a well-formed #pragma code_page(N), so
// we can skip to the first ( and then get the number that follows
const token_slice = self . token . slice ( source ) ;
var number_start = std . mem . indexOfScalar ( u8 , token_slice , '(' ) .? + 1 ;
while ( std . ascii . isWhitespace ( token_slice [ number_start ] ) ) {
number_start += 1 ;
}
var number_slice = token_slice [ number_start .. number_start ] ;
while ( std . ascii . isDigit ( token_slice [ number_start + number_slice . len ] ) ) {
number_slice . len += 1 ;
}
const number = std . fmt . parseUnsigned ( u16 , number_slice , 10 ) catch unreachable ;
2025-01-17 19:07:46 -08:00
const code_page = code_pages . getByIdentifier ( number ) catch unreachable ;
2023-06-30 15:02:32 -07:00
// TODO: Improve or maybe add a note making it more clear that the code page
// is valid and that the code page is unsupported purely due to a limitation
// in this compiler.
return writer . print ( " unsupported code page '{s} (id={})' in #pragma code_page " , . { @tagName ( code_page ) , number } ) ;
} ,
. unfinished_raw_data_block = > {
2025-07-07 19:33:20 -07:00
return writer . print ( " unfinished raw data block at '{f}', expected closing '}}' or 'END' " , . { self . fmtToken ( source ) } ) ;
2023-06-30 15:02:32 -07:00
} ,
. unfinished_string_table_block = > {
2025-07-07 19:33:20 -07:00
return writer . print ( " unfinished STRINGTABLE block at '{f}', expected closing '}}' or 'END' " , . { self . fmtToken ( source ) } ) ;
2023-06-30 15:02:32 -07:00
} ,
. expected_token = > {
2025-07-07 19:33:20 -07:00
return writer . print ( " expected '{s}', got '{f}' " , . { self . extra . expected . nameForErrorDisplay ( ) , self . fmtToken ( source ) } ) ;
2023-06-30 15:02:32 -07:00
} ,
. expected_something_else = > {
try writer . writeAll ( " expected " ) ;
try self . extra . expected_types . writeCommaSeparated ( writer ) ;
2025-07-07 19:33:20 -07:00
return writer . print ( " ; got '{f}' " , . { self . fmtToken ( source ) } ) ;
2023-06-30 15:02:32 -07:00
} ,
. resource_type_cant_use_raw_data = > switch ( self . type ) {
2025-07-07 19:33:20 -07:00
. err , . warning = > try writer . print ( " expected '<filename>', found '{f}' (resource type '{s}' can't use raw data) " , . { self . fmtToken ( source ) , self . extra . resource . nameForErrorDisplay ( ) } ) ,
. note = > try writer . print ( " if '{f}' is intended to be a filename, it must be specified as a quoted string literal " , . { self . fmtToken ( source ) } ) ,
2023-06-30 15:02:32 -07:00
. hint = > return ,
} ,
. id_must_be_ordinal = > {
2025-07-07 19:33:20 -07:00
try writer . print ( " id of resource type '{s}' must be an ordinal (u16), got '{f}' " , . { self . extra . resource . nameForErrorDisplay ( ) , self . fmtToken ( source ) } ) ;
2023-06-30 15:02:32 -07:00
} ,
. name_or_id_not_allowed = > {
try writer . print ( " name or id is not allowed for resource type '{s}' " , . { self . extra . resource . nameForErrorDisplay ( ) } ) ;
} ,
. string_resource_as_numeric_type = > switch ( self . type ) {
. err , . warning = > try writer . writeAll ( " the number 6 (RT_STRING) cannot be used as a resource type " ) ,
. note = > try writer . writeAll ( " using RT_STRING directly likely results in an invalid .res file, use a STRINGTABLE instead " ) ,
. hint = > return ,
} ,
. ascii_character_not_equivalent_to_virtual_key_code = > {
// TODO: Better wording? This is what the Win32 RC compiler emits.
// This occurs when VIRTKEY and a control code is specified ("^c", etc)
try writer . writeAll ( " ASCII character not equivalent to virtual key code " ) ;
} ,
. empty_menu_not_allowed = > {
2025-07-07 19:33:20 -07:00
try writer . print ( " empty menu of type '{f}' not allowed " , . { self . fmtToken ( source ) } ) ;
2023-06-30 15:02:32 -07:00
} ,
. rc_would_miscompile_version_value_padding = > switch ( self . type ) {
. err , . warning = > return writer . print ( " the padding before this quoted string value would be miscompiled by the Win32 RC compiler " , . { } ) ,
. note = > return writer . print ( " to avoid the potential miscompilation, consider adding a comma between the key and the quoted string " , . { } ) ,
. hint = > return ,
} ,
. rc_would_miscompile_version_value_byte_count = > switch ( self . type ) {
. err , . warning = > return writer . print ( " the byte count of this value would be miscompiled by the Win32 RC compiler " , . { } ) ,
. note = > return writer . print ( " to avoid the potential miscompilation, do not mix numbers and strings within a value " , . { } ) ,
. hint = > return ,
} ,
. code_page_pragma_in_included_file = > {
try writer . print ( " #pragma code_page is not supported in an included resource file " , . { } ) ;
} ,
. nested_resource_level_exceeds_max = > switch ( self . type ) {
. err , . warning = > {
const max = switch ( self . extra . resource ) {
. versioninfo = > parse . max_nested_version_level ,
. menu , . menuex = > parse . max_nested_menu_level ,
else = > unreachable ,
} ;
return writer . print ( " {s} contains too many nested children (max is {}) " , . { self . extra . resource . nameForErrorDisplay ( ) , max } ) ;
} ,
. note = > return writer . print ( " max {s} nesting level exceeded here " , . { self . extra . resource . nameForErrorDisplay ( ) } ) ,
. hint = > return ,
} ,
2024-03-02 00:50:53 -08:00
. too_many_dialog_controls_or_toolbar_buttons = > switch ( self . type ) {
. err , . warning = > return writer . print ( " {s} contains too many {s} (max is {}) " , . { self . extra . resource . nameForErrorDisplay ( ) , switch ( self . extra . resource ) {
. toolbar = > " buttons " ,
else = > " controls " ,
} , std . math . maxInt ( u16 ) } ) ,
. note = > return writer . print ( " maximum number of {s} exceeded here " , . { switch ( self . extra . resource ) {
. toolbar = > " buttons " ,
else = > " controls " ,
} } ) ,
2023-06-30 15:02:32 -07:00
. hint = > return ,
} ,
. nested_expression_level_exceeds_max = > switch ( self . type ) {
. err , . warning = > return writer . print ( " expression contains too many syntax levels (max is {}) " , . { parse . max_nested_expression_level } ) ,
. note = > return writer . print ( " maximum expression level exceeded here " , . { } ) ,
. hint = > return ,
} ,
. close_paren_expression = > {
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 " ) ;
} ,
. unary_plus_expression = > {
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 + " ) ;
} ,
. rc_could_miscompile_control_params = > switch ( self . type ) {
. err , . warning = > return writer . print ( " this token could be erroneously skipped over by the Win32 RC compiler " , . { } ) ,
. note = > return writer . print ( " to avoid the potential miscompilation, consider adding a comma after the style parameter " , . { } ) ,
. hint = > return ,
} ,
2025-01-17 19:07:46 -08:00
. dangling_literal_at_eof = > {
try writer . writeAll ( " dangling literal at end-of-file; this is not a problem, but it is likely a mistake " ) ;
} ,
. disjoint_code_page = > switch ( self . type ) {
. 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 " , . { } ) ,
. note = > return writer . print ( " to avoid unexpected behavior, add a comment (or anything else) above the #pragma code_page line " , . { } ) ,
. hint = > return ,
} ,
2023-06-30 15:02:32 -07:00
. string_already_defined = > switch ( self . type ) {
2023-09-27 14:00:31 -07:00
. err , . warning = > {
2025-01-17 19:07:46 -08:00
const language = self . extra . string_and_language . language ;
2025-07-07 19:33:20 -07:00
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 } ) ;
2023-09-27 14:00:31 -07:00
} ,
2023-06-30 15:02:32 -07:00
. 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 } ) ,
. hint = > return ,
} ,
. font_id_already_defined = > switch ( self . type ) {
. err = > return writer . print ( " font with id {d} already defined " , . { self . extra . number } ) ,
. warning = > return writer . print ( " skipped duplicate font with id {d} " , . { self . extra . number } ) ,
. note = > return writer . print ( " previous definition of font with id {d} here " , . { self . extra . number } ) ,
. hint = > return ,
} ,
. file_open_error = > {
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 ) } ) ;
} ,
. invalid_accelerator_key = > {
2025-07-07 19:33:20 -07:00
try writer . print ( " invalid accelerator key '{f}': {s} " , . { self . fmtToken ( source ) , @tagName ( self . extra . accelerator_error . err ) } ) ;
2023-06-30 15:02:32 -07:00
} ,
. accelerator_type_required = > {
2025-01-17 19:07:46 -08:00
try writer . writeAll ( " accelerator type [ASCII or VIRTKEY] required when key is an integer " ) ;
} ,
. accelerator_shift_or_control_without_virtkey = > {
try writer . writeAll ( " SHIFT or CONTROL used without VIRTKEY " ) ;
2023-06-30 15:02:32 -07:00
} ,
. rc_would_miscompile_control_padding = > switch ( self . type ) {
. 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) " , . { } ) ,
2025-01-17 19:07:46 -08:00
. note = > return writer . print ( " to avoid the potential miscompilation, consider adding one more byte to the control data of the control preceding this one " , . { } ) ,
2023-06-30 15:02:32 -07:00
. hint = > return ,
} ,
. rc_would_miscompile_control_class_ordinal = > switch ( self . type ) {
. err , . warning = > return writer . print ( " the control class of this CONTROL would be miscompiled by the Win32 RC compiler " , . { } ) ,
. note = > return writer . print ( " to avoid the potential miscompilation, consider specifying the control class using a string (BUTTON, EDIT, etc) instead of a number " , . { } ) ,
. hint = > return ,
} ,
. rc_would_error_on_icon_dir = > switch ( self . type ) {
. 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 ) } ) ,
. note = > {
// The only note supported is one specific to exactly this combination
if ( ! ( self . extra . icon_dir . icon_type == . icon and self . extra . icon_dir . icon_format == . riff ) ) unreachable ;
try writer . print ( " animated RIFF icons within resource groups may not be well supported, consider using an animated icon file (.ani) instead " , . { } ) ;
} ,
. hint = > return ,
} ,
. format_not_supported_in_icon_dir = > {
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 ) } ) ;
} ,
. icon_dir_and_resource_type_mismatch = > {
2025-01-17 19:07:46 -08:00
const unexpected_type : rc . ResourceType = if ( self . extra . resource == . icon ) . cursor else . icon ;
2023-06-30 15:02:32 -07:00
// TODO: Better wording
try writer . print ( " resource type '{s}' does not match type '{s}' specified in the file " , . { self . extra . resource . nameForErrorDisplay ( ) , unexpected_type . nameForErrorDisplay ( ) } ) ;
} ,
. icon_read_error = > {
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 ) } ) ;
} ,
. rc_would_error_on_bitmap_version = > switch ( self . type ) {
. 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}' " , . {
self . extra . icon_dir . index ,
@tagName ( self . extra . icon_dir . icon_type ) ,
self . extra . icon_dir . bitmap_version . nameForErrorDisplay ( ) ,
ico . BitmapHeader . Version . @" nt3.1 " . nameForErrorDisplay ( ) ,
} ) ,
. 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 " , . {
self . extra . icon_dir . index ,
@tagName ( self . extra . icon_dir . icon_type ) ,
self . extra . icon_dir . bitmap_version . nameForErrorDisplay ( ) ,
} ) ,
. note = > unreachable ,
. hint = > return ,
} ,
. max_icon_ids_exhausted = > switch ( self . type ) {
. err , . warning = > try writer . print ( " maximum global icon/cursor ids exhausted (max is {}) " , . { std . math . maxInt ( u16 ) - 1 } ) ,
. 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 ) } ) ,
. hint = > return ,
} ,
. bmp_read_error = > {
try writer . print ( " invalid bitmap file '{s}': {s} " , . { strings [ self . extra . bmp_read_error . filename_string_index ] , @tagName ( self . extra . bmp_read_error . err ) } ) ;
} ,
. bmp_ignored_palette_bytes = > {
const bytes = strings [ self . extra . number ] ;
2023-10-31 04:26:57 -04:00
const ignored_bytes = std . mem . readInt ( u64 , bytes [ 0 .. 8 ] , native_endian ) ;
2023-06-30 15:02:32 -07:00
try writer . print ( " bitmap has {d} extra bytes preceding the pixel data which will be ignored " , . { ignored_bytes } ) ;
} ,
. bmp_missing_palette_bytes = > {
const bytes = strings [ self . extra . number ] ;
2023-10-31 04:26:57 -04:00
const missing_bytes = std . mem . readInt ( u64 , bytes [ 0 .. 8 ] , native_endian ) ;
2025-01-17 19:07:46 -08:00
try writer . print ( " bitmap has {d} missing color palette bytes " , . { missing_bytes } ) ;
2023-06-30 15:02:32 -07:00
} ,
. rc_would_miscompile_bmp_palette_padding = > {
2025-01-17 19:07:46 -08:00
try writer . writeAll ( " the Win32 RC compiler would erroneously pad out the missing bytes " ) ;
if ( self . extra . number != 0 ) {
2023-06-30 15:02:32 -07:00
const bytes = strings [ self . extra . number ] ;
2025-01-17 19:07:46 -08:00
const miscompiled_bytes = std . mem . readInt ( u64 , bytes [ 0 .. 8 ] , native_endian ) ;
try writer . print ( " (and the added padding bytes would include {d} bytes of the pixel data) " , . { miscompiled_bytes } ) ;
}
2023-06-30 15:02:32 -07:00
} ,
. resource_header_size_exceeds_max = > {
try writer . print ( " resource's header length exceeds maximum of {} bytes " , . { std . math . maxInt ( u32 ) } ) ;
} ,
. resource_data_size_exceeds_max = > switch ( self . type ) {
. err , . warning = > return writer . print ( " resource's data length exceeds maximum of {} bytes " , . { std . math . maxInt ( u32 ) } ) ,
. note = > return writer . print ( " maximum data length exceeded here " , . { } ) ,
. hint = > return ,
} ,
. control_extra_data_size_exceeds_max = > switch ( self . type ) {
. err , . warning = > try writer . print ( " control data length exceeds maximum of {} bytes " , . { std . math . maxInt ( u16 ) } ) ,
. note = > return writer . print ( " maximum control data length exceeded here " , . { } ) ,
. hint = > return ,
} ,
. version_node_size_exceeds_max = > switch ( self . type ) {
. err , . warning = > return writer . print ( " version node tree size exceeds maximum of {} bytes " , . { std . math . maxInt ( u16 ) } ) ,
. note = > return writer . print ( " maximum tree size exceeded while writing this child " , . { } ) ,
. hint = > return ,
} ,
. fontdir_size_exceeds_max = > switch ( self . type ) {
. err , . warning = > return writer . print ( " FONTDIR data length exceeds maximum of {} bytes " , . { std . math . maxInt ( u32 ) } ) ,
. note = > return writer . writeAll ( " this is likely due to the size of the combined lengths of the device/face names of all FONT resources " ) ,
. hint = > return ,
} ,
. number_expression_as_filename = > switch ( self . type ) {
. err , . warning = > return writer . writeAll ( " filename cannot be specified using a number expression, consider using a quoted string instead " ) ,
. note = > return writer . print ( " the Win32 RC compiler would evaluate this number expression as the filename '{s}' " , . { strings [ self . extra . number ] } ) ,
. hint = > return ,
} ,
. control_id_already_defined = > switch ( self . type ) {
. err , . warning = > return writer . print ( " control with id {d} already defined for this dialog " , . { self . extra . number } ) ,
. note = > return writer . print ( " previous definition of control with id {d} here " , . { self . extra . number } ) ,
. hint = > return ,
} ,
. invalid_filename = > {
const disallowed_codepoint = self . extra . number ;
if ( disallowed_codepoint < 128 and std . ascii . isPrint ( @intCast ( disallowed_codepoint ) ) ) {
try writer . print ( " evaluated filename contains a disallowed character: '{c}' " , . { @as ( u8 , @intCast ( disallowed_codepoint ) ) } ) ;
} else {
try writer . print ( " evaluated filename contains a disallowed codepoint: <U+{X:0>4}> " , . { disallowed_codepoint } ) ;
}
} ,
. rc_would_error_u16_with_l_suffix = > switch ( self . type ) {
. err , . warning = > return writer . print ( " this {s} parameter would be an error in the Win32 RC compiler " , . { @tagName ( self . extra . statement_with_u16_param ) } ) ,
. note = > return writer . writeAll ( " to avoid the error, remove any L suffixes from numbers within the parameter " ) ,
. hint = > return ,
} ,
. result_contains_fontdir = > return ,
. rc_would_miscompile_dialog_menu_id = > switch ( self . type ) {
. err , . warning = > return writer . print ( " the id of this menu would be miscompiled by the Win32 RC compiler " , . { } ) ,
. note = > return writer . print ( " the Win32 RC compiler would evaluate the id as the ordinal/number value {d} " , . { self . extra . number } ) ,
. hint = > return ,
} ,
. rc_would_miscompile_dialog_class = > switch ( self . type ) {
. err , . warning = > return writer . print ( " this class would be miscompiled by the Win32 RC compiler " , . { } ) ,
. note = > return writer . print ( " the Win32 RC compiler would evaluate it as the ordinal/number value {d} " , . { self . extra . number } ) ,
. hint = > return ,
} ,
. rc_would_miscompile_dialog_menu_or_class_id_forced_ordinal = > switch ( self . type ) {
. err , . warning = > return ,
. note = > return writer . print ( " to avoid the potential miscompilation, only specify one {s} per dialog resource " , . { @tagName ( self . extra . menu_or_class ) } ) ,
. hint = > return ,
} ,
. rc_would_miscompile_dialog_menu_id_starts_with_digit = > switch ( self . type ) {
. err , . warning = > return ,
. note = > return writer . writeAll ( " to avoid the potential miscompilation, the first character of the id should not be a digit " ) ,
. hint = > return ,
} ,
. dialog_menu_id_was_uppercased = > return ,
2025-01-17 19:07:46 -08:00
. duplicate_optional_statement_skipped = > {
return writer . writeAll ( " this statement was ignored; when multiple statements of the same type are specified, only the last takes precedence " ) ;
2023-06-30 15:02:32 -07:00
} ,
. invalid_digit_character_in_ordinal = > {
return writer . writeAll ( " non-ASCII digit characters are not allowed in ordinal (number) values " ) ;
} ,
2025-01-17 19:07:46 -08:00
. rc_would_miscompile_codepoint_whitespace = > {
const treated_as = self . extra . number >> 8 ;
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 } ) ;
2023-06-30 15:02:32 -07:00
} ,
2025-01-17 19:07:46 -08:00
. rc_would_miscompile_codepoint_skip = > {
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 } ) ;
} ,
. rc_would_miscompile_codepoint_bom = > switch ( self . type ) {
. 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 } ) ,
. note = > return writer . writeAll ( " the presence of this codepoint causes all non-ASCII codepoints to be byteswapped by the Win32 RC preprocessor " ) ,
2023-06-30 15:02:32 -07:00
. hint = > return ,
} ,
. tab_converted_to_spaces = > switch ( self . type ) {
. 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) " ) ,
. note = > return writer . writeAll ( " to include the tab character itself in a string, the escape sequence \\ t should be used " ) ,
. hint = > return ,
} ,
. win32_non_ascii_ordinal = > switch ( self . type ) {
. err , . warning = > unreachable ,
. note = > return writer . print ( " the Win32 RC compiler would accept this as an ordinal but its value would be {} " , . { self . extra . number } ) ,
. hint = > return ,
} ,
2023-10-18 18:00:50 -07:00
. failed_to_open_cwd = > {
try writer . print ( " failed to open CWD for compilation: {s} " , . { @tagName ( self . extra . file_open_error . err ) } ) ;
} ,
2023-06-30 15:02:32 -07:00
}
}
pub const VisualTokenInfo = struct {
before_len : usize ,
point_offset : usize ,
after_len : usize ,
} ;
2025-01-17 19:07:46 -08:00
pub fn visualTokenInfo ( self : ErrorDetails , source_line_start : usize , source_line_end : usize , source : [ ] const u8 ) VisualTokenInfo {
2023-06-30 15:02:32 -07:00
return switch ( self . err ) {
// These can technically be more than 1 byte depending on encoding,
// but they always refer to one visual character/grapheme.
. illegal_byte ,
. illegal_byte_outside_string_literals ,
. illegal_codepoint_outside_string_literals ,
. illegal_byte_order_mark ,
. illegal_private_use_character ,
= > . {
. before_len = 0 ,
2025-01-17 19:07:46 -08:00
. point_offset = cellCount ( self . code_page , source , source_line_start , self . token . start ) ,
2023-06-30 15:02:32 -07:00
. after_len = 0 ,
} ,
else = > . {
. before_len = before : {
const start = @max ( source_line_start , if ( self . token_span_start ) | span_start | span_start . start else self . token . start ) ;
2025-01-17 19:07:46 -08:00
break : before cellCount ( self . code_page , source , start , self . token . start ) ;
2023-06-30 15:02:32 -07:00
} ,
2025-01-17 19:07:46 -08:00
. point_offset = cellCount ( self . code_page , source , source_line_start , self . token . start ) ,
2023-06-30 15:02:32 -07:00
. after_len = after : {
const end = @min ( source_line_end , if ( self . token_span_end ) | span_end | span_end . end else self . token . end ) ;
2023-10-18 18:00:50 -07:00
// end may be less than start when pointing to EOF
if ( end <= self . token . start ) break : after 0 ;
2025-01-17 19:07:46 -08:00
break : after cellCount ( self . code_page , source , self . token . start , end ) - 1 ;
2023-06-30 15:02:32 -07:00
} ,
} ,
} ;
}
} ;
2025-01-17 19:07:46 -08:00
/// Convenience struct only useful when the code page can be inferred from the token
pub const ErrorDetailsWithoutCodePage = blk : {
const details_info = @typeInfo ( ErrorDetails ) ;
const fields = details_info . @" struct " . fields ;
2025-04-30 18:42:23 +03:30
var field_names : [ fields . len - 1 ] [ ] const u8 = undefined ;
var field_types : [ fields . len - 1 ] type = undefined ;
var field_attrs : [ fields . len - 1 ] std . builtin . Type . StructField . Attributes = undefined ;
2025-01-17 19:07:46 -08:00
var i : usize = 0 ;
for ( fields ) | field | {
if ( std . mem . eql ( u8 , field . name , " code_page " ) ) continue ;
2025-04-30 18:42:23 +03:30
field_names [ i ] = field . name ;
field_types [ i ] = field . type ;
field_attrs [ i ] = . {
. @" comptime " = field . is_comptime ,
. @" align " = field . alignment ,
. default_value_ptr = field . default_value_ptr ,
} ;
2025-01-17 19:07:46 -08:00
i += 1 ;
}
2025-04-30 18:42:23 +03:30
std . debug . assert ( i == fields . len - 1 ) ;
break : blk @Struct ( . auto , null , & field_names , & field_types , & field_attrs ) ;
2025-01-17 19:07:46 -08:00
} ;
fn cellCount ( code_page : SupportedCodePage , source : [ ] const u8 , start_index : usize , end_index : usize ) usize {
// Note: This is an imperfect solution. A proper implementation here would
// involve full grapheme cluster awareness + grapheme width data, but oh well.
var codepoint_count : usize = 0 ;
var index : usize = start_index ;
while ( index < end_index ) {
const codepoint = code_page . codepointAt ( index , source ) orelse break ;
defer index += codepoint . byte_len ;
_ = codepointForDisplay ( codepoint ) orelse continue ;
codepoint_count += 1 ;
// no need to count more than we will display
if ( codepoint_count >= max_source_line_codepoints + truncated_str . len ) break ;
}
return codepoint_count ;
}
const truncated_str = " <...truncated...> " ;
2025-10-20 23:25:57 -07:00
pub fn renderErrorMessage (
io : Io ,
writer : * std . Io . Writer ,
tty_config : std . Io . tty . Config ,
cwd : std . fs . Dir ,
err_details : ErrorDetails ,
source : [ ] const u8 ,
strings : [ ] const [ ] const u8 ,
source_mappings : ? SourceMappings ,
) ! void {
2023-06-30 15:02:32 -07:00
if ( err_details . type == . hint ) return ;
2024-03-02 00:50:53 -08:00
const source_line_start = err_details . token . getLineStartForErrorDisplay ( source ) ;
2023-10-18 18:00:50 -07:00
// Treat tab stops as 1 column wide for error display purposes,
// and add one to get a 1-based column
const column = err_details . token . calculateColumn ( source , 1 , source_line_start ) + 1 ;
2024-03-02 00:50:53 -08:00
const corresponding_span : ? SourceMappings . CorrespondingSpan = if ( source_mappings ) | mappings |
mappings . getCorrespondingSpan ( err_details . token . line_number )
2023-10-18 18:00:50 -07:00
else
null ;
const corresponding_file : ? [ ] const u8 = if ( source_mappings != null and corresponding_span != null )
source_mappings .? . files . get ( corresponding_span .? . filename_offset )
else
null ;
2023-06-30 15:02:32 -07:00
const err_line = if ( corresponding_span ) | span | span . start_line else err_details . token . line_number ;
try tty_config . setColor ( writer , . bold ) ;
if ( corresponding_file ) | file | {
try writer . writeAll ( file ) ;
} else {
try tty_config . setColor ( writer , . dim ) ;
try writer . writeAll ( " <after preprocessor> " ) ;
try tty_config . setColor ( writer , . reset ) ;
try tty_config . setColor ( writer , . bold ) ;
}
try writer . print ( " :{d}:{d}: " , . { err_line , column } ) ;
switch ( err_details . type ) {
. err = > {
try tty_config . setColor ( writer , . red ) ;
try writer . writeAll ( " error: " ) ;
} ,
. warning = > {
try tty_config . setColor ( writer , . yellow ) ;
try writer . writeAll ( " warning: " ) ;
} ,
. note = > {
try tty_config . setColor ( writer , . cyan ) ;
try writer . writeAll ( " note: " ) ;
} ,
. hint = > unreachable ,
}
try tty_config . setColor ( writer , . reset ) ;
try tty_config . setColor ( writer , . bold ) ;
try err_details . render ( writer , source , strings ) ;
try writer . writeByte ( '\n' ) ;
try tty_config . setColor ( writer , . reset ) ;
if ( ! err_details . print_source_line ) {
try writer . writeByte ( '\n' ) ;
return ;
}
2024-03-02 00:50:53 -08:00
const source_line = err_details . token . getLineForErrorDisplay ( source , source_line_start ) ;
2025-01-17 19:07:46 -08:00
const visual_info = err_details . visualTokenInfo ( source_line_start , source_line_start + source_line . len , source ) ;
const truncated_visual_info = ErrorDetails . VisualTokenInfo {
. before_len = if ( visual_info . point_offset > max_source_line_codepoints and visual_info . before_len > 0 )
( visual_info . before_len + 1 ) -| ( visual_info . point_offset - max_source_line_codepoints )
else
visual_info . before_len ,
. point_offset = @min ( max_source_line_codepoints + 1 , visual_info . point_offset ) ,
. after_len = if ( visual_info . point_offset > max_source_line_codepoints )
@min ( truncated_str . len - 3 , visual_info . after_len )
else
@min ( max_source_line_codepoints - visual_info . point_offset + ( truncated_str . len - 2 ) , visual_info . after_len ) ,
} ;
2023-06-30 15:02:32 -07:00
// Need this to determine if the 'line originated from' note is worth printing
2025-01-17 19:07:46 -08:00
var source_line_for_display_buf : [ max_source_line_bytes ] u8 = undefined ;
const source_line_for_display = writeSourceSlice ( & source_line_for_display_buf , source_line , err_details . code_page ) ;
try writer . writeAll ( source_line_for_display . line ) ;
if ( source_line_for_display . truncated ) {
2023-06-30 15:02:32 -07:00
try tty_config . setColor ( writer , . dim ) ;
2025-01-17 19:07:46 -08:00
try writer . writeAll ( truncated_str ) ;
2023-06-30 15:02:32 -07:00
try tty_config . setColor ( writer , . reset ) ;
}
try writer . writeByte ( '\n' ) ;
try tty_config . setColor ( writer , . green ) ;
2025-01-17 19:07:46 -08:00
const num_spaces = truncated_visual_info . point_offset - truncated_visual_info . before_len ;
2025-07-07 19:33:20 -07:00
try writer . splatByteAll ( ' ' , num_spaces ) ;
try writer . splatByteAll ( '~' , truncated_visual_info . before_len ) ;
2023-06-30 15:02:32 -07:00
try writer . writeByte ( '^' ) ;
2025-07-07 19:33:20 -07:00
try writer . splatByteAll ( '~' , truncated_visual_info . after_len ) ;
2023-06-30 15:02:32 -07:00
try writer . writeByte ( '\n' ) ;
try tty_config . setColor ( writer , . reset ) ;
2023-10-18 18:00:50 -07:00
if ( corresponding_span != null and corresponding_file != null ) {
2025-01-17 19:07:46 -08:00
var worth_printing_lines : bool = true ;
var initial_lines_err : ? anyerror = null ;
2025-08-27 20:33:36 -07:00
var file_reader_buf : [ max_source_line_bytes * 2 ] u8 = undefined ;
2025-01-17 19:07:46 -08:00
var corresponding_lines : ? CorrespondingLines = CorrespondingLines . init (
2025-10-20 23:25:57 -07:00
io ,
2025-01-17 19:07:46 -08:00
cwd ,
err_details ,
source_line_for_display . line ,
corresponding_span .? ,
corresponding_file .? ,
2025-08-27 20:33:36 -07:00
& file_reader_buf ,
2025-01-17 19:07:46 -08:00
) catch | err | switch ( err ) {
error . NotWorthPrintingLines = > blk : {
worth_printing_lines = false ;
break : blk null ;
} ,
error . NotWorthPrintingNote = > return ,
else = > | e | blk : {
initial_lines_err = e ;
break : blk null ;
} ,
} ;
defer if ( corresponding_lines ) | * cl | cl . deinit ( ) ;
2023-06-30 15:02:32 -07:00
try tty_config . setColor ( writer , . bold ) ;
if ( corresponding_file ) | file | {
try writer . writeAll ( file ) ;
} else {
try tty_config . setColor ( writer , . dim ) ;
try writer . writeAll ( " <after preprocessor> " ) ;
try tty_config . setColor ( writer , . reset ) ;
try tty_config . setColor ( writer , . bold ) ;
}
try writer . print ( " :{d}:{d}: " , . { err_line , column } ) ;
try tty_config . setColor ( writer , . cyan ) ;
try writer . writeAll ( " note: " ) ;
try tty_config . setColor ( writer , . reset ) ;
try tty_config . setColor ( writer , . bold ) ;
try writer . writeAll ( " this line originated from line " ) ;
if ( corresponding_span .? . start_line != corresponding_span .? . end_line ) {
try writer . print ( " s {}-{} " , . { corresponding_span .? . start_line , corresponding_span .? . end_line } ) ;
} else {
try writer . print ( " {} " , . { corresponding_span .? . start_line } ) ;
}
try writer . print ( " of file '{s}' \n " , . { corresponding_file .? } ) ;
try tty_config . setColor ( writer , . reset ) ;
2025-01-17 19:07:46 -08:00
if ( ! worth_printing_lines ) return ;
const write_lines_err : ? anyerror = write_lines : {
if ( initial_lines_err ) | err | break : write_lines err ;
while ( corresponding_lines .? . next ( ) catch | err | {
break : write_lines err ;
} ) | display_line | {
try writer . writeAll ( display_line . line ) ;
if ( display_line . truncated ) {
try tty_config . setColor ( writer , . dim ) ;
try writer . writeAll ( truncated_str ) ;
try tty_config . setColor ( writer , . reset ) ;
}
try writer . writeByte ( '\n' ) ;
}
break : write_lines null ;
} ;
if ( write_lines_err ) | err | {
2023-06-30 15:02:32 -07:00
try tty_config . setColor ( writer , . red ) ;
try writer . writeAll ( " | " ) ;
try tty_config . setColor ( writer , . reset ) ;
try tty_config . setColor ( writer , . dim ) ;
2025-01-17 19:07:46 -08:00
try writer . print ( " unable to print line(s) from file: {s} \n " , . { @errorName ( err ) } ) ;
2023-06-30 15:02:32 -07:00
try tty_config . setColor ( writer , . reset ) ;
}
2025-01-17 19:07:46 -08:00
try writer . writeByte ( '\n' ) ;
2023-06-30 15:02:32 -07:00
}
}
2025-01-17 19:07:46 -08:00
const VisualLine = struct {
line : [ ] u8 ,
truncated : bool ,
} ;
2023-06-30 15:02:32 -07:00
2025-01-17 19:07:46 -08:00
const CorrespondingLines = struct {
// enough room for one more codepoint, just so that we don't have to keep
// track of this being truncated, since the extra codepoint will ensure
// the visual line will need to truncate in that case.
line_buf : [ max_source_line_bytes + 4 ] u8 = undefined ,
line_len : usize = 0 ,
visual_line_buf : [ max_source_line_bytes ] u8 = undefined ,
visual_line_len : usize = 0 ,
truncated : bool = false ,
line_num : usize = 1 ,
initial_line : bool = true ,
last_byte : u8 = 0 ,
at_eof : bool = false ,
span : SourceMappings . CorrespondingSpan ,
file : std . fs . File ,
2025-08-27 20:33:36 -07:00
file_reader : std . fs . File . Reader ,
2025-01-17 19:07:46 -08:00
code_page : SupportedCodePage ,
2025-08-27 20:33:36 -07:00
pub fn init (
2025-10-20 23:25:57 -07:00
io : Io ,
2025-08-27 20:33:36 -07:00
cwd : std . fs . Dir ,
err_details : ErrorDetails ,
line_for_comparison : [ ] const u8 ,
corresponding_span : SourceMappings . CorrespondingSpan ,
corresponding_file : [ ] const u8 ,
file_reader_buf : [ ] u8 ,
) ! CorrespondingLines {
2023-06-30 15:02:32 -07:00
// We don't do line comparison for this error, so don't print the note if the line
// number is different
2025-01-17 19:07:46 -08:00
if ( err_details . err == . string_literal_too_long and err_details . token . line_number != corresponding_span . start_line ) {
return error . NotWorthPrintingNote ;
2023-06-30 15:02:32 -07:00
}
// Don't print the originating line for this error, we know it's really long
if ( err_details . err == . string_literal_too_long ) {
2025-01-17 19:07:46 -08:00
return error . NotWorthPrintingLines ;
2023-06-30 15:02:32 -07:00
}
2025-01-17 19:07:46 -08:00
var corresponding_lines = CorrespondingLines {
. span = corresponding_span ,
. file = try utils . openFileNotDir ( cwd , corresponding_file , . { } ) ,
. code_page = err_details . code_page ,
2025-08-27 20:33:36 -07:00
. file_reader = undefined ,
2025-01-17 19:07:46 -08:00
} ;
2025-10-20 23:25:57 -07:00
corresponding_lines . file_reader = corresponding_lines . file . reader ( io , file_reader_buf ) ;
2025-01-17 19:07:46 -08:00
errdefer corresponding_lines . deinit ( ) ;
try corresponding_lines . writeLineFromStreamVerbatim (
2025-08-27 20:33:36 -07:00
& corresponding_lines . file_reader . interface ,
2025-01-17 19:07:46 -08:00
corresponding_span . start_line ,
) ;
const visual_line = writeSourceSlice (
& corresponding_lines . visual_line_buf ,
corresponding_lines . line_buf [ 0 .. corresponding_lines . line_len ] ,
err_details . code_page ,
) ;
corresponding_lines . visual_line_len = visual_line . line . len ;
corresponding_lines . truncated = visual_line . truncated ;
2023-06-30 15:02:32 -07:00
// If the lines are the same as they were before preprocessing, skip printing the note entirely
2025-01-17 19:07:46 -08:00
if ( corresponding_span . start_line == corresponding_span . end_line and std . mem . eql (
u8 ,
line_for_comparison ,
corresponding_lines . visual_line_buf [ 0 .. corresponding_lines . visual_line_len ] ,
) ) {
return error . NotWorthPrintingNote ;
2023-06-30 15:02:32 -07:00
}
2025-01-17 19:07:46 -08:00
2023-06-30 15:02:32 -07:00
return corresponding_lines ;
}
2025-01-17 19:07:46 -08:00
pub fn next ( self : * CorrespondingLines ) ! ? VisualLine {
if ( self . initial_line ) {
self . initial_line = false ;
return . {
. line = self . visual_line_buf [ 0 .. self . visual_line_len ] ,
. truncated = self . truncated ,
} ;
}
if ( self . line_num > self . span . end_line ) return null ;
if ( self . at_eof ) return error . LinesNotFound ;
self . line_len = 0 ;
self . visual_line_len = 0 ;
try self . writeLineFromStreamVerbatim (
2025-08-27 20:33:36 -07:00
& self . file_reader . interface ,
2025-01-17 19:07:46 -08:00
self . line_num ,
) ;
const visual_line = writeSourceSlice (
& self . visual_line_buf ,
self . line_buf [ 0 .. self . line_len ] ,
self . code_page ,
) ;
self . visual_line_len = visual_line . line . len ;
return visual_line ;
}
2025-08-27 20:33:36 -07:00
fn writeLineFromStreamVerbatim ( self : * CorrespondingLines , input : * std . Io . Reader , line_num : usize ) ! void {
2025-01-17 19:07:46 -08:00
while ( try readByteOrEof ( input ) ) | byte | {
switch ( byte ) {
'\n' , '\r' = > {
if ( ! utils . isLineEndingPair ( self . last_byte , byte ) ) {
const line_complete = self . line_num == line_num ;
self . line_num += 1 ;
if ( line_complete ) {
self . last_byte = byte ;
return ;
}
} else {
// reset last_byte to a non-line ending so that
// consecutive CRLF pairs don't get treated as one
// long line ending 'pair'
self . last_byte = 0 ;
continue ;
}
} ,
else = > {
2025-08-27 20:33:36 -07:00
if ( self . line_num == line_num and self . line_len < self . line_buf . len ) {
self . line_buf [ self . line_len ] = byte ;
self . line_len += 1 ;
2025-01-17 19:07:46 -08:00
}
} ,
}
self . last_byte = byte ;
}
self . at_eof = true ;
// hacky way to get next to return null
self . line_num += 1 ;
}
2025-08-27 20:33:36 -07:00
fn readByteOrEof ( reader : * std . Io . Reader ) ! ? u8 {
return reader . takeByte ( ) catch | err | switch ( err ) {
2025-01-17 19:07:46 -08:00
error . EndOfStream = > return null ,
else = > | e | return e ,
} ;
}
pub fn deinit ( self : * CorrespondingLines ) void {
self . file . close ( ) ;
2023-06-30 15:02:32 -07:00
}
} ;
2025-01-17 19:07:46 -08:00
const max_source_line_codepoints = 120 ;
const max_source_line_bytes = max_source_line_codepoints * 4 ;
fn writeSourceSlice ( buf : [ ] u8 , slice : [ ] const u8 , code_page : SupportedCodePage ) VisualLine {
var src_i : usize = 0 ;
var dest_i : usize = 0 ;
var codepoint_count : usize = 0 ;
while ( src_i < slice . len ) {
const codepoint = code_page . codepointAt ( src_i , slice ) orelse break ;
defer src_i += codepoint . byte_len ;
const display_codepoint = codepointForDisplay ( codepoint ) orelse continue ;
codepoint_count += 1 ;
if ( codepoint_count > max_source_line_codepoints ) {
return . { . line = buf [ 0 .. dest_i ] , . truncated = true } ;
}
const utf8_len = std . unicode . utf8Encode ( display_codepoint , buf [ dest_i .. ] ) catch unreachable ;
dest_i += utf8_len ;
}
return . { . line = buf [ 0 .. dest_i ] , . truncated = false } ;
2023-06-30 15:02:32 -07:00
}
2025-01-17 19:07:46 -08:00
fn codepointForDisplay ( codepoint : code_pages . Codepoint ) ? u21 {
return switch ( codepoint . value ) {
'\x00' .. . '\x08' ,
'\x0E' .. . '\x1F' ,
'\x7F' ,
code_pages . Codepoint . invalid ,
= > '<27> ' ,
2023-06-30 15:02:32 -07:00
// \r is seemingly ignored by the RC compiler so skipping it when printing source lines
// could help avoid confusing output (e.g. RC\rDATA if printed verbatim would show up
// in the console as DATA but the compiler reads it as RCDATA)
//
// NOTE: This is irrelevant when using the clang preprocessor, because unpaired \r
// characters get converted to \n, but may become relevant if another
// preprocessor is used instead.
2025-01-17 19:07:46 -08:00
'\r' = > null ,
'\t' , '\x0B' , '\x0C' = > ' ' ,
else = > | v | v ,
2023-06-30 15:02:32 -07:00
} ;
}