const std = @import("std"); const Lexer = @import("lex.zig").Lexer; const Token = @import("lex.zig").Token; const Node = @import("ast.zig").Node; const Tree = @import("ast.zig").Tree; const CodePageLookup = @import("ast.zig").CodePageLookup; const Resource = @import("rc.zig").Resource; const Allocator = std.mem.Allocator; const ErrorDetails = @import("errors.zig").ErrorDetails; const Diagnostics = @import("errors.zig").Diagnostics; const SourceBytes = @import("literals.zig").SourceBytes; const Compiler = @import("compile.zig").Compiler; const rc = @import("rc.zig"); const res = @import("res.zig"); // TODO: Make these configurable? pub const max_nested_menu_level: u32 = 512; pub const max_nested_version_level: u32 = 512; pub const max_nested_expression_level: u32 = 200; pub const Parser = struct { const Self = @This(); lexer: *Lexer, /// values that need to be initialized per-parse state: Parser.State = undefined, options: Parser.Options, pub const Error = error{ParseError} || Allocator.Error; pub const Options = struct { warn_instead_of_error_on_invalid_code_page: bool = false, }; pub fn init(lexer: *Lexer, options: Options) Parser { return Parser{ .lexer = lexer, .options = options, }; } pub const State = struct { token: Token, lookahead_lexer: Lexer, allocator: Allocator, arena: Allocator, diagnostics: *Diagnostics, input_code_page_lookup: CodePageLookup, output_code_page_lookup: CodePageLookup, }; pub fn parse(self: *Self, allocator: Allocator, diagnostics: *Diagnostics) Error!*Tree { var arena = std.heap.ArenaAllocator.init(allocator); errdefer arena.deinit(); self.state = Parser.State{ .token = undefined, .lookahead_lexer = undefined, .allocator = allocator, .arena = arena.allocator(), .diagnostics = diagnostics, .input_code_page_lookup = CodePageLookup.init(arena.allocator(), self.lexer.default_code_page), .output_code_page_lookup = CodePageLookup.init(arena.allocator(), self.lexer.default_code_page), }; const parsed_root = try self.parseRoot(); const tree = try self.state.arena.create(Tree); tree.* = .{ .node = parsed_root, .input_code_pages = self.state.input_code_page_lookup, .output_code_pages = self.state.output_code_page_lookup, .source = self.lexer.buffer, .arena = arena.state, .allocator = allocator, }; return tree; } fn parseRoot(self: *Self) Error!*Node { var statements = std.ArrayList(*Node).init(self.state.allocator); defer statements.deinit(); try self.parseStatements(&statements); try self.check(.eof); const node = try self.state.arena.create(Node.Root); node.* = .{ .body = try self.state.arena.dupe(*Node, statements.items), }; return &node.base; } fn parseStatements(self: *Self, statements: *std.ArrayList(*Node)) Error!void { while (true) { try self.nextToken(.whitespace_delimiter_only); if (self.state.token.id == .eof) break; // The Win32 compiler will sometimes try to recover from errors // and then restart parsing afterwards. We don't ever do this // because it almost always leads to unhelpful error messages // (usually it will end up with bogus things like 'file // not found: {') const statement = try self.parseStatement(); try statements.append(statement); } } /// Expects the current token to be the token before possible common resource attributes. /// After return, the current token will be the token immediately before the end of the /// common resource attributes (if any). If there are no common resource attributes, the /// current token is unchanged. /// The returned slice is allocated by the parser's arena fn parseCommonResourceAttributes(self: *Self) ![]Token { var common_resource_attributes: std.ArrayListUnmanaged(Token) = .empty; while (true) { const maybe_common_resource_attribute = try self.lookaheadToken(.normal); if (maybe_common_resource_attribute.id == .literal and rc.CommonResourceAttributes.map.has(maybe_common_resource_attribute.slice(self.lexer.buffer))) { try common_resource_attributes.append(self.state.arena, maybe_common_resource_attribute); self.nextToken(.normal) catch unreachable; } else { break; } } return common_resource_attributes.toOwnedSlice(self.state.arena); } /// Expects the current token to have already been dealt with, and that the /// optional statements will potentially start on the next token. /// After return, the current token will be the token immediately before the end of the /// optional statements (if any). If there are no optional statements, the /// current token is unchanged. /// The returned slice is allocated by the parser's arena fn parseOptionalStatements(self: *Self, resource: Resource) ![]*Node { var optional_statements: std.ArrayListUnmanaged(*Node) = .empty; while (true) { const lookahead_token = try self.lookaheadToken(.normal); if (lookahead_token.id != .literal) break; const slice = lookahead_token.slice(self.lexer.buffer); const optional_statement_type = rc.OptionalStatements.map.get(slice) orelse switch (resource) { .dialog, .dialogex => rc.OptionalStatements.dialog_map.get(slice) orelse break, else => break, }; self.nextToken(.normal) catch unreachable; switch (optional_statement_type) { .language => { const language = try self.parseLanguageStatement(); try optional_statements.append(self.state.arena, language); }, // Number only .version, .characteristics, .style, .exstyle => { const identifier = self.state.token; const value = try self.parseExpression(.{ .can_contain_not_expressions = optional_statement_type == .style or optional_statement_type == .exstyle, .allowed_types = .{ .number = true }, }); const node = try self.state.arena.create(Node.SimpleStatement); node.* = .{ .identifier = identifier, .value = value, }; try optional_statements.append(self.state.arena, &node.base); }, // String only .caption => { const identifier = self.state.token; try self.nextToken(.normal); const value = self.state.token; if (!value.isStringLiteral()) { return self.addErrorDetailsAndFail(ErrorDetails{ .err = .expected_something_else, .token = value, .extra = .{ .expected_types = .{ .string_literal = true, } }, }); } const value_node = try self.state.arena.create(Node.Literal); value_node.* = .{ .token = value, }; const node = try self.state.arena.create(Node.SimpleStatement); node.* = .{ .identifier = identifier, .value = &value_node.base, }; try optional_statements.append(self.state.arena, &node.base); }, // String or number .class => { const identifier = self.state.token; const value = try self.parseExpression(.{ .allowed_types = .{ .number = true, .string = true } }); const node = try self.state.arena.create(Node.SimpleStatement); node.* = .{ .identifier = identifier, .value = value, }; try optional_statements.append(self.state.arena, &node.base); }, // Special case .menu => { const identifier = self.state.token; try self.nextToken(.whitespace_delimiter_only); try self.check(.literal); const value_node = try self.state.arena.create(Node.Literal); value_node.* = .{ .token = self.state.token, }; const node = try self.state.arena.create(Node.SimpleStatement); node.* = .{ .identifier = identifier, .value = &value_node.base, }; try optional_statements.append(self.state.arena, &node.base); }, .font => { const identifier = self.state.token; const point_size = try self.parseExpression(.{ .allowed_types = .{ .number = true } }); // The comma between point_size and typeface is both optional and // there can be any number of them try self.skipAnyCommas(); try self.nextToken(.normal); const typeface = self.state.token; if (!typeface.isStringLiteral()) { return self.addErrorDetailsAndFail(ErrorDetails{ .err = .expected_something_else, .token = typeface, .extra = .{ .expected_types = .{ .string_literal = true, } }, }); } const ExSpecificValues = struct { weight: ?*Node = null, italic: ?*Node = null, char_set: ?*Node = null, }; var ex_specific = ExSpecificValues{}; ex_specific: { var optional_param_parser = OptionalParamParser{ .parser = self }; switch (resource) { .dialogex => { { ex_specific.weight = try optional_param_parser.parse(.{}); if (optional_param_parser.finished) break :ex_specific; } { if (!(try self.parseOptionalToken(.comma))) break :ex_specific; ex_specific.italic = try self.parseExpression(.{ .allowed_types = .{ .number = true } }); } { ex_specific.char_set = try optional_param_parser.parse(.{}); if (optional_param_parser.finished) break :ex_specific; } }, .dialog => {}, else => unreachable, // only DIALOG and DIALOGEX have FONT optional-statements } } const node = try self.state.arena.create(Node.FontStatement); node.* = .{ .identifier = identifier, .point_size = point_size, .typeface = typeface, .weight = ex_specific.weight, .italic = ex_specific.italic, .char_set = ex_specific.char_set, }; try optional_statements.append(self.state.arena, &node.base); }, } } return optional_statements.toOwnedSlice(self.state.arena); } /// Expects the current token to be the first token of the statement. fn parseStatement(self: *Self) Error!*Node { const first_token = self.state.token; std.debug.assert(first_token.id == .literal); if (rc.TopLevelKeywords.map.get(first_token.slice(self.lexer.buffer))) |keyword| switch (keyword) { .language => { const language_statement = try self.parseLanguageStatement(); return language_statement; }, .version, .characteristics => { const identifier = self.state.token; const value = try self.parseExpression(.{ .allowed_types = .{ .number = true } }); const node = try self.state.arena.create(Node.SimpleStatement); node.* = .{ .identifier = identifier, .value = value, }; return &node.base; }, .stringtable => { // common resource attributes must all be contiguous and come before optional-statements const common_resource_attributes = try self.parseCommonResourceAttributes(); const optional_statements = try self.parseOptionalStatements(.stringtable); try self.nextToken(.normal); const begin_token = self.state.token; try self.check(.begin); var strings = std.ArrayList(*Node).init(self.state.allocator); defer strings.deinit(); while (true) { const maybe_end_token = try self.lookaheadToken(.normal); switch (maybe_end_token.id) { .end => { self.nextToken(.normal) catch unreachable; break; }, .eof => { return self.addErrorDetailsAndFail(ErrorDetails{ .err = .unfinished_string_table_block, .token = maybe_end_token, }); }, else => {}, } const id_expression = try self.parseExpression(.{ .allowed_types = .{ .number = true } }); const comma_token: ?Token = if (try self.parseOptionalToken(.comma)) self.state.token else null; try self.nextToken(.normal); if (self.state.token.id != .quoted_ascii_string and self.state.token.id != .quoted_wide_string) { return self.addErrorDetailsAndFail(ErrorDetails{ .err = .expected_something_else, .token = self.state.token, .extra = .{ .expected_types = .{ .string_literal = true } }, }); } const string_node = try self.state.arena.create(Node.StringTableString); string_node.* = .{ .id = id_expression, .maybe_comma = comma_token, .string = self.state.token, }; try strings.append(&string_node.base); } if (strings.items.len == 0) { return self.addErrorDetailsAndFail(ErrorDetails{ .err = .expected_token, // TODO: probably a more specific error message .token = self.state.token, .extra = .{ .expected = .number }, }); } const end_token = self.state.token; try self.check(.end); const node = try self.state.arena.create(Node.StringTable); node.* = .{ .type = first_token, .common_resource_attributes = common_resource_attributes, .optional_statements = optional_statements, .begin_token = begin_token, .strings = try self.state.arena.dupe(*Node, strings.items), .end_token = end_token, }; return &node.base; }, }; // The Win32 RC compiler allows for a 'dangling' literal at the end of a file // (as long as it's not a valid top-level keyword), and there is actually an // .rc file with a such a dangling literal in the Windows-classic-samples set // of projects. So, we have special compatibility for this particular case. const maybe_eof = try self.lookaheadToken(.whitespace_delimiter_only); if (maybe_eof.id == .eof) { // TODO: emit warning var context = try self.state.arena.alloc(Token, 2); context[0] = first_token; context[1] = maybe_eof; const invalid_node = try self.state.arena.create(Node.Invalid); invalid_node.* = .{ .context = context, }; return &invalid_node.base; } const id_token = first_token; const id_code_page = self.lexer.current_code_page; try self.nextToken(.whitespace_delimiter_only); const resource = try self.checkResource(); const type_token = self.state.token; if (resource == .string_num) { try self.addErrorDetails(.{ .err = .string_resource_as_numeric_type, .token = type_token, }); return self.addErrorDetailsAndFail(.{ .err = .string_resource_as_numeric_type, .token = type_token, .type = .note, .print_source_line = false, }); } if (resource == .font) { const id_bytes = SourceBytes{ .slice = id_token.slice(self.lexer.buffer), .code_page = id_code_page, }; const maybe_ordinal = res.NameOrOrdinal.maybeOrdinalFromString(id_bytes); if (maybe_ordinal == null) { const would_be_win32_rc_ordinal = res.NameOrOrdinal.maybeNonAsciiOrdinalFromString(id_bytes); if (would_be_win32_rc_ordinal) |win32_rc_ordinal| { try self.addErrorDetails(ErrorDetails{ .err = .id_must_be_ordinal, .token = id_token, .extra = .{ .resource = resource }, }); return self.addErrorDetailsAndFail(ErrorDetails{ .err = .win32_non_ascii_ordinal, .token = id_token, .type = .note, .print_source_line = false, .extra = .{ .number = win32_rc_ordinal.ordinal }, }); } else { return self.addErrorDetailsAndFail(ErrorDetails{ .err = .id_must_be_ordinal, .token = id_token, .extra = .{ .resource = resource }, }); } } } switch (resource) { .accelerators => { // common resource attributes must all be contiguous and come before optional-statements const common_resource_attributes = try self.parseCommonResourceAttributes(); const optional_statements = try self.parseOptionalStatements(resource); try self.nextToken(.normal); const begin_token = self.state.token; try self.check(.begin); var accelerators: std.ArrayListUnmanaged(*Node) = .empty; while (true) { const lookahead = try self.lookaheadToken(.normal); switch (lookahead.id) { .end, .eof => { self.nextToken(.normal) catch unreachable; break; }, else => {}, } const event = try self.parseExpression(.{ .allowed_types = .{ .number = true, .string = true } }); try self.nextToken(.normal); try self.check(.comma); const idvalue = try self.parseExpression(.{ .allowed_types = .{ .number = true } }); var type_and_options: std.ArrayListUnmanaged(Token) = .empty; while (true) { if (!(try self.parseOptionalToken(.comma))) break; try self.nextToken(.normal); if (!rc.AcceleratorTypeAndOptions.map.has(self.tokenSlice())) { return self.addErrorDetailsAndFail(.{ .err = .expected_something_else, .token = self.state.token, .extra = .{ .expected_types = .{ .accelerator_type_or_option = true, } }, }); } try type_and_options.append(self.state.arena, self.state.token); } const node = try self.state.arena.create(Node.Accelerator); node.* = .{ .event = event, .idvalue = idvalue, .type_and_options = try type_and_options.toOwnedSlice(self.state.arena), }; try accelerators.append(self.state.arena, &node.base); } const end_token = self.state.token; try self.check(.end); const node = try self.state.arena.create(Node.Accelerators); node.* = .{ .id = id_token, .type = type_token, .common_resource_attributes = common_resource_attributes, .optional_statements = optional_statements, .begin_token = begin_token, .accelerators = try accelerators.toOwnedSlice(self.state.arena), .end_token = end_token, }; return &node.base; }, .dialog, .dialogex => { // common resource attributes must all be contiguous and come before optional-statements const common_resource_attributes = try self.parseCommonResourceAttributes(); const x = try self.parseExpression(.{ .allowed_types = .{ .number = true } }); _ = try self.parseOptionalToken(.comma); const y = try self.parseExpression(.{ .allowed_types = .{ .number = true } }); _ = try self.parseOptionalToken(.comma); const width = try self.parseExpression(.{ .allowed_types = .{ .number = true } }); _ = try self.parseOptionalToken(.comma); const height = try self.parseExpression(.{ .allowed_types = .{ .number = true } }); var optional_param_parser = OptionalParamParser{ .parser = self }; const help_id: ?*Node = try optional_param_parser.parse(.{}); const optional_statements = try self.parseOptionalStatements(resource); try self.nextToken(.normal); const begin_token = self.state.token; try self.check(.begin); var controls: std.ArrayListUnmanaged(*Node) = .empty; defer controls.deinit(self.state.allocator); while (try self.parseControlStatement(resource)) |control_node| { // The number of controls must fit in a u16 in order for it to // be able to be written into the relevant field in the .res data. if (controls.items.len >= std.math.maxInt(u16)) { try self.addErrorDetails(.{ .err = .too_many_dialog_controls_or_toolbar_buttons, .token = id_token, .extra = .{ .resource = resource }, }); return self.addErrorDetailsAndFail(.{ .err = .too_many_dialog_controls_or_toolbar_buttons, .type = .note, .token = control_node.getFirstToken(), .token_span_end = control_node.getLastToken(), .extra = .{ .resource = resource }, }); } try controls.append(self.state.allocator, control_node); } try self.nextToken(.normal); const end_token = self.state.token; try self.check(.end); const node = try self.state.arena.create(Node.Dialog); node.* = .{ .id = id_token, .type = type_token, .common_resource_attributes = common_resource_attributes, .x = x, .y = y, .width = width, .height = height, .help_id = help_id, .optional_statements = optional_statements, .begin_token = begin_token, .controls = try self.state.arena.dupe(*Node, controls.items), .end_token = end_token, }; return &node.base; }, .toolbar => { // common resource attributes must all be contiguous and come before optional-statements const common_resource_attributes = try self.parseCommonResourceAttributes(); const button_width = try self.parseExpression(.{ .allowed_types = .{ .number = true } }); try self.nextToken(.normal); try self.check(.comma); const button_height = try self.parseExpression(.{ .allowed_types = .{ .number = true } }); try self.nextToken(.normal); const begin_token = self.state.token; try self.check(.begin); var buttons: std.ArrayListUnmanaged(*Node) = .empty; defer buttons.deinit(self.state.allocator); while (try self.parseToolbarButtonStatement()) |button_node| { // The number of buttons must fit in a u16 in order for it to // be able to be written into the relevant field in the .res data. if (buttons.items.len >= std.math.maxInt(u16)) { try self.addErrorDetails(.{ .err = .too_many_dialog_controls_or_toolbar_buttons, .token = id_token, .extra = .{ .resource = resource }, }); return self.addErrorDetailsAndFail(.{ .err = .too_many_dialog_controls_or_toolbar_buttons, .type = .note, .token = button_node.getFirstToken(), .token_span_end = button_node.getLastToken(), .extra = .{ .resource = resource }, }); } try buttons.append(self.state.allocator, button_node); } try self.nextToken(.normal); const end_token = self.state.token; try self.check(.end); const node = try self.state.arena.create(Node.Toolbar); node.* = .{ .id = id_token, .type = type_token, .common_resource_attributes = common_resource_attributes, .button_width = button_width, .button_height = button_height, .begin_token = begin_token, .buttons = try self.state.arena.dupe(*Node, buttons.items), .end_token = end_token, }; return &node.base; }, .menu, .menuex => { // common resource attributes must all be contiguous and come before optional-statements const common_resource_attributes = try self.parseCommonResourceAttributes(); // help id is optional but must come between common resource attributes and optional-statements var help_id: ?*Node = null; // Note: No comma is allowed before or after help_id of MENUEX and help_id is not // a possible field of MENU. if (resource == .menuex and try self.lookaheadCouldBeNumberExpression(.not_disallowed)) { help_id = try self.parseExpression(.{ .is_known_to_be_number_expression = true, }); } const optional_statements = try self.parseOptionalStatements(.stringtable); try self.nextToken(.normal); const begin_token = self.state.token; try self.check(.begin); var items: std.ArrayListUnmanaged(*Node) = .empty; defer items.deinit(self.state.allocator); while (try self.parseMenuItemStatement(resource, id_token, 1)) |item_node| { try items.append(self.state.allocator, item_node); } try self.nextToken(.normal); const end_token = self.state.token; try self.check(.end); if (items.items.len == 0) { return self.addErrorDetailsAndFail(.{ .err = .empty_menu_not_allowed, .token = type_token, }); } const node = try self.state.arena.create(Node.Menu); node.* = .{ .id = id_token, .type = type_token, .common_resource_attributes = common_resource_attributes, .optional_statements = optional_statements, .help_id = help_id, .begin_token = begin_token, .items = try self.state.arena.dupe(*Node, items.items), .end_token = end_token, }; return &node.base; }, .versioninfo => { // common resource attributes must all be contiguous and come before optional-statements const common_resource_attributes = try self.parseCommonResourceAttributes(); var fixed_info: std.ArrayListUnmanaged(*Node) = .empty; while (try self.parseVersionStatement()) |version_statement| { try fixed_info.append(self.state.arena, version_statement); } try self.nextToken(.normal); const begin_token = self.state.token; try self.check(.begin); var block_statements: std.ArrayListUnmanaged(*Node) = .empty; while (try self.parseVersionBlockOrValue(id_token, 1)) |block_node| { try block_statements.append(self.state.arena, block_node); } try self.nextToken(.normal); const end_token = self.state.token; try self.check(.end); const node = try self.state.arena.create(Node.VersionInfo); node.* = .{ .id = id_token, .versioninfo = type_token, .common_resource_attributes = common_resource_attributes, .fixed_info = try fixed_info.toOwnedSlice(self.state.arena), .begin_token = begin_token, .block_statements = try block_statements.toOwnedSlice(self.state.arena), .end_token = end_token, }; return &node.base; }, .dlginclude => { const common_resource_attributes = try self.parseCommonResourceAttributes(); const filename_expression = try self.parseExpression(.{ .allowed_types = .{ .string = true }, }); const node = try self.state.arena.create(Node.ResourceExternal); node.* = .{ .id = id_token, .type = type_token, .common_resource_attributes = common_resource_attributes, .filename = filename_expression, }; return &node.base; }, .stringtable => { return self.addErrorDetailsAndFail(.{ .err = .name_or_id_not_allowed, .token = id_token, .extra = .{ .resource = resource }, }); }, // Just try everything as a 'generic' resource (raw data or external file) // TODO: More fine-grained switch cases as necessary else => { const common_resource_attributes = try self.parseCommonResourceAttributes(); const maybe_begin = try self.lookaheadToken(.normal); if (maybe_begin.id == .begin) { self.nextToken(.normal) catch unreachable; if (!resource.canUseRawData()) { try self.addErrorDetails(ErrorDetails{ .err = .resource_type_cant_use_raw_data, .token = maybe_begin, .extra = .{ .resource = resource }, }); return self.addErrorDetailsAndFail(ErrorDetails{ .err = .resource_type_cant_use_raw_data, .type = .note, .print_source_line = false, .token = maybe_begin, }); } const raw_data = try self.parseRawDataBlock(); const end_token = self.state.token; const node = try self.state.arena.create(Node.ResourceRawData); node.* = .{ .id = id_token, .type = type_token, .common_resource_attributes = common_resource_attributes, .begin_token = maybe_begin, .raw_data = raw_data, .end_token = end_token, }; return &node.base; } const filename_expression = try self.parseExpression(.{ // Don't tell the user that numbers are accepted since we error on // number expressions and regular number literals are treated as unquoted // literals rather than numbers, so from the users perspective // numbers aren't really allowed. .expected_types_override = .{ .literal = true, .string_literal = true, }, }); const node = try self.state.arena.create(Node.ResourceExternal); node.* = .{ .id = id_token, .type = type_token, .common_resource_attributes = common_resource_attributes, .filename = filename_expression, }; return &node.base; }, } } /// Expects the current token to be a begin token. /// After return, the current token will be the end token. fn parseRawDataBlock(self: *Self) Error![]*Node { var raw_data = std.ArrayList(*Node).init(self.state.allocator); defer raw_data.deinit(); while (true) { const maybe_end_token = try self.lookaheadToken(.normal); switch (maybe_end_token.id) { .comma => { // comma as the first token in a raw data block is an error if (raw_data.items.len == 0) { return self.addErrorDetailsAndFail(ErrorDetails{ .err = .expected_something_else, .token = maybe_end_token, .extra = .{ .expected_types = .{ .number = true, .number_expression = true, .string_literal = true, } }, }); } // otherwise just skip over commas self.nextToken(.normal) catch unreachable; continue; }, .end => { self.nextToken(.normal) catch unreachable; break; }, .eof => { return self.addErrorDetailsAndFail(ErrorDetails{ .err = .unfinished_raw_data_block, .token = maybe_end_token, }); }, else => {}, } const expression = try self.parseExpression(.{ .allowed_types = .{ .number = true, .string = true } }); try raw_data.append(expression); if (expression.isNumberExpression()) { const maybe_close_paren = try self.lookaheadToken(.normal); if (maybe_close_paren.id == .close_paren) { // ) is an error return self.addErrorDetailsAndFail(ErrorDetails{ .err = .expected_token, .token = maybe_close_paren, .extra = .{ .expected = .operator }, }); } } } return try self.state.arena.dupe(*Node, raw_data.items); } /// Expects the current token to be handled, and that the control statement will /// begin on the next token. /// After return, the current token will be the token immediately before the end of the /// control statement (or unchanged if the function returns null). fn parseControlStatement(self: *Self, resource: Resource) Error!?*Node { const control_token = try self.lookaheadToken(.normal); const control = rc.Control.map.get(control_token.slice(self.lexer.buffer)) orelse return null; self.nextToken(.normal) catch unreachable; try self.skipAnyCommas(); var text: ?Token = null; if (control.hasTextParam()) { try self.nextToken(.normal); switch (self.state.token.id) { .quoted_ascii_string, .quoted_wide_string, .number => { text = self.state.token; }, else => { return self.addErrorDetailsAndFail(ErrorDetails{ .err = .expected_something_else, .token = self.state.token, .extra = .{ .expected_types = .{ .number = true, .string_literal = true, } }, }); }, } try self.skipAnyCommas(); } const id = try self.parseExpression(.{ .allowed_types = .{ .number = true } }); try self.skipAnyCommas(); var class: ?*Node = null; var style: ?*Node = null; if (control == .control) { class = try self.parseExpression(.{}); if (class.?.id == .literal) { const class_literal: *Node.Literal = @alignCast(@fieldParentPtr("base", class.?)); const is_invalid_control_class = class_literal.token.id == .literal and !rc.ControlClass.map.has(class_literal.token.slice(self.lexer.buffer)); if (is_invalid_control_class) { return self.addErrorDetailsAndFail(.{ .err = .expected_something_else, .token = self.state.token, .extra = .{ .expected_types = .{ .control_class = true, } }, }); } } try self.skipAnyCommas(); style = try self.parseExpression(.{ .can_contain_not_expressions = true, .allowed_types = .{ .number = true }, }); // If there is no comma after the style paramter, the Win32 RC compiler // could misinterpret the statement and end up skipping over at least one token // that should have been interepeted as the next parameter (x). For example: // CONTROL "text", 1, BUTTON, 15 30, 1, 2, 3, 4 // the `15` is the style parameter, but in the Win32 implementation the `30` // is completely ignored (i.e. the `1, 2, 3, 4` are `x`, `y`, `w`, `h`). // If a comma is added after the `15`, then `30` gets interpreted (correctly) // as the `x` value. // // Instead of emulating this behavior, we just warn about the potential for // weird behavior in the Win32 implementation whenever there isn't a comma after // the style parameter. const lookahead_token = try self.lookaheadToken(.normal); if (lookahead_token.id != .comma and lookahead_token.id != .eof) { try self.addErrorDetails(.{ .err = .rc_could_miscompile_control_params, .type = .warning, .token = lookahead_token, }); try self.addErrorDetails(.{ .err = .rc_could_miscompile_control_params, .type = .note, .token = style.?.getFirstToken(), .token_span_end = style.?.getLastToken(), }); } try self.skipAnyCommas(); } const x = try self.parseExpression(.{ .allowed_types = .{ .number = true } }); _ = try self.parseOptionalToken(.comma); const y = try self.parseExpression(.{ .allowed_types = .{ .number = true } }); _ = try self.parseOptionalToken(.comma); const width = try self.parseExpression(.{ .allowed_types = .{ .number = true } }); _ = try self.parseOptionalToken(.comma); const height = try self.parseExpression(.{ .allowed_types = .{ .number = true } }); var optional_param_parser = OptionalParamParser{ .parser = self }; if (control != .control) { style = try optional_param_parser.parse(.{ .not_expression_allowed = true }); } const exstyle: ?*Node = try optional_param_parser.parse(.{ .not_expression_allowed = true }); const help_id: ?*Node = switch (resource) { .dialogex => try optional_param_parser.parse(.{}), else => null, }; var extra_data: []*Node = &[_]*Node{}; var extra_data_begin: ?Token = null; var extra_data_end: ?Token = null; // extra data is DIALOGEX-only if (resource == .dialogex and try self.parseOptionalToken(.begin)) { extra_data_begin = self.state.token; extra_data = try self.parseRawDataBlock(); extra_data_end = self.state.token; } const node = try self.state.arena.create(Node.ControlStatement); node.* = .{ .type = control_token, .text = text, .class = class, .id = id, .x = x, .y = y, .width = width, .height = height, .style = style, .exstyle = exstyle, .help_id = help_id, .extra_data_begin = extra_data_begin, .extra_data = extra_data, .extra_data_end = extra_data_end, }; return &node.base; } fn parseToolbarButtonStatement(self: *Self) Error!?*Node { const keyword_token = try self.lookaheadToken(.normal); const button_type = rc.ToolbarButton.map.get(keyword_token.slice(self.lexer.buffer)) orelse return null; self.nextToken(.normal) catch unreachable; switch (button_type) { .separator => { const node = try self.state.arena.create(Node.Literal); node.* = .{ .token = keyword_token, }; return &node.base; }, .button => { const button_id = try self.parseExpression(.{ .allowed_types = .{ .number = true } }); const node = try self.state.arena.create(Node.SimpleStatement); node.* = .{ .identifier = keyword_token, .value = button_id, }; return &node.base; }, } } /// Expects the current token to be handled, and that the menuitem/popup statement will /// begin on the next token. /// After return, the current token will be the token immediately before the end of the /// menuitem statement (or unchanged if the function returns null). fn parseMenuItemStatement(self: *Self, resource: Resource, top_level_menu_id_token: Token, nesting_level: u32) Error!?*Node { const menuitem_token = try self.lookaheadToken(.normal); const menuitem = rc.MenuItem.map.get(menuitem_token.slice(self.lexer.buffer)) orelse return null; self.nextToken(.normal) catch unreachable; if (nesting_level > max_nested_menu_level) { try self.addErrorDetails(.{ .err = .nested_resource_level_exceeds_max, .token = top_level_menu_id_token, .extra = .{ .resource = resource }, }); return self.addErrorDetailsAndFail(.{ .err = .nested_resource_level_exceeds_max, .type = .note, .token = menuitem_token, .extra = .{ .resource = resource }, }); } switch (resource) { .menu => switch (menuitem) { .menuitem => { try self.nextToken(.normal); if (rc.MenuItem.isSeparator(self.state.token.slice(self.lexer.buffer))) { const separator_token = self.state.token; // There can be any number of trailing commas after SEPARATOR try self.skipAnyCommas(); const node = try self.state.arena.create(Node.MenuItemSeparator); node.* = .{ .menuitem = menuitem_token, .separator = separator_token, }; return &node.base; } else { const text = self.state.token; if (!text.isStringLiteral()) { return self.addErrorDetailsAndFail(ErrorDetails{ .err = .expected_something_else, .token = text, .extra = .{ .expected_types = .{ .string_literal = true, } }, }); } try self.skipAnyCommas(); const result = try self.parseExpression(.{ .allowed_types = .{ .number = true } }); _ = try self.parseOptionalToken(.comma); var options: std.ArrayListUnmanaged(Token) = .empty; while (true) { const option_token = try self.lookaheadToken(.normal); if (!rc.MenuItem.Option.map.has(option_token.slice(self.lexer.buffer))) { break; } self.nextToken(.normal) catch unreachable; try options.append(self.state.arena, option_token); try self.skipAnyCommas(); } const node = try self.state.arena.create(Node.MenuItem); node.* = .{ .menuitem = menuitem_token, .text = text, .result = result, .option_list = try options.toOwnedSlice(self.state.arena), }; return &node.base; } }, .popup => { try self.nextToken(.normal); const text = self.state.token; if (!text.isStringLiteral()) { return self.addErrorDetailsAndFail(ErrorDetails{ .err = .expected_something_else, .token = text, .extra = .{ .expected_types = .{ .string_literal = true, } }, }); } try self.skipAnyCommas(); var options: std.ArrayListUnmanaged(Token) = .empty; while (true) { const option_token = try self.lookaheadToken(.normal); if (!rc.MenuItem.Option.map.has(option_token.slice(self.lexer.buffer))) { break; } self.nextToken(.normal) catch unreachable; try options.append(self.state.arena, option_token); try self.skipAnyCommas(); } try self.nextToken(.normal); const begin_token = self.state.token; try self.check(.begin); var items: std.ArrayListUnmanaged(*Node) = .empty; while (try self.parseMenuItemStatement(resource, top_level_menu_id_token, nesting_level + 1)) |item_node| { try items.append(self.state.arena, item_node); } try self.nextToken(.normal); const end_token = self.state.token; try self.check(.end); if (items.items.len == 0) { return self.addErrorDetailsAndFail(.{ .err = .empty_menu_not_allowed, .token = menuitem_token, }); } const node = try self.state.arena.create(Node.Popup); node.* = .{ .popup = menuitem_token, .text = text, .option_list = try options.toOwnedSlice(self.state.arena), .begin_token = begin_token, .items = try items.toOwnedSlice(self.state.arena), .end_token = end_token, }; return &node.base; }, }, .menuex => { try self.nextToken(.normal); const text = self.state.token; if (!text.isStringLiteral()) { return self.addErrorDetailsAndFail(ErrorDetails{ .err = .expected_something_else, .token = text, .extra = .{ .expected_types = .{ .string_literal = true, } }, }); } var param_parser = OptionalParamParser{ .parser = self }; const id = try param_parser.parse(.{}); const item_type = try param_parser.parse(.{}); const state = try param_parser.parse(.{}); if (menuitem == .menuitem) { // trailing comma is allowed, skip it _ = try self.parseOptionalToken(.comma); const node = try self.state.arena.create(Node.MenuItemEx); node.* = .{ .menuitem = menuitem_token, .text = text, .id = id, .type = item_type, .state = state, }; return &node.base; } const help_id = try param_parser.parse(.{}); // trailing comma is allowed, skip it _ = try self.parseOptionalToken(.comma); try self.nextToken(.normal); const begin_token = self.state.token; try self.check(.begin); var items: std.ArrayListUnmanaged(*Node) = .empty; while (try self.parseMenuItemStatement(resource, top_level_menu_id_token, nesting_level + 1)) |item_node| { try items.append(self.state.arena, item_node); } try self.nextToken(.normal); const end_token = self.state.token; try self.check(.end); if (items.items.len == 0) { return self.addErrorDetailsAndFail(.{ .err = .empty_menu_not_allowed, .token = menuitem_token, }); } const node = try self.state.arena.create(Node.PopupEx); node.* = .{ .popup = menuitem_token, .text = text, .id = id, .type = item_type, .state = state, .help_id = help_id, .begin_token = begin_token, .items = try items.toOwnedSlice(self.state.arena), .end_token = end_token, }; return &node.base; }, else => unreachable, } @compileError("unreachable"); } pub const OptionalParamParser = struct { finished: bool = false, parser: *Self, pub const Options = struct { not_expression_allowed: bool = false, }; pub fn parse(self: *OptionalParamParser, options: OptionalParamParser.Options) Error!?*Node { if (self.finished) return null; if (!(try self.parser.parseOptionalToken(.comma))) { self.finished = true; return null; } // If the next lookahead token could be part of a number expression, // then parse it. Otherwise, treat it as an 'empty' expression and // continue parsing, since 'empty' values are allowed. if (try self.parser.lookaheadCouldBeNumberExpression(switch (options.not_expression_allowed) { true => .not_allowed, false => .not_disallowed, })) { const node = try self.parser.parseExpression(.{ .allowed_types = .{ .number = true }, .can_contain_not_expressions = options.not_expression_allowed, }); return node; } return null; } }; /// Expects the current token to be handled, and that the version statement will /// begin on the next token. /// After return, the current token will be the token immediately before the end of the /// version statement (or unchanged if the function returns null). fn parseVersionStatement(self: *Self) Error!?*Node { const type_token = try self.lookaheadToken(.normal); const statement_type = rc.VersionInfo.map.get(type_token.slice(self.lexer.buffer)) orelse return null; self.nextToken(.normal) catch unreachable; switch (statement_type) { .file_version, .product_version => { var parts_buffer: [4]*Node = undefined; var parts = std.ArrayListUnmanaged(*Node).initBuffer(&parts_buffer); while (true) { const value = try self.parseExpression(.{ .allowed_types = .{ .number = true } }); parts.addOneAssumeCapacity().* = value; if (parts.unusedCapacitySlice().len == 0 or !(try self.parseOptionalToken(.comma))) { break; } } const node = try self.state.arena.create(Node.VersionStatement); node.* = .{ .type = type_token, .parts = try self.state.arena.dupe(*Node, parts.items), }; return &node.base; }, else => { const value = try self.parseExpression(.{ .allowed_types = .{ .number = true } }); const node = try self.state.arena.create(Node.SimpleStatement); node.* = .{ .identifier = type_token, .value = value, }; return &node.base; }, } } /// Expects the current token to be handled, and that the version BLOCK/VALUE will /// begin on the next token. /// After return, the current token will be the token immediately before the end of the /// version BLOCK/VALUE (or unchanged if the function returns null). fn parseVersionBlockOrValue(self: *Self, top_level_version_id_token: Token, nesting_level: u32) Error!?*Node { const keyword_token = try self.lookaheadToken(.normal); const keyword = rc.VersionBlock.map.get(keyword_token.slice(self.lexer.buffer)) orelse return null; self.nextToken(.normal) catch unreachable; if (nesting_level > max_nested_version_level) { try self.addErrorDetails(.{ .err = .nested_resource_level_exceeds_max, .token = top_level_version_id_token, .extra = .{ .resource = .versioninfo }, }); return self.addErrorDetailsAndFail(.{ .err = .nested_resource_level_exceeds_max, .type = .note, .token = keyword_token, .extra = .{ .resource = .versioninfo }, }); } try self.nextToken(.normal); const key = self.state.token; if (!key.isStringLiteral()) { return self.addErrorDetailsAndFail(.{ .err = .expected_something_else, .token = key, .extra = .{ .expected_types = .{ .string_literal = true, } }, }); } // Need to keep track of this to detect a potential miscompilation when // the comma is omitted and the first value is a quoted string. const had_comma_before_first_value = try self.parseOptionalToken(.comma); try self.skipAnyCommas(); const values = try self.parseBlockValuesList(had_comma_before_first_value); switch (keyword) { .block => { try self.nextToken(.normal); const begin_token = self.state.token; try self.check(.begin); var children: std.ArrayListUnmanaged(*Node) = .empty; while (try self.parseVersionBlockOrValue(top_level_version_id_token, nesting_level + 1)) |value_node| { try children.append(self.state.arena, value_node); } try self.nextToken(.normal); const end_token = self.state.token; try self.check(.end); const node = try self.state.arena.create(Node.Block); node.* = .{ .identifier = keyword_token, .key = key, .values = values, .begin_token = begin_token, .children = try children.toOwnedSlice(self.state.arena), .end_token = end_token, }; return &node.base; }, .value => { const node = try self.state.arena.create(Node.BlockValue); node.* = .{ .identifier = keyword_token, .key = key, .values = values, }; return &node.base; }, } } fn parseBlockValuesList(self: *Self, had_comma_before_first_value: bool) Error![]*Node { var values: std.ArrayListUnmanaged(*Node) = .empty; var seen_number: bool = false; var first_string_value: ?*Node = null; while (true) { const lookahead_token = try self.lookaheadToken(.normal); switch (lookahead_token.id) { .operator, .number, .open_paren, .quoted_ascii_string, .quoted_wide_string, => {}, else => break, } const value = try self.parseExpression(.{}); if (value.isNumberExpression()) { seen_number = true; } else if (first_string_value == null) { std.debug.assert(value.isStringLiteral()); first_string_value = value; } const has_trailing_comma = try self.parseOptionalToken(.comma); try self.skipAnyCommas(); const value_value = try self.state.arena.create(Node.BlockValueValue); value_value.* = .{ .expression = value, .trailing_comma = has_trailing_comma, }; try values.append(self.state.arena, &value_value.base); } if (seen_number and first_string_value != null) { // The Win32 RC compiler does some strange stuff with the data size: // Strings are counted as UTF-16 code units including the null-terminator // Numbers are counted as their byte lengths // So, when both strings and numbers are within a single value, // it incorrectly sets the value's type as binary, but then gives the // data length as a mixture of bytes and UTF-16 code units. This means that // when the length is read, it will be treated as byte length and will // not read the full value. We don't reproduce this behavior, so we warn // of the miscompilation here. try self.addErrorDetails(.{ .err = .rc_would_miscompile_version_value_byte_count, .type = .warning, .token = first_string_value.?.getFirstToken(), .token_span_start = values.items[0].getFirstToken(), .token_span_end = values.items[values.items.len - 1].getLastToken(), }); try self.addErrorDetails(.{ .err = .rc_would_miscompile_version_value_byte_count, .type = .note, .token = first_string_value.?.getFirstToken(), .token_span_start = values.items[0].getFirstToken(), .token_span_end = values.items[values.items.len - 1].getLastToken(), .print_source_line = false, }); } if (!had_comma_before_first_value and values.items.len > 0 and values.items[0].cast(.block_value_value).?.expression.isStringLiteral()) { const token = values.items[0].cast(.block_value_value).?.expression.cast(.literal).?.token; try self.addErrorDetails(.{ .err = .rc_would_miscompile_version_value_padding, .type = .warning, .token = token, }); try self.addErrorDetails(.{ .err = .rc_would_miscompile_version_value_padding, .type = .note, .token = token, .print_source_line = false, }); } return values.toOwnedSlice(self.state.arena); } fn numberExpressionContainsAnyLSuffixes(expression_node: *Node, source: []const u8, code_page_lookup: *const CodePageLookup) bool { // TODO: This could probably be done without evaluating the whole expression return Compiler.evaluateNumberExpression(expression_node, source, code_page_lookup).is_long; } /// Expects the current token to be a literal token that contains the string LANGUAGE fn parseLanguageStatement(self: *Self) Error!*Node { const language_token = self.state.token; const primary_language = try self.parseExpression(.{ .allowed_types = .{ .number = true } }); try self.nextToken(.normal); try self.check(.comma); const sublanguage = try self.parseExpression(.{ .allowed_types = .{ .number = true } }); // The Win32 RC compiler errors if either parameter contains any number with an L // suffix. Instead of that, we want to warn and then let the values get truncated. // The warning is done here to allow the compiler logic to not have to deal with this. if (numberExpressionContainsAnyLSuffixes(primary_language, self.lexer.buffer, &self.state.input_code_page_lookup)) { try self.addErrorDetails(.{ .err = .rc_would_error_u16_with_l_suffix, .type = .warning, .token = primary_language.getFirstToken(), .token_span_end = primary_language.getLastToken(), .extra = .{ .statement_with_u16_param = .language }, }); try self.addErrorDetails(.{ .err = .rc_would_error_u16_with_l_suffix, .print_source_line = false, .type = .note, .token = primary_language.getFirstToken(), .token_span_end = primary_language.getLastToken(), .extra = .{ .statement_with_u16_param = .language }, }); } if (numberExpressionContainsAnyLSuffixes(sublanguage, self.lexer.buffer, &self.state.input_code_page_lookup)) { try self.addErrorDetails(.{ .err = .rc_would_error_u16_with_l_suffix, .type = .warning, .token = sublanguage.getFirstToken(), .token_span_end = sublanguage.getLastToken(), .extra = .{ .statement_with_u16_param = .language }, }); try self.addErrorDetails(.{ .err = .rc_would_error_u16_with_l_suffix, .print_source_line = false, .type = .note, .token = sublanguage.getFirstToken(), .token_span_end = sublanguage.getLastToken(), .extra = .{ .statement_with_u16_param = .language }, }); } const node = try self.state.arena.create(Node.LanguageStatement); node.* = .{ .language_token = language_token, .primary_language_id = primary_language, .sublanguage_id = sublanguage, }; return &node.base; } pub const ParseExpressionOptions = struct { is_known_to_be_number_expression: bool = false, can_contain_not_expressions: bool = false, nesting_context: NestingContext = .{}, allowed_types: AllowedTypes = .{ .literal = true, .number = true, .string = true }, expected_types_override: ?ErrorDetails.ExpectedTypes = null, pub const AllowedTypes = struct { literal: bool = false, number: bool = false, string: bool = false, }; pub const NestingContext = struct { first_token: ?Token = null, last_token: ?Token = null, level: u32 = 0, /// Returns a new NestingContext with values modified appropriately for an increased nesting level fn incremented(ctx: NestingContext, first_token: Token, most_recent_token: Token) NestingContext { return .{ .first_token = ctx.first_token orelse first_token, .last_token = most_recent_token, .level = ctx.level + 1, }; } }; pub fn toErrorDetails(options: ParseExpressionOptions, token: Token) ErrorDetails { // TODO: expected_types_override interaction with is_known_to_be_number_expression? const expected_types = options.expected_types_override orelse ErrorDetails.ExpectedTypes{ .number = options.allowed_types.number, .number_expression = options.allowed_types.number, .string_literal = options.allowed_types.string and !options.is_known_to_be_number_expression, .literal = options.allowed_types.literal and !options.is_known_to_be_number_expression, }; return ErrorDetails{ .err = .expected_something_else, .token = token, .extra = .{ .expected_types = expected_types }, }; } }; /// Returns true if the next lookahead token is a number or could be the start of a number expression. /// Only useful when looking for empty expressions in optional fields. fn lookaheadCouldBeNumberExpression(self: *Self, not_allowed: enum { not_allowed, not_disallowed }) Error!bool { var lookahead_token = try self.lookaheadToken(.normal); switch (lookahead_token.id) { .literal => if (not_allowed == .not_allowed) { return std.ascii.eqlIgnoreCase("NOT", lookahead_token.slice(self.lexer.buffer)); } else return false, .number => return true, .open_paren => return true, .operator => { // + can be a unary operator, see parseExpression's handling of unary + const operator_char = lookahead_token.slice(self.lexer.buffer)[0]; return operator_char == '+'; }, else => return false, } } fn parsePrimary(self: *Self, options: ParseExpressionOptions) Error!*Node { try self.nextToken(.normal); const first_token = self.state.token; var is_close_paren_expression = false; var is_unary_plus_expression = false; switch (self.state.token.id) { .quoted_ascii_string, .quoted_wide_string => { if (!options.allowed_types.string) return self.addErrorDetailsAndFail(options.toErrorDetails(self.state.token)); const node = try self.state.arena.create(Node.Literal); node.* = .{ .token = self.state.token }; return &node.base; }, .literal => { if (options.can_contain_not_expressions and std.ascii.eqlIgnoreCase("NOT", self.state.token.slice(self.lexer.buffer))) { const not_token = self.state.token; try self.nextToken(.normal); try self.check(.number); if (!options.allowed_types.number) return self.addErrorDetailsAndFail(options.toErrorDetails(self.state.token)); const node = try self.state.arena.create(Node.NotExpression); node.* = .{ .not_token = not_token, .number_token = self.state.token, }; return &node.base; } if (!options.allowed_types.literal) return self.addErrorDetailsAndFail(options.toErrorDetails(self.state.token)); const node = try self.state.arena.create(Node.Literal); node.* = .{ .token = self.state.token }; return &node.base; }, .number => { if (!options.allowed_types.number) return self.addErrorDetailsAndFail(options.toErrorDetails(self.state.token)); const node = try self.state.arena.create(Node.Literal); node.* = .{ .token = self.state.token }; return &node.base; }, .open_paren => { const open_paren_token = self.state.token; const expression = try self.parseExpression(.{ .is_known_to_be_number_expression = true, .can_contain_not_expressions = options.can_contain_not_expressions, .nesting_context = options.nesting_context.incremented(first_token, open_paren_token), .allowed_types = .{ .number = true }, }); try self.nextToken(.normal); // TODO: Add context to error about where the open paren is try self.check(.close_paren); if (!options.allowed_types.number) return self.addErrorDetailsAndFail(options.toErrorDetails(open_paren_token)); const node = try self.state.arena.create(Node.GroupedExpression); node.* = .{ .open_token = open_paren_token, .expression = expression, .close_token = self.state.token, }; return &node.base; }, .close_paren => { // Note: In the Win32 implementation, a single close paren // counts as a valid "expression", but only when its the first and // only token in the expression. Such an expression is then treated // as a 'skip this expression' instruction. For example: // 1 RCDATA { 1, ), ), ), 2 } // will be evaluated as if it were `1 RCDATA { 1, 2 }` and only // 0x0001 and 0x0002 will be written to the .res data. // // This behavior is not emulated because it almost certainly has // no valid use cases and only introduces edge cases that are // not worth the effort to track down and deal with. Instead, // we error but also add a note about the Win32 RC behavior if // this edge case is detected. if (!options.is_known_to_be_number_expression) { is_close_paren_expression = true; } }, .operator => { // In the Win32 implementation, something akin to a unary + // is allowed but it doesn't behave exactly like a unary +. // Instead of emulating the Win32 behavior, we instead error // and add a note about unary plus not being allowed. // // This is done because unary + only works in some places, // and there's no real use-case for it since it's so limited // in how it can be used (e.g. +1 is accepted but (+1) will error) // // Even understanding when unary plus is allowed is difficult, so // we don't do any fancy detection of when the Win32 RC compiler would // allow a unary + and instead just output the note in all cases. // // Some examples of allowed expressions by the Win32 compiler: // +1 // 0|+5 // +1+2 // +~-5 // +(1) // // Some examples of disallowed expressions by the Win32 compiler: // (+1) // ++5 // // TODO: Potentially re-evaluate and support the unary plus in a bug-for-bug // compatible way. const operator_char = self.state.token.slice(self.lexer.buffer)[0]; if (operator_char == '+') { is_unary_plus_expression = true; } }, else => {}, } try self.addErrorDetails(options.toErrorDetails(self.state.token)); if (is_close_paren_expression) { try self.addErrorDetails(ErrorDetails{ .err = .close_paren_expression, .type = .note, .token = self.state.token, .print_source_line = false, }); } if (is_unary_plus_expression) { try self.addErrorDetails(ErrorDetails{ .err = .unary_plus_expression, .type = .note, .token = self.state.token, .print_source_line = false, }); } return error.ParseError; } /// Expects the current token to have already been dealt with, and that the /// expression will start on the next token. /// After return, the current token will have been dealt with. fn parseExpression(self: *Self, options: ParseExpressionOptions) Error!*Node { if (options.nesting_context.level > max_nested_expression_level) { try self.addErrorDetails(.{ .err = .nested_expression_level_exceeds_max, .token = options.nesting_context.first_token.?, }); return self.addErrorDetailsAndFail(.{ .err = .nested_expression_level_exceeds_max, .type = .note, .token = options.nesting_context.last_token.?, }); } var expr: *Node = try self.parsePrimary(options); const first_token = expr.getFirstToken(); // Non-number expressions can't have operators, so we can just return if (!expr.isNumberExpression()) return expr; while (try self.parseOptionalTokenAdvanced(.operator, .normal_expect_operator)) { const operator = self.state.token; const rhs_node = try self.parsePrimary(.{ .is_known_to_be_number_expression = true, .can_contain_not_expressions = options.can_contain_not_expressions, .nesting_context = options.nesting_context.incremented(first_token, operator), .allowed_types = options.allowed_types, }); if (!rhs_node.isNumberExpression()) { return self.addErrorDetailsAndFail(ErrorDetails{ .err = .expected_something_else, .token = rhs_node.getFirstToken(), .token_span_end = rhs_node.getLastToken(), .extra = .{ .expected_types = .{ .number = true, .number_expression = true, } }, }); } const node = try self.state.arena.create(Node.BinaryExpression); node.* = .{ .left = expr, .operator = operator, .right = rhs_node, }; expr = &node.base; } return expr; } /// Skips any amount of commas (including zero) /// In other words, it will skip the regex `,*` /// Assumes the token(s) should be parsed with `.normal` as the method. fn skipAnyCommas(self: *Self) !void { while (try self.parseOptionalToken(.comma)) {} } /// Advances the current token only if the token's id matches the specified `id`. /// Assumes the token should be parsed with `.normal` as the method. /// Returns true if the token matched, false otherwise. fn parseOptionalToken(self: *Self, id: Token.Id) Error!bool { return self.parseOptionalTokenAdvanced(id, .normal); } /// Advances the current token only if the token's id matches the specified `id`. /// Returns true if the token matched, false otherwise. fn parseOptionalTokenAdvanced(self: *Self, id: Token.Id, comptime method: Lexer.LexMethod) Error!bool { const maybe_token = try self.lookaheadToken(method); if (maybe_token.id != id) return false; self.nextToken(method) catch unreachable; return true; } fn addErrorDetails(self: *Self, details: ErrorDetails) Allocator.Error!void { try self.state.diagnostics.append(details); } fn addErrorDetailsAndFail(self: *Self, details: ErrorDetails) Error { try self.addErrorDetails(details); return error.ParseError; } fn nextToken(self: *Self, comptime method: Lexer.LexMethod) Error!void { self.state.token = token: while (true) { const token = self.lexer.next(method) catch |err| switch (err) { error.CodePagePragmaInIncludedFile => { // The Win32 RC compiler silently ignores such `#pragma code_point` directives, // but we want to both ignore them *and* emit a warning try self.addErrorDetails(.{ .err = .code_page_pragma_in_included_file, .type = .warning, .token = self.lexer.error_context_token.?, }); continue; }, error.CodePagePragmaInvalidCodePage => { var details = self.lexer.getErrorDetails(err); if (!self.options.warn_instead_of_error_on_invalid_code_page) { return self.addErrorDetailsAndFail(details); } details.type = .warning; try self.addErrorDetails(details); continue; }, error.InvalidDigitCharacterInNumberLiteral => { const details = self.lexer.getErrorDetails(err); try self.addErrorDetails(details); return self.addErrorDetailsAndFail(.{ .err = details.err, .type = .note, .token = details.token, .print_source_line = false, }); }, else => return self.addErrorDetailsAndFail(self.lexer.getErrorDetails(err)), }; break :token token; }; // After every token, set the input code page for its line try self.state.input_code_page_lookup.setForToken(self.state.token, self.lexer.current_code_page); // But only set the output code page to the current code page if we are past the first code_page pragma in the file. // Otherwise, we want to fill the lookup using the default code page so that lookups still work for lines that // don't have an explicit output code page set. const output_code_page = if (self.lexer.seen_pragma_code_pages > 1) self.lexer.current_code_page else self.state.output_code_page_lookup.default_code_page; try self.state.output_code_page_lookup.setForToken(self.state.token, output_code_page); } fn lookaheadToken(self: *Self, comptime method: Lexer.LexMethod) Error!Token { self.state.lookahead_lexer = self.lexer.*; return token: while (true) { break :token self.state.lookahead_lexer.next(method) catch |err| switch (err) { // Ignore this error and get the next valid token, we'll deal with this // properly when getting the token for real error.CodePagePragmaInIncludedFile => continue, else => return self.addErrorDetailsAndFail(self.state.lookahead_lexer.getErrorDetails(err)), }; }; } fn tokenSlice(self: *Self) []const u8 { return self.state.token.slice(self.lexer.buffer); } /// Check that the current token is something that can be used as an ID fn checkId(self: *Self) !void { switch (self.state.token.id) { .literal => {}, else => { return self.addErrorDetailsAndFail(ErrorDetails{ .err = .expected_token, .token = self.state.token, .extra = .{ .expected = .literal }, }); }, } } fn check(self: *Self, expected_token_id: Token.Id) !void { if (self.state.token.id != expected_token_id) { return self.addErrorDetailsAndFail(ErrorDetails{ .err = .expected_token, .token = self.state.token, .extra = .{ .expected = expected_token_id }, }); } } fn checkResource(self: *Self) !Resource { switch (self.state.token.id) { .literal => return Resource.fromString(.{ .slice = self.state.token.slice(self.lexer.buffer), .code_page = self.lexer.current_code_page, }), else => { return self.addErrorDetailsAndFail(ErrorDetails{ .err = .expected_token, .token = self.state.token, .extra = .{ .expected = .literal }, }); }, } } };