| 1 | const std = @import("std"); |
| 2 | const Lexer = @import("lex.zig").Lexer; |
| 3 | const Token = @import("lex.zig").Token; |
| 4 | const Node = @import("ast.zig").Node; |
| 5 | const Tree = @import("ast.zig").Tree; |
| 6 | const CodePageLookup = @import("ast.zig").CodePageLookup; |
| 7 | const ResourceType = @import("rc.zig").ResourceType; |
| 8 | const Allocator = std.mem.Allocator; |
| 9 | const ErrorDetails = @import("errors.zig").ErrorDetails; |
| 10 | const ErrorDetailsWithoutCodePage = @import("errors.zig").ErrorDetailsWithoutCodePage; |
| 11 | const Diagnostics = @import("errors.zig").Diagnostics; |
| 12 | const SourceBytes = @import("literals.zig").SourceBytes; |
| 13 | const Compiler = @import("compile.zig").Compiler; |
| 14 | const rc = @import("rc.zig"); |
| 15 | const res = @import("res.zig"); |
| 16 | |
| 17 | // TODO: Make these configurable? |
| 18 | pub const max_nested_menu_level: u32 = 512; |
| 19 | pub const max_nested_version_level: u32 = 512; |
| 20 | pub const max_nested_expression_level: u32 = 200; |
| 21 | |
| 22 | pub const Parser = struct { |
| 23 | const Self = @This(); |
| 24 | |
| 25 | lexer: *Lexer, |
| 26 | /// values that need to be initialized per-parse |
| 27 | state: Parser.State = undefined, |
| 28 | options: Parser.Options, |
| 29 | |
| 30 | pub const Error = error{ParseError} || Allocator.Error; |
| 31 | |
| 32 | pub const Options = struct { |
| 33 | warn_instead_of_error_on_invalid_code_page: bool = false, |
| 34 | disjoint_code_page: bool = false, |
| 35 | }; |
| 36 | |
| 37 | pub fn init(lexer: *Lexer, options: Options) Parser { |
| 38 | return Parser{ |
| 39 | .lexer = lexer, |
| 40 | .options = options, |
| 41 | }; |
| 42 | } |
| 43 | |
| 44 | pub const State = struct { |
| 45 | token: Token, |
| 46 | lookahead_lexer: Lexer, |
| 47 | allocator: Allocator, |
| 48 | arena: Allocator, |
| 49 | diagnostics: *Diagnostics, |
| 50 | input_code_page_lookup: CodePageLookup, |
| 51 | output_code_page_lookup: CodePageLookup, |
| 52 | warned_about_disjoint_code_page: bool, |
| 53 | }; |
| 54 | |
| 55 | pub fn parse(self: *Self, allocator: Allocator, diagnostics: *Diagnostics) Error!*Tree { |
| 56 | var arena = std.heap.ArenaAllocator.init(allocator); |
| 57 | errdefer arena.deinit(); |
| 58 | |
| 59 | self.state = Parser.State{ |
| 60 | .token = undefined, |
| 61 | .lookahead_lexer = undefined, |
| 62 | .allocator = allocator, |
| 63 | .arena = arena.allocator(), |
| 64 | .diagnostics = diagnostics, |
| 65 | .input_code_page_lookup = CodePageLookup.init(arena.allocator(), self.lexer.default_code_page), |
| 66 | .output_code_page_lookup = CodePageLookup.init(arena.allocator(), self.lexer.default_code_page), |
| 67 | .warned_about_disjoint_code_page = false, |
| 68 | }; |
| 69 | |
| 70 | const parsed_root = try self.parseRoot(); |
| 71 | |
| 72 | const tree = try self.state.arena.create(Tree); |
| 73 | tree.* = .{ |
| 74 | .node = parsed_root, |
| 75 | .input_code_pages = self.state.input_code_page_lookup, |
| 76 | .output_code_pages = self.state.output_code_page_lookup, |
| 77 | .source = self.lexer.buffer, |
| 78 | .arena = arena.state, |
| 79 | .allocator = allocator, |
| 80 | }; |
| 81 | return tree; |
| 82 | } |
| 83 | |
| 84 | fn parseRoot(self: *Self) Error!*Node { |
| 85 | var statements: std.ArrayList(*Node) = .empty; |
| 86 | defer statements.deinit(self.state.allocator); |
| 87 | |
| 88 | try self.parseStatements(&statements); |
| 89 | try self.check(.eof); |
| 90 | |
| 91 | const node = try self.state.arena.create(Node.Root); |
| 92 | node.* = .{ |
| 93 | .body = try self.state.arena.dupe(*Node, statements.items), |
| 94 | }; |
| 95 | return &node.base; |
| 96 | } |
| 97 | |
| 98 | fn parseStatements(self: *Self, statements: *std.ArrayList(*Node)) Error!void { |
| 99 | while (true) { |
| 100 | try self.nextToken(.whitespace_delimiter_only); |
| 101 | if (self.state.token.id == .eof) break; |
| 102 | // The Win32 compiler will sometimes try to recover from errors |
| 103 | // and then restart parsing afterwards. We don't ever do this |
| 104 | // because it almost always leads to unhelpful error messages |
| 105 | // (usually it will end up with bogus things like 'file |
| 106 | // not found: {') |
| 107 | const statement = try self.parseStatement(); |
| 108 | try statements.append(self.state.allocator, statement); |
| 109 | } |
| 110 | } |
| 111 | |
| 112 | /// Expects the current token to be the token before possible common resource attributes. |
| 113 | /// After return, the current token will be the token immediately before the end of the |
| 114 | /// common resource attributes (if any). If there are no common resource attributes, the |
| 115 | /// current token is unchanged. |
| 116 | /// The returned slice is allocated by the parser's arena |
| 117 | fn parseCommonResourceAttributes(self: *Self) ![]Token { |
| 118 | var common_resource_attributes: std.ArrayList(Token) = .empty; |
| 119 | while (true) { |
| 120 | const maybe_common_resource_attribute = try self.lookaheadToken(.normal); |
| 121 | if (maybe_common_resource_attribute.id == .literal and rc.CommonResourceAttributes.map.has(maybe_common_resource_attribute.slice(self.lexer.buffer))) { |
| 122 | try common_resource_attributes.append(self.state.arena, maybe_common_resource_attribute); |
| 123 | try self.nextToken(.normal); |
| 124 | } else { |
| 125 | break; |
| 126 | } |
| 127 | } |
| 128 | return common_resource_attributes.toOwnedSlice(self.state.arena); |
| 129 | } |
| 130 | |
| 131 | /// Expects the current token to have already been dealt with, and that the |
| 132 | /// optional statements will potentially start on the next token. |
| 133 | /// After return, the current token will be the token immediately before the end of the |
| 134 | /// optional statements (if any). If there are no optional statements, the |
| 135 | /// current token is unchanged. |
| 136 | /// The returned slice is allocated by the parser's arena |
| 137 | fn parseOptionalStatements(self: *Self, resource: ResourceType) ![]*Node { |
| 138 | var optional_statements: std.ArrayList(*Node) = .empty; |
| 139 | |
| 140 | const num_statement_types = @typeInfo(rc.OptionalStatements).@"enum".field_names.len; |
| 141 | var statement_type_has_duplicates: [num_statement_types]bool = @splat(false); |
| 142 | var last_statement_per_type: [num_statement_types]?*Node = @splat(null); |
| 143 | |
| 144 | while (true) { |
| 145 | const lookahead_token = try self.lookaheadToken(.normal); |
| 146 | if (lookahead_token.id != .literal) break; |
| 147 | const slice = lookahead_token.slice(self.lexer.buffer); |
| 148 | const optional_statement_type = rc.OptionalStatements.map.get(slice) orelse switch (resource) { |
| 149 | .dialog, .dialogex => rc.OptionalStatements.dialog_map.get(slice) orelse break, |
| 150 | else => break, |
| 151 | }; |
| 152 | try self.nextToken(.normal); |
| 153 | |
| 154 | const type_i = @backingInt(optional_statement_type); |
| 155 | if (last_statement_per_type[type_i] != null) { |
| 156 | statement_type_has_duplicates[type_i] = true; |
| 157 | } |
| 158 | |
| 159 | switch (optional_statement_type) { |
| 160 | .language => { |
| 161 | const language = try self.parseLanguageStatement(); |
| 162 | try optional_statements.append(self.state.arena, language); |
| 163 | }, |
| 164 | // Number only |
| 165 | .version, .characteristics, .style, .exstyle => { |
| 166 | const identifier = self.state.token; |
| 167 | const value = try self.parseExpression(.{ |
| 168 | .can_contain_not_expressions = optional_statement_type == .style or optional_statement_type == .exstyle, |
| 169 | .allowed_types = .{ .number = true }, |
| 170 | }); |
| 171 | const node = try self.state.arena.create(Node.SimpleStatement); |
| 172 | node.* = .{ |
| 173 | .identifier = identifier, |
| 174 | .value = value, |
| 175 | }; |
| 176 | try optional_statements.append(self.state.arena, &node.base); |
| 177 | }, |
| 178 | // String only |
| 179 | .caption => { |
| 180 | const identifier = self.state.token; |
| 181 | try self.nextToken(.normal); |
| 182 | const value = self.state.token; |
| 183 | if (!value.isStringLiteral()) { |
| 184 | return self.addErrorDetailsAndFail(.{ |
| 185 | .err = .expected_something_else, |
| 186 | .token = value, |
| 187 | .extra = .{ .expected_types = .{ |
| 188 | .string_literal = true, |
| 189 | } }, |
| 190 | }); |
| 191 | } |
| 192 | const value_node = try self.state.arena.create(Node.Literal); |
| 193 | value_node.* = .{ |
| 194 | .token = value, |
| 195 | }; |
| 196 | const node = try self.state.arena.create(Node.SimpleStatement); |
| 197 | node.* = .{ |
| 198 | .identifier = identifier, |
| 199 | .value = &value_node.base, |
| 200 | }; |
| 201 | try optional_statements.append(self.state.arena, &node.base); |
| 202 | }, |
| 203 | // String or number |
| 204 | .class => { |
| 205 | const identifier = self.state.token; |
| 206 | const value = try self.parseExpression(.{ .allowed_types = .{ .number = true, .string = true } }); |
| 207 | const node = try self.state.arena.create(Node.SimpleStatement); |
| 208 | node.* = .{ |
| 209 | .identifier = identifier, |
| 210 | .value = value, |
| 211 | }; |
| 212 | try optional_statements.append(self.state.arena, &node.base); |
| 213 | }, |
| 214 | // Special case |
| 215 | .menu => { |
| 216 | const identifier = self.state.token; |
| 217 | try self.nextToken(.whitespace_delimiter_only); |
| 218 | try self.check(.literal); |
| 219 | const value_node = try self.state.arena.create(Node.Literal); |
| 220 | value_node.* = .{ |
| 221 | .token = self.state.token, |
| 222 | }; |
| 223 | const node = try self.state.arena.create(Node.SimpleStatement); |
| 224 | node.* = .{ |
| 225 | .identifier = identifier, |
| 226 | .value = &value_node.base, |
| 227 | }; |
| 228 | try optional_statements.append(self.state.arena, &node.base); |
| 229 | }, |
| 230 | .font => { |
| 231 | const identifier = self.state.token; |
| 232 | const point_size = try self.parseExpression(.{ .allowed_types = .{ .number = true } }); |
| 233 | |
| 234 | // The comma between point_size and typeface is both optional and |
| 235 | // there can be any number of them |
| 236 | try self.skipAnyCommas(); |
| 237 | |
| 238 | try self.nextToken(.normal); |
| 239 | const typeface = self.state.token; |
| 240 | if (!typeface.isStringLiteral()) { |
| 241 | return self.addErrorDetailsAndFail(.{ |
| 242 | .err = .expected_something_else, |
| 243 | .token = typeface, |
| 244 | .extra = .{ .expected_types = .{ |
| 245 | .string_literal = true, |
| 246 | } }, |
| 247 | }); |
| 248 | } |
| 249 | |
| 250 | const ExSpecificValues = struct { |
| 251 | weight: ?*Node = null, |
| 252 | italic: ?*Node = null, |
| 253 | char_set: ?*Node = null, |
| 254 | }; |
| 255 | var ex_specific = ExSpecificValues{}; |
| 256 | ex_specific: { |
| 257 | var optional_param_parser = OptionalParamParser{ .parser = self }; |
| 258 | switch (resource) { |
| 259 | .dialogex => { |
| 260 | { |
| 261 | ex_specific.weight = try optional_param_parser.parse(.{}); |
| 262 | if (optional_param_parser.finished) break :ex_specific; |
| 263 | } |
| 264 | { |
| 265 | if (!(try self.parseOptionalToken(.comma))) break :ex_specific; |
| 266 | ex_specific.italic = try self.parseExpression(.{ .allowed_types = .{ .number = true } }); |
| 267 | } |
| 268 | { |
| 269 | ex_specific.char_set = try optional_param_parser.parse(.{}); |
| 270 | if (optional_param_parser.finished) break :ex_specific; |
| 271 | } |
| 272 | }, |
| 273 | .dialog => {}, |
| 274 | else => unreachable, // only DIALOG and DIALOGEX have FONT optional-statements |
| 275 | } |
| 276 | } |
| 277 | |
| 278 | const node = try self.state.arena.create(Node.FontStatement); |
| 279 | node.* = .{ |
| 280 | .identifier = identifier, |
| 281 | .point_size = point_size, |
| 282 | .typeface = typeface, |
| 283 | .weight = ex_specific.weight, |
| 284 | .italic = ex_specific.italic, |
| 285 | .char_set = ex_specific.char_set, |
| 286 | }; |
| 287 | try optional_statements.append(self.state.arena, &node.base); |
| 288 | }, |
| 289 | } |
| 290 | |
| 291 | last_statement_per_type[type_i] = optional_statements.items[optional_statements.items.len - 1]; |
| 292 | } |
| 293 | |
| 294 | for (optional_statements.items) |optional_statement| { |
| 295 | const type_i = type_i: { |
| 296 | switch (optional_statement.id) { |
| 297 | .simple_statement => { |
| 298 | const simple_statement: *Node.SimpleStatement = @alignCast(@fieldParentPtr("base", optional_statement)); |
| 299 | const statement_identifier = simple_statement.identifier; |
| 300 | const slice = statement_identifier.slice(self.lexer.buffer); |
| 301 | const optional_statement_type = rc.OptionalStatements.map.get(slice) orelse |
| 302 | rc.OptionalStatements.dialog_map.get(slice).?; |
| 303 | break :type_i @backingInt(optional_statement_type); |
| 304 | }, |
| 305 | .font_statement => { |
| 306 | break :type_i @backingInt(rc.OptionalStatements.font); |
| 307 | }, |
| 308 | .language_statement => { |
| 309 | break :type_i @backingInt(rc.OptionalStatements.language); |
| 310 | }, |
| 311 | else => unreachable, |
| 312 | } |
| 313 | }; |
| 314 | if (!statement_type_has_duplicates[type_i]) continue; |
| 315 | if (optional_statement == last_statement_per_type[type_i].?) continue; |
| 316 | |
| 317 | try self.addErrorDetails(.{ |
| 318 | .err = .duplicate_optional_statement_skipped, |
| 319 | .type = .warning, |
| 320 | .token = optional_statement.getFirstToken(), |
| 321 | .token_span_start = optional_statement.getFirstToken(), |
| 322 | .token_span_end = optional_statement.getLastToken(), |
| 323 | }); |
| 324 | } |
| 325 | |
| 326 | return optional_statements.toOwnedSlice(self.state.arena); |
| 327 | } |
| 328 | |
| 329 | /// Expects the current token to be the first token of the statement. |
| 330 | fn parseStatement(self: *Self) Error!*Node { |
| 331 | const first_token = self.state.token; |
| 332 | std.debug.assert(first_token.id == .literal); |
| 333 | |
| 334 | if (rc.TopLevelKeywords.map.get(first_token.slice(self.lexer.buffer))) |keyword| switch (keyword) { |
| 335 | .language => { |
| 336 | const language_statement = try self.parseLanguageStatement(); |
| 337 | return language_statement; |
| 338 | }, |
| 339 | .version, .characteristics => { |
| 340 | const identifier = self.state.token; |
| 341 | const value = try self.parseExpression(.{ .allowed_types = .{ .number = true } }); |
| 342 | const node = try self.state.arena.create(Node.SimpleStatement); |
| 343 | node.* = .{ |
| 344 | .identifier = identifier, |
| 345 | .value = value, |
| 346 | }; |
| 347 | return &node.base; |
| 348 | }, |
| 349 | .stringtable => { |
| 350 | // common resource attributes must all be contiguous and come before optional-statements |
| 351 | const common_resource_attributes = try self.parseCommonResourceAttributes(); |
| 352 | const optional_statements = try self.parseOptionalStatements(.stringtable); |
| 353 | |
| 354 | try self.nextToken(.normal); |
| 355 | const begin_token = self.state.token; |
| 356 | try self.check(.begin); |
| 357 | |
| 358 | var strings: std.ArrayList(*Node) = .empty; |
| 359 | defer strings.deinit(self.state.allocator); |
| 360 | while (true) { |
| 361 | const maybe_end_token = try self.lookaheadToken(.normal); |
| 362 | switch (maybe_end_token.id) { |
| 363 | .end => { |
| 364 | try self.nextToken(.normal); |
| 365 | break; |
| 366 | }, |
| 367 | .eof => { |
| 368 | return self.addErrorDetailsWithCodePageAndFail(.{ |
| 369 | .err = .unfinished_string_table_block, |
| 370 | .code_page = self.lexer.current_code_page, |
| 371 | .token = maybe_end_token, |
| 372 | }); |
| 373 | }, |
| 374 | else => {}, |
| 375 | } |
| 376 | const id_expression = try self.parseExpression(.{ .allowed_types = .{ .number = true } }); |
| 377 | |
| 378 | const comma_token: ?Token = if (try self.parseOptionalToken(.comma)) self.state.token else null; |
| 379 | |
| 380 | try self.nextToken(.normal); |
| 381 | if (self.state.token.id != .quoted_ascii_string and self.state.token.id != .quoted_wide_string) { |
| 382 | return self.addErrorDetailsAndFail(.{ |
| 383 | .err = .expected_something_else, |
| 384 | .token = self.state.token, |
| 385 | .extra = .{ .expected_types = .{ .string_literal = true } }, |
| 386 | }); |
| 387 | } |
| 388 | |
| 389 | const string_node = try self.state.arena.create(Node.StringTableString); |
| 390 | string_node.* = .{ |
| 391 | .id = id_expression, |
| 392 | .maybe_comma = comma_token, |
| 393 | .string = self.state.token, |
| 394 | }; |
| 395 | try strings.append(self.state.allocator, &string_node.base); |
| 396 | } |
| 397 | |
| 398 | if (strings.items.len == 0) { |
| 399 | return self.addErrorDetailsAndFail(.{ |
| 400 | .err = .expected_token, // TODO: probably a more specific error message |
| 401 | .token = self.state.token, |
| 402 | .extra = .{ .expected = .number }, |
| 403 | }); |
| 404 | } |
| 405 | |
| 406 | const end_token = self.state.token; |
| 407 | try self.check(.end); |
| 408 | |
| 409 | const node = try self.state.arena.create(Node.StringTable); |
| 410 | node.* = .{ |
| 411 | .type = first_token, |
| 412 | .common_resource_attributes = common_resource_attributes, |
| 413 | .optional_statements = optional_statements, |
| 414 | .begin_token = begin_token, |
| 415 | .strings = try self.state.arena.dupe(*Node, strings.items), |
| 416 | .end_token = end_token, |
| 417 | }; |
| 418 | return &node.base; |
| 419 | }, |
| 420 | }; |
| 421 | |
| 422 | // The Win32 RC compiler allows for a 'dangling' literal at the end of a file |
| 423 | // (as long as it's not a valid top-level keyword), and there is actually an |
| 424 | // .rc file with a such a dangling literal in the Windows-classic-samples set |
| 425 | // of projects. So, we have special compatibility for this particular case. |
| 426 | const maybe_eof = try self.lookaheadToken(.whitespace_delimiter_only); |
| 427 | if (maybe_eof.id == .eof) { |
| 428 | try self.addErrorDetails(.{ |
| 429 | .err = .dangling_literal_at_eof, |
| 430 | .type = .warning, |
| 431 | .token = first_token, |
| 432 | }); |
| 433 | |
| 434 | var context = try self.state.arena.alloc(Token, 2); |
| 435 | context[0] = first_token; |
| 436 | context[1] = maybe_eof; |
| 437 | const invalid_node = try self.state.arena.create(Node.Invalid); |
| 438 | invalid_node.* = .{ |
| 439 | .context = context, |
| 440 | }; |
| 441 | return &invalid_node.base; |
| 442 | } |
| 443 | |
| 444 | const id_token = first_token; |
| 445 | const id_code_page = self.lexer.current_code_page; |
| 446 | try self.nextToken(.whitespace_delimiter_only); |
| 447 | const resource = try self.checkResource(); |
| 448 | const type_token = self.state.token; |
| 449 | |
| 450 | if (resource == .string_num) { |
| 451 | try self.addErrorDetails(.{ |
| 452 | .err = .string_resource_as_numeric_type, |
| 453 | .token = type_token, |
| 454 | }); |
| 455 | return self.addErrorDetailsAndFail(.{ |
| 456 | .err = .string_resource_as_numeric_type, |
| 457 | .token = type_token, |
| 458 | .type = .note, |
| 459 | .print_source_line = false, |
| 460 | }); |
| 461 | } |
| 462 | |
| 463 | if (resource == .font) { |
| 464 | const id_bytes = SourceBytes{ |
| 465 | .slice = id_token.slice(self.lexer.buffer), |
| 466 | .code_page = id_code_page, |
| 467 | }; |
| 468 | const maybe_ordinal = res.NameOrOrdinal.maybeOrdinalFromString(id_bytes); |
| 469 | if (maybe_ordinal == null) { |
| 470 | const would_be_win32_rc_ordinal = res.NameOrOrdinal.maybeNonAsciiOrdinalFromString(id_bytes); |
| 471 | if (would_be_win32_rc_ordinal) |win32_rc_ordinal| { |
| 472 | try self.addErrorDetails(.{ |
| 473 | .err = .id_must_be_ordinal, |
| 474 | .token = id_token, |
| 475 | .extra = .{ .resource = resource }, |
| 476 | }); |
| 477 | return self.addErrorDetailsAndFail(.{ |
| 478 | .err = .win32_non_ascii_ordinal, |
| 479 | .token = id_token, |
| 480 | .type = .note, |
| 481 | .print_source_line = false, |
| 482 | .extra = .{ .number = win32_rc_ordinal.ordinal }, |
| 483 | }); |
| 484 | } else { |
| 485 | return self.addErrorDetailsAndFail(.{ |
| 486 | .err = .id_must_be_ordinal, |
| 487 | .token = id_token, |
| 488 | .extra = .{ .resource = resource }, |
| 489 | }); |
| 490 | } |
| 491 | } |
| 492 | } |
| 493 | |
| 494 | switch (resource) { |
| 495 | .accelerators => { |
| 496 | // common resource attributes must all be contiguous and come before optional-statements |
| 497 | const common_resource_attributes = try self.parseCommonResourceAttributes(); |
| 498 | const optional_statements = try self.parseOptionalStatements(resource); |
| 499 | |
| 500 | try self.nextToken(.normal); |
| 501 | const begin_token = self.state.token; |
| 502 | try self.check(.begin); |
| 503 | |
| 504 | var accelerators: std.ArrayList(*Node) = .empty; |
| 505 | |
| 506 | while (true) { |
| 507 | const lookahead = try self.lookaheadToken(.normal); |
| 508 | switch (lookahead.id) { |
| 509 | .end, .eof => { |
| 510 | try self.nextToken(.normal); |
| 511 | break; |
| 512 | }, |
| 513 | else => {}, |
| 514 | } |
| 515 | const event = try self.parseExpression(.{ .allowed_types = .{ .number = true, .string = true } }); |
| 516 | |
| 517 | try self.nextToken(.normal); |
| 518 | try self.check(.comma); |
| 519 | |
| 520 | const idvalue = try self.parseExpression(.{ .allowed_types = .{ .number = true } }); |
| 521 | |
| 522 | var type_and_options: std.ArrayList(Token) = .empty; |
| 523 | while (true) { |
| 524 | if (!(try self.parseOptionalToken(.comma))) break; |
| 525 | |
| 526 | try self.nextToken(.normal); |
| 527 | if (!rc.AcceleratorTypeAndOptions.map.has(self.tokenSlice())) { |
| 528 | return self.addErrorDetailsAndFail(.{ |
| 529 | .err = .expected_something_else, |
| 530 | .token = self.state.token, |
| 531 | .extra = .{ .expected_types = .{ |
| 532 | .accelerator_type_or_option = true, |
| 533 | } }, |
| 534 | }); |
| 535 | } |
| 536 | try type_and_options.append(self.state.arena, self.state.token); |
| 537 | } |
| 538 | |
| 539 | const node = try self.state.arena.create(Node.Accelerator); |
| 540 | node.* = .{ |
| 541 | .event = event, |
| 542 | .idvalue = idvalue, |
| 543 | .type_and_options = try type_and_options.toOwnedSlice(self.state.arena), |
| 544 | }; |
| 545 | try accelerators.append(self.state.arena, &node.base); |
| 546 | } |
| 547 | |
| 548 | const end_token = self.state.token; |
| 549 | try self.check(.end); |
| 550 | |
| 551 | const node = try self.state.arena.create(Node.Accelerators); |
| 552 | node.* = .{ |
| 553 | .id = id_token, |
| 554 | .type = type_token, |
| 555 | .common_resource_attributes = common_resource_attributes, |
| 556 | .optional_statements = optional_statements, |
| 557 | .begin_token = begin_token, |
| 558 | .accelerators = try accelerators.toOwnedSlice(self.state.arena), |
| 559 | .end_token = end_token, |
| 560 | }; |
| 561 | return &node.base; |
| 562 | }, |
| 563 | .dialog, .dialogex => { |
| 564 | // common resource attributes must all be contiguous and come before optional-statements |
| 565 | const common_resource_attributes = try self.parseCommonResourceAttributes(); |
| 566 | |
| 567 | const x = try self.parseExpression(.{ .allowed_types = .{ .number = true } }); |
| 568 | _ = try self.parseOptionalToken(.comma); |
| 569 | |
| 570 | const y = try self.parseExpression(.{ .allowed_types = .{ .number = true } }); |
| 571 | _ = try self.parseOptionalToken(.comma); |
| 572 | |
| 573 | const width = try self.parseExpression(.{ .allowed_types = .{ .number = true } }); |
| 574 | _ = try self.parseOptionalToken(.comma); |
| 575 | |
| 576 | const height = try self.parseExpression(.{ .allowed_types = .{ .number = true } }); |
| 577 | |
| 578 | var optional_param_parser = OptionalParamParser{ .parser = self }; |
| 579 | const help_id: ?*Node = try optional_param_parser.parse(.{}); |
| 580 | |
| 581 | const optional_statements = try self.parseOptionalStatements(resource); |
| 582 | |
| 583 | try self.nextToken(.normal); |
| 584 | const begin_token = self.state.token; |
| 585 | try self.check(.begin); |
| 586 | |
| 587 | var controls: std.ArrayList(*Node) = .empty; |
| 588 | defer controls.deinit(self.state.allocator); |
| 589 | while (try self.parseControlStatement(resource)) |control_node| { |
| 590 | // The number of controls must fit in a u16 in order for it to |
| 591 | // be able to be written into the relevant field in the .res data. |
| 592 | if (controls.items.len >= std.math.maxInt(u16)) { |
| 593 | try self.addErrorDetails(.{ |
| 594 | .err = .too_many_dialog_controls_or_toolbar_buttons, |
| 595 | .token = id_token, |
| 596 | .extra = .{ .resource = resource }, |
| 597 | }); |
| 598 | return self.addErrorDetailsAndFail(.{ |
| 599 | .err = .too_many_dialog_controls_or_toolbar_buttons, |
| 600 | .type = .note, |
| 601 | .token = control_node.getFirstToken(), |
| 602 | .token_span_end = control_node.getLastToken(), |
| 603 | .extra = .{ .resource = resource }, |
| 604 | }); |
| 605 | } |
| 606 | |
| 607 | try controls.append(self.state.allocator, control_node); |
| 608 | } |
| 609 | |
| 610 | try self.nextToken(.normal); |
| 611 | const end_token = self.state.token; |
| 612 | try self.check(.end); |
| 613 | |
| 614 | const node = try self.state.arena.create(Node.Dialog); |
| 615 | node.* = .{ |
| 616 | .id = id_token, |
| 617 | .type = type_token, |
| 618 | .common_resource_attributes = common_resource_attributes, |
| 619 | .x = x, |
| 620 | .y = y, |
| 621 | .width = width, |
| 622 | .height = height, |
| 623 | .help_id = help_id, |
| 624 | .optional_statements = optional_statements, |
| 625 | .begin_token = begin_token, |
| 626 | .controls = try self.state.arena.dupe(*Node, controls.items), |
| 627 | .end_token = end_token, |
| 628 | }; |
| 629 | return &node.base; |
| 630 | }, |
| 631 | .toolbar => { |
| 632 | // common resource attributes must all be contiguous and come before optional-statements |
| 633 | const common_resource_attributes = try self.parseCommonResourceAttributes(); |
| 634 | |
| 635 | const button_width = try self.parseExpression(.{ .allowed_types = .{ .number = true } }); |
| 636 | |
| 637 | try self.nextToken(.normal); |
| 638 | try self.check(.comma); |
| 639 | |
| 640 | const button_height = try self.parseExpression(.{ .allowed_types = .{ .number = true } }); |
| 641 | |
| 642 | try self.nextToken(.normal); |
| 643 | const begin_token = self.state.token; |
| 644 | try self.check(.begin); |
| 645 | |
| 646 | var buttons: std.ArrayList(*Node) = .empty; |
| 647 | defer buttons.deinit(self.state.allocator); |
| 648 | while (try self.parseToolbarButtonStatement()) |button_node| { |
| 649 | // The number of buttons must fit in a u16 in order for it to |
| 650 | // be able to be written into the relevant field in the .res data. |
| 651 | if (buttons.items.len >= std.math.maxInt(u16)) { |
| 652 | try self.addErrorDetails(.{ |
| 653 | .err = .too_many_dialog_controls_or_toolbar_buttons, |
| 654 | .token = id_token, |
| 655 | .extra = .{ .resource = resource }, |
| 656 | }); |
| 657 | return self.addErrorDetailsAndFail(.{ |
| 658 | .err = .too_many_dialog_controls_or_toolbar_buttons, |
| 659 | .type = .note, |
| 660 | .token = button_node.getFirstToken(), |
| 661 | .token_span_end = button_node.getLastToken(), |
| 662 | .extra = .{ .resource = resource }, |
| 663 | }); |
| 664 | } |
| 665 | |
| 666 | try buttons.append(self.state.allocator, button_node); |
| 667 | } |
| 668 | |
| 669 | try self.nextToken(.normal); |
| 670 | const end_token = self.state.token; |
| 671 | try self.check(.end); |
| 672 | |
| 673 | const node = try self.state.arena.create(Node.Toolbar); |
| 674 | node.* = .{ |
| 675 | .id = id_token, |
| 676 | .type = type_token, |
| 677 | .common_resource_attributes = common_resource_attributes, |
| 678 | .button_width = button_width, |
| 679 | .button_height = button_height, |
| 680 | .begin_token = begin_token, |
| 681 | .buttons = try self.state.arena.dupe(*Node, buttons.items), |
| 682 | .end_token = end_token, |
| 683 | }; |
| 684 | return &node.base; |
| 685 | }, |
| 686 | .menu, .menuex => { |
| 687 | // common resource attributes must all be contiguous and come before optional-statements |
| 688 | const common_resource_attributes = try self.parseCommonResourceAttributes(); |
| 689 | // help id is optional but must come between common resource attributes and optional-statements |
| 690 | var help_id: ?*Node = null; |
| 691 | // Note: No comma is allowed before or after help_id of MENUEX and help_id is not |
| 692 | // a possible field of MENU. |
| 693 | if (resource == .menuex and try self.lookaheadCouldBeNumberExpression(.not_disallowed)) { |
| 694 | help_id = try self.parseExpression(.{ |
| 695 | .is_known_to_be_number_expression = true, |
| 696 | }); |
| 697 | } |
| 698 | const optional_statements = try self.parseOptionalStatements(.stringtable); |
| 699 | |
| 700 | try self.nextToken(.normal); |
| 701 | const begin_token = self.state.token; |
| 702 | try self.check(.begin); |
| 703 | |
| 704 | var items: std.ArrayList(*Node) = .empty; |
| 705 | defer items.deinit(self.state.allocator); |
| 706 | while (try self.parseMenuItemStatement(resource, id_token, 1)) |item_node| { |
| 707 | try items.append(self.state.allocator, item_node); |
| 708 | } |
| 709 | |
| 710 | try self.nextToken(.normal); |
| 711 | const end_token = self.state.token; |
| 712 | try self.check(.end); |
| 713 | |
| 714 | if (items.items.len == 0) { |
| 715 | return self.addErrorDetailsAndFail(.{ |
| 716 | .err = .empty_menu_not_allowed, |
| 717 | .token = type_token, |
| 718 | }); |
| 719 | } |
| 720 | |
| 721 | const node = try self.state.arena.create(Node.Menu); |
| 722 | node.* = .{ |
| 723 | .id = id_token, |
| 724 | .type = type_token, |
| 725 | .common_resource_attributes = common_resource_attributes, |
| 726 | .optional_statements = optional_statements, |
| 727 | .help_id = help_id, |
| 728 | .begin_token = begin_token, |
| 729 | .items = try self.state.arena.dupe(*Node, items.items), |
| 730 | .end_token = end_token, |
| 731 | }; |
| 732 | return &node.base; |
| 733 | }, |
| 734 | .versioninfo => { |
| 735 | // common resource attributes must all be contiguous and come before optional-statements |
| 736 | const common_resource_attributes = try self.parseCommonResourceAttributes(); |
| 737 | |
| 738 | var fixed_info: std.ArrayList(*Node) = .empty; |
| 739 | while (try self.parseVersionStatement()) |version_statement| { |
| 740 | try fixed_info.append(self.state.arena, version_statement); |
| 741 | } |
| 742 | |
| 743 | try self.nextToken(.normal); |
| 744 | const begin_token = self.state.token; |
| 745 | try self.check(.begin); |
| 746 | |
| 747 | var block_statements: std.ArrayList(*Node) = .empty; |
| 748 | while (try self.parseVersionBlockOrValue(id_token, 1)) |block_node| { |
| 749 | try block_statements.append(self.state.arena, block_node); |
| 750 | } |
| 751 | |
| 752 | try self.nextToken(.normal); |
| 753 | const end_token = self.state.token; |
| 754 | try self.check(.end); |
| 755 | |
| 756 | const node = try self.state.arena.create(Node.VersionInfo); |
| 757 | node.* = .{ |
| 758 | .id = id_token, |
| 759 | .versioninfo = type_token, |
| 760 | .common_resource_attributes = common_resource_attributes, |
| 761 | .fixed_info = try fixed_info.toOwnedSlice(self.state.arena), |
| 762 | .begin_token = begin_token, |
| 763 | .block_statements = try block_statements.toOwnedSlice(self.state.arena), |
| 764 | .end_token = end_token, |
| 765 | }; |
| 766 | return &node.base; |
| 767 | }, |
| 768 | .dlginclude => { |
| 769 | const common_resource_attributes = try self.parseCommonResourceAttributes(); |
| 770 | |
| 771 | const filename_expression = try self.parseExpression(.{ |
| 772 | .allowed_types = .{ .string = true }, |
| 773 | }); |
| 774 | |
| 775 | const node = try self.state.arena.create(Node.ResourceExternal); |
| 776 | node.* = .{ |
| 777 | .id = id_token, |
| 778 | .type = type_token, |
| 779 | .common_resource_attributes = common_resource_attributes, |
| 780 | .filename = filename_expression, |
| 781 | }; |
| 782 | return &node.base; |
| 783 | }, |
| 784 | .stringtable => { |
| 785 | return self.addErrorDetailsAndFail(.{ |
| 786 | .err = .name_or_id_not_allowed, |
| 787 | .token = id_token, |
| 788 | .extra = .{ .resource = resource }, |
| 789 | }); |
| 790 | }, |
| 791 | // Just try everything as a 'generic' resource (raw data or external file) |
| 792 | // TODO: More fine-grained switch cases as necessary |
| 793 | else => { |
| 794 | const common_resource_attributes = try self.parseCommonResourceAttributes(); |
| 795 | |
| 796 | const maybe_begin = try self.lookaheadToken(.normal); |
| 797 | if (maybe_begin.id == .begin) { |
| 798 | try self.nextToken(.normal); |
| 799 | |
| 800 | if (!resource.canUseRawData()) { |
| 801 | try self.addErrorDetails(.{ |
| 802 | .err = .resource_type_cant_use_raw_data, |
| 803 | .token = self.state.token, |
| 804 | .extra = .{ .resource = resource }, |
| 805 | }); |
| 806 | return self.addErrorDetailsAndFail(.{ |
| 807 | .err = .resource_type_cant_use_raw_data, |
| 808 | .type = .note, |
| 809 | .print_source_line = false, |
| 810 | .token = self.state.token, |
| 811 | }); |
| 812 | } |
| 813 | |
| 814 | const raw_data = try self.parseRawDataBlock(); |
| 815 | const end_token = self.state.token; |
| 816 | |
| 817 | const node = try self.state.arena.create(Node.ResourceRawData); |
| 818 | node.* = .{ |
| 819 | .id = id_token, |
| 820 | .type = type_token, |
| 821 | .common_resource_attributes = common_resource_attributes, |
| 822 | .begin_token = maybe_begin, |
| 823 | .raw_data = raw_data, |
| 824 | .end_token = end_token, |
| 825 | }; |
| 826 | return &node.base; |
| 827 | } |
| 828 | |
| 829 | const filename_expression = try self.parseExpression(.{ |
| 830 | // Don't tell the user that numbers are accepted since we error on |
| 831 | // number expressions and regular number literals are treated as unquoted |
| 832 | // literals rather than numbers, so from the users perspective |
| 833 | // numbers aren't really allowed. |
| 834 | .expected_types_override = .{ |
| 835 | .literal = true, |
| 836 | .string_literal = true, |
| 837 | }, |
| 838 | }); |
| 839 | |
| 840 | const node = try self.state.arena.create(Node.ResourceExternal); |
| 841 | node.* = .{ |
| 842 | .id = id_token, |
| 843 | .type = type_token, |
| 844 | .common_resource_attributes = common_resource_attributes, |
| 845 | .filename = filename_expression, |
| 846 | }; |
| 847 | return &node.base; |
| 848 | }, |
| 849 | } |
| 850 | } |
| 851 | |
| 852 | /// Expects the current token to be a begin token. |
| 853 | /// After return, the current token will be the end token. |
| 854 | fn parseRawDataBlock(self: *Self) Error![]*Node { |
| 855 | var raw_data: std.ArrayList(*Node) = .empty; |
| 856 | defer raw_data.deinit(self.state.allocator); |
| 857 | while (true) { |
| 858 | const maybe_end_token = try self.lookaheadToken(.normal); |
| 859 | switch (maybe_end_token.id) { |
| 860 | .comma => { |
| 861 | try self.nextToken(.normal); |
| 862 | // comma as the first token in a raw data block is an error |
| 863 | if (raw_data.items.len == 0) { |
| 864 | return self.addErrorDetailsAndFail(.{ |
| 865 | .err = .expected_something_else, |
| 866 | .token = self.state.token, |
| 867 | .extra = .{ .expected_types = .{ |
| 868 | .number = true, |
| 869 | .number_expression = true, |
| 870 | .string_literal = true, |
| 871 | } }, |
| 872 | }); |
| 873 | } |
| 874 | // otherwise just skip over commas |
| 875 | continue; |
| 876 | }, |
| 877 | .end => { |
| 878 | try self.nextToken(.normal); |
| 879 | break; |
| 880 | }, |
| 881 | .eof => { |
| 882 | return self.addErrorDetailsWithCodePageAndFail(.{ |
| 883 | .err = .unfinished_raw_data_block, |
| 884 | .code_page = self.lexer.current_code_page, |
| 885 | .token = maybe_end_token, |
| 886 | }); |
| 887 | }, |
| 888 | else => {}, |
| 889 | } |
| 890 | const expression = try self.parseExpression(.{ .allowed_types = .{ .number = true, .string = true } }); |
| 891 | try raw_data.append(self.state.allocator, expression); |
| 892 | |
| 893 | if (expression.isNumberExpression()) { |
| 894 | const maybe_close_paren = try self.lookaheadToken(.normal); |
| 895 | if (maybe_close_paren.id == .close_paren) { |
| 896 | // advance to ensure that the code page lookup is populated for this token |
| 897 | try self.nextToken(.normal); |
| 898 | // <number expression>) is an error |
| 899 | return self.addErrorDetailsAndFail(.{ |
| 900 | .err = .expected_token, |
| 901 | .token = self.state.token, |
| 902 | .extra = .{ .expected = .operator }, |
| 903 | }); |
| 904 | } |
| 905 | } |
| 906 | } |
| 907 | return try self.state.arena.dupe(*Node, raw_data.items); |
| 908 | } |
| 909 | |
| 910 | /// Expects the current token to be handled, and that the control statement will |
| 911 | /// begin on the next token. |
| 912 | /// After return, the current token will be the token immediately before the end of the |
| 913 | /// control statement (or unchanged if the function returns null). |
| 914 | fn parseControlStatement(self: *Self, resource: ResourceType) Error!?*Node { |
| 915 | const control_token = try self.lookaheadToken(.normal); |
| 916 | const control = rc.Control.map.get(control_token.slice(self.lexer.buffer)) orelse return null; |
| 917 | try self.nextToken(.normal); |
| 918 | |
| 919 | try self.skipAnyCommas(); |
| 920 | |
| 921 | var text: ?Token = null; |
| 922 | if (control.hasTextParam()) { |
| 923 | try self.nextToken(.normal); |
| 924 | switch (self.state.token.id) { |
| 925 | .quoted_ascii_string, .quoted_wide_string, .number => { |
| 926 | text = self.state.token; |
| 927 | }, |
| 928 | else => { |
| 929 | return self.addErrorDetailsAndFail(.{ |
| 930 | .err = .expected_something_else, |
| 931 | .token = self.state.token, |
| 932 | .extra = .{ .expected_types = .{ |
| 933 | .number = true, |
| 934 | .string_literal = true, |
| 935 | } }, |
| 936 | }); |
| 937 | }, |
| 938 | } |
| 939 | try self.skipAnyCommas(); |
| 940 | } |
| 941 | |
| 942 | const id = try self.parseExpression(.{ .allowed_types = .{ .number = true } }); |
| 943 | |
| 944 | try self.skipAnyCommas(); |
| 945 | |
| 946 | var class: ?*Node = null; |
| 947 | var style: ?*Node = null; |
| 948 | if (control == .control) { |
| 949 | class = try self.parseExpression(.{}); |
| 950 | if (class.?.id == .literal) { |
| 951 | const class_literal: *Node.Literal = @alignCast(@fieldParentPtr("base", class.?)); |
| 952 | const is_invalid_control_class = class_literal.token.id == .literal and !rc.ControlClass.map.has(class_literal.token.slice(self.lexer.buffer)); |
| 953 | if (is_invalid_control_class) { |
| 954 | return self.addErrorDetailsAndFail(.{ |
| 955 | .err = .expected_something_else, |
| 956 | .token = self.state.token, |
| 957 | .extra = .{ .expected_types = .{ |
| 958 | .control_class = true, |
| 959 | } }, |
| 960 | }); |
| 961 | } |
| 962 | } |
| 963 | try self.skipAnyCommas(); |
| 964 | style = try self.parseExpression(.{ |
| 965 | .can_contain_not_expressions = true, |
| 966 | .allowed_types = .{ .number = true }, |
| 967 | }); |
| 968 | // If there is no comma after the style paramter, the Win32 RC compiler |
| 969 | // could misinterpret the statement and end up skipping over at least one token |
| 970 | // that should have been interepeted as the next parameter (x). For example: |
| 971 | // CONTROL "text", 1, BUTTON, 15 30, 1, 2, 3, 4 |
| 972 | // the `15` is the style parameter, but in the Win32 implementation the `30` |
| 973 | // is completely ignored (i.e. the `1, 2, 3, 4` are `x`, `y`, `w`, `h`). |
| 974 | // If a comma is added after the `15`, then `30` gets interpreted (correctly) |
| 975 | // as the `x` value. |
| 976 | // |
| 977 | // Instead of emulating this behavior, we just warn about the potential for |
| 978 | // weird behavior in the Win32 implementation whenever there isn't a comma after |
| 979 | // the style parameter. |
| 980 | const lookahead_token = try self.lookaheadToken(.normal); |
| 981 | if (lookahead_token.id != .comma and lookahead_token.id != .eof) { |
| 982 | try self.addErrorDetailsWithCodePage(.{ |
| 983 | .err = .rc_could_miscompile_control_params, |
| 984 | .type = .warning, |
| 985 | .code_page = self.lexer.current_code_page, |
| 986 | .token = lookahead_token, |
| 987 | }); |
| 988 | try self.addErrorDetailsWithCodePage(.{ |
| 989 | .err = .rc_could_miscompile_control_params, |
| 990 | .type = .note, |
| 991 | .code_page = self.lexer.current_code_page, |
| 992 | .token = style.?.getFirstToken(), |
| 993 | .token_span_end = style.?.getLastToken(), |
| 994 | }); |
| 995 | } |
| 996 | try self.skipAnyCommas(); |
| 997 | } |
| 998 | |
| 999 | const x = try self.parseExpression(.{ .allowed_types = .{ .number = true } }); |
| 1000 | _ = try self.parseOptionalToken(.comma); |
| 1001 | const y = try self.parseExpression(.{ .allowed_types = .{ .number = true } }); |
| 1002 | _ = try self.parseOptionalToken(.comma); |
| 1003 | const width = try self.parseExpression(.{ .allowed_types = .{ .number = true } }); |
| 1004 | _ = try self.parseOptionalToken(.comma); |
| 1005 | const height = try self.parseExpression(.{ .allowed_types = .{ .number = true } }); |
| 1006 | |
| 1007 | var optional_param_parser = OptionalParamParser{ .parser = self }; |
| 1008 | if (control != .control) { |
| 1009 | style = try optional_param_parser.parse(.{ .not_expression_allowed = true }); |
| 1010 | } |
| 1011 | |
| 1012 | const exstyle: ?*Node = try optional_param_parser.parse(.{ .not_expression_allowed = true }); |
| 1013 | const help_id: ?*Node = switch (resource) { |
| 1014 | .dialogex => try optional_param_parser.parse(.{}), |
| 1015 | else => null, |
| 1016 | }; |
| 1017 | |
| 1018 | var extra_data: []*Node = &[_]*Node{}; |
| 1019 | var extra_data_begin: ?Token = null; |
| 1020 | var extra_data_end: ?Token = null; |
| 1021 | // extra data is DIALOGEX-only |
| 1022 | if (resource == .dialogex and try self.parseOptionalToken(.begin)) { |
| 1023 | extra_data_begin = self.state.token; |
| 1024 | extra_data = try self.parseRawDataBlock(); |
| 1025 | extra_data_end = self.state.token; |
| 1026 | } |
| 1027 | |
| 1028 | const node = try self.state.arena.create(Node.ControlStatement); |
| 1029 | node.* = .{ |
| 1030 | .type = control_token, |
| 1031 | .text = text, |
| 1032 | .class = class, |
| 1033 | .id = id, |
| 1034 | .x = x, |
| 1035 | .y = y, |
| 1036 | .width = width, |
| 1037 | .height = height, |
| 1038 | .style = style, |
| 1039 | .exstyle = exstyle, |
| 1040 | .help_id = help_id, |
| 1041 | .extra_data_begin = extra_data_begin, |
| 1042 | .extra_data = extra_data, |
| 1043 | .extra_data_end = extra_data_end, |
| 1044 | }; |
| 1045 | return &node.base; |
| 1046 | } |
| 1047 | |
| 1048 | fn parseToolbarButtonStatement(self: *Self) Error!?*Node { |
| 1049 | const keyword_token = try self.lookaheadToken(.normal); |
| 1050 | const button_type = rc.ToolbarButton.map.get(keyword_token.slice(self.lexer.buffer)) orelse return null; |
| 1051 | try self.nextToken(.normal); |
| 1052 | |
| 1053 | switch (button_type) { |
| 1054 | .separator => { |
| 1055 | const node = try self.state.arena.create(Node.Literal); |
| 1056 | node.* = .{ |
| 1057 | .token = keyword_token, |
| 1058 | }; |
| 1059 | return &node.base; |
| 1060 | }, |
| 1061 | .button => { |
| 1062 | const button_id = try self.parseExpression(.{ .allowed_types = .{ .number = true } }); |
| 1063 | |
| 1064 | const node = try self.state.arena.create(Node.SimpleStatement); |
| 1065 | node.* = .{ |
| 1066 | .identifier = keyword_token, |
| 1067 | .value = button_id, |
| 1068 | }; |
| 1069 | return &node.base; |
| 1070 | }, |
| 1071 | } |
| 1072 | } |
| 1073 | |
| 1074 | /// Expects the current token to be handled, and that the menuitem/popup statement will |
| 1075 | /// begin on the next token. |
| 1076 | /// After return, the current token will be the token immediately before the end of the |
| 1077 | /// menuitem statement (or unchanged if the function returns null). |
| 1078 | fn parseMenuItemStatement(self: *Self, resource: ResourceType, top_level_menu_id_token: Token, nesting_level: u32) Error!?*Node { |
| 1079 | const menuitem_token = try self.lookaheadToken(.normal); |
| 1080 | const menuitem = rc.MenuItem.map.get(menuitem_token.slice(self.lexer.buffer)) orelse return null; |
| 1081 | try self.nextToken(.normal); |
| 1082 | |
| 1083 | if (nesting_level > max_nested_menu_level) { |
| 1084 | try self.addErrorDetails(.{ |
| 1085 | .err = .nested_resource_level_exceeds_max, |
| 1086 | .token = top_level_menu_id_token, |
| 1087 | .extra = .{ .resource = resource }, |
| 1088 | }); |
| 1089 | return self.addErrorDetailsAndFail(.{ |
| 1090 | .err = .nested_resource_level_exceeds_max, |
| 1091 | .type = .note, |
| 1092 | .token = menuitem_token, |
| 1093 | .extra = .{ .resource = resource }, |
| 1094 | }); |
| 1095 | } |
| 1096 | |
| 1097 | switch (resource) { |
| 1098 | .menu => switch (menuitem) { |
| 1099 | .menuitem => { |
| 1100 | try self.nextToken(.normal); |
| 1101 | if (rc.MenuItem.isSeparator(self.state.token.slice(self.lexer.buffer))) { |
| 1102 | const separator_token = self.state.token; |
| 1103 | // There can be any number of trailing commas after SEPARATOR |
| 1104 | try self.skipAnyCommas(); |
| 1105 | const node = try self.state.arena.create(Node.MenuItemSeparator); |
| 1106 | node.* = .{ |
| 1107 | .menuitem = menuitem_token, |
| 1108 | .separator = separator_token, |
| 1109 | }; |
| 1110 | return &node.base; |
| 1111 | } else { |
| 1112 | const text = self.state.token; |
| 1113 | if (!text.isStringLiteral()) { |
| 1114 | return self.addErrorDetailsAndFail(.{ |
| 1115 | .err = .expected_something_else, |
| 1116 | .token = text, |
| 1117 | .extra = .{ .expected_types = .{ |
| 1118 | .string_literal = true, |
| 1119 | } }, |
| 1120 | }); |
| 1121 | } |
| 1122 | try self.skipAnyCommas(); |
| 1123 | |
| 1124 | const result = try self.parseExpression(.{ .allowed_types = .{ .number = true } }); |
| 1125 | |
| 1126 | _ = try self.parseOptionalToken(.comma); |
| 1127 | |
| 1128 | var options: std.ArrayList(Token) = .empty; |
| 1129 | while (true) { |
| 1130 | const option_token = try self.lookaheadToken(.normal); |
| 1131 | if (!rc.MenuItem.Option.map.has(option_token.slice(self.lexer.buffer))) { |
| 1132 | break; |
| 1133 | } |
| 1134 | try self.nextToken(.normal); |
| 1135 | try options.append(self.state.arena, option_token); |
| 1136 | try self.skipAnyCommas(); |
| 1137 | } |
| 1138 | |
| 1139 | const node = try self.state.arena.create(Node.MenuItem); |
| 1140 | node.* = .{ |
| 1141 | .menuitem = menuitem_token, |
| 1142 | .text = text, |
| 1143 | .result = result, |
| 1144 | .option_list = try options.toOwnedSlice(self.state.arena), |
| 1145 | }; |
| 1146 | return &node.base; |
| 1147 | } |
| 1148 | }, |
| 1149 | .popup => { |
| 1150 | try self.nextToken(.normal); |
| 1151 | const text = self.state.token; |
| 1152 | if (!text.isStringLiteral()) { |
| 1153 | return self.addErrorDetailsAndFail(.{ |
| 1154 | .err = .expected_something_else, |
| 1155 | .token = text, |
| 1156 | .extra = .{ .expected_types = .{ |
| 1157 | .string_literal = true, |
| 1158 | } }, |
| 1159 | }); |
| 1160 | } |
| 1161 | try self.skipAnyCommas(); |
| 1162 | |
| 1163 | var options: std.ArrayList(Token) = .empty; |
| 1164 | while (true) { |
| 1165 | const option_token = try self.lookaheadToken(.normal); |
| 1166 | if (!rc.MenuItem.Option.map.has(option_token.slice(self.lexer.buffer))) { |
| 1167 | break; |
| 1168 | } |
| 1169 | try self.nextToken(.normal); |
| 1170 | try options.append(self.state.arena, option_token); |
| 1171 | try self.skipAnyCommas(); |
| 1172 | } |
| 1173 | |
| 1174 | try self.nextToken(.normal); |
| 1175 | const begin_token = self.state.token; |
| 1176 | try self.check(.begin); |
| 1177 | |
| 1178 | var items: std.ArrayList(*Node) = .empty; |
| 1179 | while (try self.parseMenuItemStatement(resource, top_level_menu_id_token, nesting_level + 1)) |item_node| { |
| 1180 | try items.append(self.state.arena, item_node); |
| 1181 | } |
| 1182 | |
| 1183 | try self.nextToken(.normal); |
| 1184 | const end_token = self.state.token; |
| 1185 | try self.check(.end); |
| 1186 | |
| 1187 | if (items.items.len == 0) { |
| 1188 | return self.addErrorDetailsAndFail(.{ |
| 1189 | .err = .empty_menu_not_allowed, |
| 1190 | .token = menuitem_token, |
| 1191 | }); |
| 1192 | } |
| 1193 | |
| 1194 | const node = try self.state.arena.create(Node.Popup); |
| 1195 | node.* = .{ |
| 1196 | .popup = menuitem_token, |
| 1197 | .text = text, |
| 1198 | .option_list = try options.toOwnedSlice(self.state.arena), |
| 1199 | .begin_token = begin_token, |
| 1200 | .items = try items.toOwnedSlice(self.state.arena), |
| 1201 | .end_token = end_token, |
| 1202 | }; |
| 1203 | return &node.base; |
| 1204 | }, |
| 1205 | }, |
| 1206 | .menuex => { |
| 1207 | try self.nextToken(.normal); |
| 1208 | const text = self.state.token; |
| 1209 | if (!text.isStringLiteral()) { |
| 1210 | return self.addErrorDetailsAndFail(.{ |
| 1211 | .err = .expected_something_else, |
| 1212 | .token = text, |
| 1213 | .extra = .{ .expected_types = .{ |
| 1214 | .string_literal = true, |
| 1215 | } }, |
| 1216 | }); |
| 1217 | } |
| 1218 | |
| 1219 | var param_parser = OptionalParamParser{ .parser = self }; |
| 1220 | const id = try param_parser.parse(.{}); |
| 1221 | const item_type = try param_parser.parse(.{}); |
| 1222 | const state = try param_parser.parse(.{}); |
| 1223 | |
| 1224 | if (menuitem == .menuitem) { |
| 1225 | // trailing comma is allowed, skip it |
| 1226 | _ = try self.parseOptionalToken(.comma); |
| 1227 | |
| 1228 | const node = try self.state.arena.create(Node.MenuItemEx); |
| 1229 | node.* = .{ |
| 1230 | .menuitem = menuitem_token, |
| 1231 | .text = text, |
| 1232 | .id = id, |
| 1233 | .type = item_type, |
| 1234 | .state = state, |
| 1235 | }; |
| 1236 | return &node.base; |
| 1237 | } |
| 1238 | |
| 1239 | const help_id = try param_parser.parse(.{}); |
| 1240 | |
| 1241 | // trailing comma is allowed, skip it |
| 1242 | _ = try self.parseOptionalToken(.comma); |
| 1243 | |
| 1244 | try self.nextToken(.normal); |
| 1245 | const begin_token = self.state.token; |
| 1246 | try self.check(.begin); |
| 1247 | |
| 1248 | var items: std.ArrayList(*Node) = .empty; |
| 1249 | while (try self.parseMenuItemStatement(resource, top_level_menu_id_token, nesting_level + 1)) |item_node| { |
| 1250 | try items.append(self.state.arena, item_node); |
| 1251 | } |
| 1252 | |
| 1253 | try self.nextToken(.normal); |
| 1254 | const end_token = self.state.token; |
| 1255 | try self.check(.end); |
| 1256 | |
| 1257 | if (items.items.len == 0) { |
| 1258 | return self.addErrorDetailsAndFail(.{ |
| 1259 | .err = .empty_menu_not_allowed, |
| 1260 | .token = menuitem_token, |
| 1261 | }); |
| 1262 | } |
| 1263 | |
| 1264 | const node = try self.state.arena.create(Node.PopupEx); |
| 1265 | node.* = .{ |
| 1266 | .popup = menuitem_token, |
| 1267 | .text = text, |
| 1268 | .id = id, |
| 1269 | .type = item_type, |
| 1270 | .state = state, |
| 1271 | .help_id = help_id, |
| 1272 | .begin_token = begin_token, |
| 1273 | .items = try items.toOwnedSlice(self.state.arena), |
| 1274 | .end_token = end_token, |
| 1275 | }; |
| 1276 | return &node.base; |
| 1277 | }, |
| 1278 | else => unreachable, |
| 1279 | } |
| 1280 | comptime unreachable; |
| 1281 | } |
| 1282 | |
| 1283 | pub const OptionalParamParser = struct { |
| 1284 | finished: bool = false, |
| 1285 | parser: *Self, |
| 1286 | |
| 1287 | pub const Options = struct { |
| 1288 | not_expression_allowed: bool = false, |
| 1289 | }; |
| 1290 | |
| 1291 | pub fn parse(self: *OptionalParamParser, options: OptionalParamParser.Options) Error!?*Node { |
| 1292 | if (self.finished) return null; |
| 1293 | if (!(try self.parser.parseOptionalToken(.comma))) { |
| 1294 | self.finished = true; |
| 1295 | return null; |
| 1296 | } |
| 1297 | // If the next lookahead token could be part of a number expression, |
| 1298 | // then parse it. Otherwise, treat it as an 'empty' expression and |
| 1299 | // continue parsing, since 'empty' values are allowed. |
| 1300 | if (try self.parser.lookaheadCouldBeNumberExpression(switch (options.not_expression_allowed) { |
| 1301 | true => .not_allowed, |
| 1302 | false => .not_disallowed, |
| 1303 | })) { |
| 1304 | const node = try self.parser.parseExpression(.{ |
| 1305 | .allowed_types = .{ .number = true }, |
| 1306 | .can_contain_not_expressions = options.not_expression_allowed, |
| 1307 | }); |
| 1308 | return node; |
| 1309 | } |
| 1310 | return null; |
| 1311 | } |
| 1312 | }; |
| 1313 | |
| 1314 | /// Expects the current token to be handled, and that the version statement will |
| 1315 | /// begin on the next token. |
| 1316 | /// After return, the current token will be the token immediately before the end of the |
| 1317 | /// version statement (or unchanged if the function returns null). |
| 1318 | fn parseVersionStatement(self: *Self) Error!?*Node { |
| 1319 | const type_token = try self.lookaheadToken(.normal); |
| 1320 | const statement_type = rc.VersionInfo.map.get(type_token.slice(self.lexer.buffer)) orelse return null; |
| 1321 | try self.nextToken(.normal); |
| 1322 | switch (statement_type) { |
| 1323 | .file_version, .product_version => { |
| 1324 | var parts_buffer: [4]*Node = undefined; |
| 1325 | var parts = std.ArrayList(*Node).initBuffer(&parts_buffer); |
| 1326 | |
| 1327 | while (true) { |
| 1328 | const value = try self.parseExpression(.{ .allowed_types = .{ .number = true } }); |
| 1329 | parts.addOneAssumeCapacity().* = value; |
| 1330 | |
| 1331 | if (parts.unusedCapacitySlice().len == 0 or |
| 1332 | !(try self.parseOptionalToken(.comma))) |
| 1333 | { |
| 1334 | break; |
| 1335 | } |
| 1336 | } |
| 1337 | |
| 1338 | const node = try self.state.arena.create(Node.VersionStatement); |
| 1339 | node.* = .{ |
| 1340 | .type = type_token, |
| 1341 | .parts = try self.state.arena.dupe(*Node, parts.items), |
| 1342 | }; |
| 1343 | return &node.base; |
| 1344 | }, |
| 1345 | else => { |
| 1346 | const value = try self.parseExpression(.{ .allowed_types = .{ .number = true } }); |
| 1347 | |
| 1348 | const node = try self.state.arena.create(Node.SimpleStatement); |
| 1349 | node.* = .{ |
| 1350 | .identifier = type_token, |
| 1351 | .value = value, |
| 1352 | }; |
| 1353 | return &node.base; |
| 1354 | }, |
| 1355 | } |
| 1356 | } |
| 1357 | |
| 1358 | /// Expects the current token to be handled, and that the version BLOCK/VALUE will |
| 1359 | /// begin on the next token. |
| 1360 | /// After return, the current token will be the token immediately before the end of the |
| 1361 | /// version BLOCK/VALUE (or unchanged if the function returns null). |
| 1362 | fn parseVersionBlockOrValue(self: *Self, top_level_version_id_token: Token, nesting_level: u32) Error!?*Node { |
| 1363 | const keyword_token = try self.lookaheadToken(.normal); |
| 1364 | const keyword = rc.VersionBlock.map.get(keyword_token.slice(self.lexer.buffer)) orelse return null; |
| 1365 | try self.nextToken(.normal); |
| 1366 | |
| 1367 | if (nesting_level > max_nested_version_level) { |
| 1368 | try self.addErrorDetails(.{ |
| 1369 | .err = .nested_resource_level_exceeds_max, |
| 1370 | .token = top_level_version_id_token, |
| 1371 | .extra = .{ .resource = .versioninfo }, |
| 1372 | }); |
| 1373 | return self.addErrorDetailsAndFail(.{ |
| 1374 | .err = .nested_resource_level_exceeds_max, |
| 1375 | .type = .note, |
| 1376 | .token = keyword_token, |
| 1377 | .extra = .{ .resource = .versioninfo }, |
| 1378 | }); |
| 1379 | } |
| 1380 | |
| 1381 | try self.nextToken(.normal); |
| 1382 | const key = self.state.token; |
| 1383 | if (!key.isStringLiteral()) { |
| 1384 | return self.addErrorDetailsAndFail(.{ |
| 1385 | .err = .expected_something_else, |
| 1386 | .token = key, |
| 1387 | .extra = .{ .expected_types = .{ |
| 1388 | .string_literal = true, |
| 1389 | } }, |
| 1390 | }); |
| 1391 | } |
| 1392 | // Need to keep track of this to detect a potential miscompilation when |
| 1393 | // the comma is omitted and the first value is a quoted string. |
| 1394 | const had_comma_before_first_value = try self.parseOptionalToken(.comma); |
| 1395 | try self.skipAnyCommas(); |
| 1396 | |
| 1397 | const values = try self.parseBlockValuesList(had_comma_before_first_value); |
| 1398 | |
| 1399 | switch (keyword) { |
| 1400 | .block => { |
| 1401 | try self.nextToken(.normal); |
| 1402 | const begin_token = self.state.token; |
| 1403 | try self.check(.begin); |
| 1404 | |
| 1405 | var children: std.ArrayList(*Node) = .empty; |
| 1406 | while (try self.parseVersionBlockOrValue(top_level_version_id_token, nesting_level + 1)) |value_node| { |
| 1407 | try children.append(self.state.arena, value_node); |
| 1408 | } |
| 1409 | |
| 1410 | try self.nextToken(.normal); |
| 1411 | const end_token = self.state.token; |
| 1412 | try self.check(.end); |
| 1413 | |
| 1414 | const node = try self.state.arena.create(Node.Block); |
| 1415 | node.* = .{ |
| 1416 | .identifier = keyword_token, |
| 1417 | .key = key, |
| 1418 | .values = values, |
| 1419 | .begin_token = begin_token, |
| 1420 | .children = try children.toOwnedSlice(self.state.arena), |
| 1421 | .end_token = end_token, |
| 1422 | }; |
| 1423 | return &node.base; |
| 1424 | }, |
| 1425 | .value => { |
| 1426 | const node = try self.state.arena.create(Node.BlockValue); |
| 1427 | node.* = .{ |
| 1428 | .identifier = keyword_token, |
| 1429 | .key = key, |
| 1430 | .values = values, |
| 1431 | }; |
| 1432 | return &node.base; |
| 1433 | }, |
| 1434 | } |
| 1435 | } |
| 1436 | |
| 1437 | fn parseBlockValuesList(self: *Self, had_comma_before_first_value: bool) Error![]*Node { |
| 1438 | var values: std.ArrayList(*Node) = .empty; |
| 1439 | var seen_number: bool = false; |
| 1440 | var first_string_value: ?*Node = null; |
| 1441 | while (true) { |
| 1442 | const lookahead_token = try self.lookaheadToken(.normal); |
| 1443 | switch (lookahead_token.id) { |
| 1444 | .operator, |
| 1445 | .number, |
| 1446 | .open_paren, |
| 1447 | .quoted_ascii_string, |
| 1448 | .quoted_wide_string, |
| 1449 | => {}, |
| 1450 | else => break, |
| 1451 | } |
| 1452 | const value = try self.parseExpression(.{}); |
| 1453 | |
| 1454 | if (value.isNumberExpression()) { |
| 1455 | seen_number = true; |
| 1456 | } else if (first_string_value == null) { |
| 1457 | std.debug.assert(value.isStringLiteral()); |
| 1458 | first_string_value = value; |
| 1459 | } |
| 1460 | |
| 1461 | const has_trailing_comma = try self.parseOptionalToken(.comma); |
| 1462 | try self.skipAnyCommas(); |
| 1463 | |
| 1464 | const value_value = try self.state.arena.create(Node.BlockValueValue); |
| 1465 | value_value.* = .{ |
| 1466 | .expression = value, |
| 1467 | .trailing_comma = has_trailing_comma, |
| 1468 | }; |
| 1469 | try values.append(self.state.arena, &value_value.base); |
| 1470 | } |
| 1471 | if (seen_number and first_string_value != null) { |
| 1472 | // The Win32 RC compiler does some strange stuff with the data size: |
| 1473 | // Strings are counted as UTF-16 code units including the null-terminator |
| 1474 | // Numbers are counted as their byte lengths |
| 1475 | // So, when both strings and numbers are within a single value, |
| 1476 | // it incorrectly sets the value's type as binary, but then gives the |
| 1477 | // data length as a mixture of bytes and UTF-16 code units. This means that |
| 1478 | // when the length is read, it will be treated as byte length and will |
| 1479 | // not read the full value. We don't reproduce this behavior, so we warn |
| 1480 | // of the miscompilation here. |
| 1481 | try self.addErrorDetails(.{ |
| 1482 | .err = .rc_would_miscompile_version_value_byte_count, |
| 1483 | .type = .warning, |
| 1484 | .token = first_string_value.?.getFirstToken(), |
| 1485 | .token_span_start = values.items[0].getFirstToken(), |
| 1486 | .token_span_end = values.items[values.items.len - 1].getLastToken(), |
| 1487 | }); |
| 1488 | try self.addErrorDetails(.{ |
| 1489 | .err = .rc_would_miscompile_version_value_byte_count, |
| 1490 | .type = .note, |
| 1491 | .token = first_string_value.?.getFirstToken(), |
| 1492 | .token_span_start = values.items[0].getFirstToken(), |
| 1493 | .token_span_end = values.items[values.items.len - 1].getLastToken(), |
| 1494 | .print_source_line = false, |
| 1495 | }); |
| 1496 | } |
| 1497 | if (!had_comma_before_first_value and values.items.len > 0 and values.items[0].cast(.block_value_value).?.expression.isStringLiteral()) { |
| 1498 | const token = values.items[0].cast(.block_value_value).?.expression.cast(.literal).?.token; |
| 1499 | try self.addErrorDetails(.{ |
| 1500 | .err = .rc_would_miscompile_version_value_padding, |
| 1501 | .type = .warning, |
| 1502 | .token = token, |
| 1503 | }); |
| 1504 | try self.addErrorDetails(.{ |
| 1505 | .err = .rc_would_miscompile_version_value_padding, |
| 1506 | .type = .note, |
| 1507 | .token = token, |
| 1508 | .print_source_line = false, |
| 1509 | }); |
| 1510 | } |
| 1511 | return values.toOwnedSlice(self.state.arena); |
| 1512 | } |
| 1513 | |
| 1514 | fn numberExpressionContainsAnyLSuffixes(expression_node: *Node, source: []const u8, code_page_lookup: *const CodePageLookup) bool { |
| 1515 | // TODO: This could probably be done without evaluating the whole expression |
| 1516 | return Compiler.evaluateNumberExpression(expression_node, source, code_page_lookup).is_long; |
| 1517 | } |
| 1518 | |
| 1519 | /// Expects the current token to be a literal token that contains the string LANGUAGE |
| 1520 | fn parseLanguageStatement(self: *Self) Error!*Node { |
| 1521 | const language_token = self.state.token; |
| 1522 | |
| 1523 | const primary_language = try self.parseExpression(.{ .allowed_types = .{ .number = true } }); |
| 1524 | |
| 1525 | try self.nextToken(.normal); |
| 1526 | try self.check(.comma); |
| 1527 | |
| 1528 | const sublanguage = try self.parseExpression(.{ .allowed_types = .{ .number = true } }); |
| 1529 | |
| 1530 | // The Win32 RC compiler errors if either parameter contains any number with an L |
| 1531 | // suffix. Instead of that, we want to warn and then let the values get truncated. |
| 1532 | // The warning is done here to allow the compiler logic to not have to deal with this. |
| 1533 | if (numberExpressionContainsAnyLSuffixes(primary_language, self.lexer.buffer, &self.state.input_code_page_lookup)) { |
| 1534 | try self.addErrorDetails(.{ |
| 1535 | .err = .rc_would_error_u16_with_l_suffix, |
| 1536 | .type = .warning, |
| 1537 | .token = primary_language.getFirstToken(), |
| 1538 | .token_span_end = primary_language.getLastToken(), |
| 1539 | .extra = .{ .statement_with_u16_param = .language }, |
| 1540 | }); |
| 1541 | try self.addErrorDetails(.{ |
| 1542 | .err = .rc_would_error_u16_with_l_suffix, |
| 1543 | .print_source_line = false, |
| 1544 | .type = .note, |
| 1545 | .token = primary_language.getFirstToken(), |
| 1546 | .token_span_end = primary_language.getLastToken(), |
| 1547 | .extra = .{ .statement_with_u16_param = .language }, |
| 1548 | }); |
| 1549 | } |
| 1550 | if (numberExpressionContainsAnyLSuffixes(sublanguage, self.lexer.buffer, &self.state.input_code_page_lookup)) { |
| 1551 | try self.addErrorDetails(.{ |
| 1552 | .err = .rc_would_error_u16_with_l_suffix, |
| 1553 | .type = .warning, |
| 1554 | .token = sublanguage.getFirstToken(), |
| 1555 | .token_span_end = sublanguage.getLastToken(), |
| 1556 | .extra = .{ .statement_with_u16_param = .language }, |
| 1557 | }); |
| 1558 | try self.addErrorDetails(.{ |
| 1559 | .err = .rc_would_error_u16_with_l_suffix, |
| 1560 | .print_source_line = false, |
| 1561 | .type = .note, |
| 1562 | .token = sublanguage.getFirstToken(), |
| 1563 | .token_span_end = sublanguage.getLastToken(), |
| 1564 | .extra = .{ .statement_with_u16_param = .language }, |
| 1565 | }); |
| 1566 | } |
| 1567 | |
| 1568 | const node = try self.state.arena.create(Node.LanguageStatement); |
| 1569 | node.* = .{ |
| 1570 | .language_token = language_token, |
| 1571 | .primary_language_id = primary_language, |
| 1572 | .sublanguage_id = sublanguage, |
| 1573 | }; |
| 1574 | return &node.base; |
| 1575 | } |
| 1576 | |
| 1577 | pub const ParseExpressionOptions = struct { |
| 1578 | is_known_to_be_number_expression: bool = false, |
| 1579 | can_contain_not_expressions: bool = false, |
| 1580 | nesting_context: NestingContext = .{}, |
| 1581 | allowed_types: AllowedTypes = .{ .literal = true, .number = true, .string = true }, |
| 1582 | expected_types_override: ?ErrorDetails.ExpectedTypes = null, |
| 1583 | |
| 1584 | pub const AllowedTypes = struct { |
| 1585 | literal: bool = false, |
| 1586 | number: bool = false, |
| 1587 | string: bool = false, |
| 1588 | }; |
| 1589 | |
| 1590 | pub const NestingContext = struct { |
| 1591 | first_token: ?Token = null, |
| 1592 | last_token: ?Token = null, |
| 1593 | level: u32 = 0, |
| 1594 | |
| 1595 | /// Returns a new NestingContext with values modified appropriately for an increased nesting level |
| 1596 | fn incremented(ctx: NestingContext, first_token: Token, most_recent_token: Token) NestingContext { |
| 1597 | return .{ |
| 1598 | .first_token = ctx.first_token orelse first_token, |
| 1599 | .last_token = most_recent_token, |
| 1600 | .level = ctx.level + 1, |
| 1601 | }; |
| 1602 | } |
| 1603 | }; |
| 1604 | |
| 1605 | pub fn toErrorDetails(options: ParseExpressionOptions, token: Token) ErrorDetailsWithoutCodePage { |
| 1606 | // TODO: expected_types_override interaction with is_known_to_be_number_expression? |
| 1607 | const expected_types = options.expected_types_override orelse ErrorDetails.ExpectedTypes{ |
| 1608 | .number = options.allowed_types.number, |
| 1609 | .number_expression = options.allowed_types.number, |
| 1610 | .string_literal = options.allowed_types.string and !options.is_known_to_be_number_expression, |
| 1611 | .literal = options.allowed_types.literal and !options.is_known_to_be_number_expression, |
| 1612 | }; |
| 1613 | return .{ |
| 1614 | .err = .expected_something_else, |
| 1615 | .token = token, |
| 1616 | .extra = .{ .expected_types = expected_types }, |
| 1617 | }; |
| 1618 | } |
| 1619 | }; |
| 1620 | |
| 1621 | /// Returns true if the next lookahead token is a number or could be the start of a number expression. |
| 1622 | /// Only useful when looking for empty expressions in optional fields. |
| 1623 | fn lookaheadCouldBeNumberExpression(self: *Self, not_allowed: enum { not_allowed, not_disallowed }) Error!bool { |
| 1624 | var lookahead_token = try self.lookaheadToken(.normal); |
| 1625 | switch (lookahead_token.id) { |
| 1626 | .literal => if (not_allowed == .not_allowed) { |
| 1627 | return std.ascii.eqlIgnoreCase("NOT", lookahead_token.slice(self.lexer.buffer)); |
| 1628 | } else return false, |
| 1629 | .number => return true, |
| 1630 | .open_paren => return true, |
| 1631 | .operator => { |
| 1632 | // + can be a unary operator, see parseExpression's handling of unary + |
| 1633 | const operator_char = lookahead_token.slice(self.lexer.buffer)[0]; |
| 1634 | return operator_char == '+'; |
| 1635 | }, |
| 1636 | else => return false, |
| 1637 | } |
| 1638 | } |
| 1639 | |
| 1640 | fn parsePrimary(self: *Self, options: ParseExpressionOptions) Error!*Node { |
| 1641 | try self.nextToken(.normal); |
| 1642 | const first_token = self.state.token; |
| 1643 | var is_close_paren_expression = false; |
| 1644 | var is_unary_plus_expression = false; |
| 1645 | switch (self.state.token.id) { |
| 1646 | .quoted_ascii_string, .quoted_wide_string => { |
| 1647 | if (!options.allowed_types.string) return self.addErrorDetailsAndFail(options.toErrorDetails(self.state.token)); |
| 1648 | const node = try self.state.arena.create(Node.Literal); |
| 1649 | node.* = .{ .token = self.state.token }; |
| 1650 | return &node.base; |
| 1651 | }, |
| 1652 | .literal => { |
| 1653 | if (options.can_contain_not_expressions and std.ascii.eqlIgnoreCase("NOT", self.state.token.slice(self.lexer.buffer))) { |
| 1654 | const not_token = self.state.token; |
| 1655 | try self.nextToken(.normal); |
| 1656 | try self.check(.number); |
| 1657 | if (!options.allowed_types.number) return self.addErrorDetailsAndFail(options.toErrorDetails(self.state.token)); |
| 1658 | const node = try self.state.arena.create(Node.NotExpression); |
| 1659 | node.* = .{ |
| 1660 | .not_token = not_token, |
| 1661 | .number_token = self.state.token, |
| 1662 | }; |
| 1663 | return &node.base; |
| 1664 | } |
| 1665 | if (!options.allowed_types.literal) return self.addErrorDetailsAndFail(options.toErrorDetails(self.state.token)); |
| 1666 | const node = try self.state.arena.create(Node.Literal); |
| 1667 | node.* = .{ .token = self.state.token }; |
| 1668 | return &node.base; |
| 1669 | }, |
| 1670 | .number => { |
| 1671 | if (!options.allowed_types.number) return self.addErrorDetailsAndFail(options.toErrorDetails(self.state.token)); |
| 1672 | const node = try self.state.arena.create(Node.Literal); |
| 1673 | node.* = .{ .token = self.state.token }; |
| 1674 | return &node.base; |
| 1675 | }, |
| 1676 | .open_paren => { |
| 1677 | const open_paren_token = self.state.token; |
| 1678 | |
| 1679 | const expression = try self.parseExpression(.{ |
| 1680 | .is_known_to_be_number_expression = true, |
| 1681 | .can_contain_not_expressions = options.can_contain_not_expressions, |
| 1682 | .nesting_context = options.nesting_context.incremented(first_token, open_paren_token), |
| 1683 | .allowed_types = .{ .number = true }, |
| 1684 | }); |
| 1685 | |
| 1686 | try self.nextToken(.normal); |
| 1687 | // TODO: Add context to error about where the open paren is |
| 1688 | try self.check(.close_paren); |
| 1689 | |
| 1690 | if (!options.allowed_types.number) return self.addErrorDetailsAndFail(options.toErrorDetails(open_paren_token)); |
| 1691 | const node = try self.state.arena.create(Node.GroupedExpression); |
| 1692 | node.* = .{ |
| 1693 | .open_token = open_paren_token, |
| 1694 | .expression = expression, |
| 1695 | .close_token = self.state.token, |
| 1696 | }; |
| 1697 | return &node.base; |
| 1698 | }, |
| 1699 | .close_paren => { |
| 1700 | // Note: In the Win32 implementation, a single close paren |
| 1701 | // counts as a valid "expression", but only when its the first and |
| 1702 | // only token in the expression. Such an expression is then treated |
| 1703 | // as a 'skip this expression' instruction. For example: |
| 1704 | // 1 RCDATA { 1, ), ), ), 2 } |
| 1705 | // will be evaluated as if it were `1 RCDATA { 1, 2 }` and only |
| 1706 | // 0x0001 and 0x0002 will be written to the .res data. |
| 1707 | // |
| 1708 | // This behavior is not emulated because it almost certainly has |
| 1709 | // no valid use cases and only introduces edge cases that are |
| 1710 | // not worth the effort to track down and deal with. Instead, |
| 1711 | // we error but also add a note about the Win32 RC behavior if |
| 1712 | // this edge case is detected. |
| 1713 | if (!options.is_known_to_be_number_expression) { |
| 1714 | is_close_paren_expression = true; |
| 1715 | } |
| 1716 | }, |
| 1717 | .operator => { |
| 1718 | // In the Win32 implementation, something akin to a unary + |
| 1719 | // is allowed but it doesn't behave exactly like a unary +. |
| 1720 | // Instead of emulating the Win32 behavior, we instead error |
| 1721 | // and add a note about unary plus not being allowed. |
| 1722 | // |
| 1723 | // This is done because unary + only works in some places, |
| 1724 | // and there's no real use-case for it since it's so limited |
| 1725 | // in how it can be used (e.g. +1 is accepted but (+1) will error) |
| 1726 | // |
| 1727 | // Even understanding when unary plus is allowed is difficult, so |
| 1728 | // we don't do any fancy detection of when the Win32 RC compiler would |
| 1729 | // allow a unary + and instead just output the note in all cases. |
| 1730 | // |
| 1731 | // Some examples of allowed expressions by the Win32 compiler: |
| 1732 | // +1 |
| 1733 | // 0|+5 |
| 1734 | // +1+2 |
| 1735 | // +~-5 |
| 1736 | // +(1) |
| 1737 | // |
| 1738 | // Some examples of disallowed expressions by the Win32 compiler: |
| 1739 | // (+1) |
| 1740 | // ++5 |
| 1741 | // |
| 1742 | // TODO: Potentially re-evaluate and support the unary plus in a bug-for-bug |
| 1743 | // compatible way. |
| 1744 | const operator_char = self.state.token.slice(self.lexer.buffer)[0]; |
| 1745 | if (operator_char == '+') { |
| 1746 | is_unary_plus_expression = true; |
| 1747 | } |
| 1748 | }, |
| 1749 | else => {}, |
| 1750 | } |
| 1751 | |
| 1752 | try self.addErrorDetails(options.toErrorDetails(self.state.token)); |
| 1753 | if (is_close_paren_expression) { |
| 1754 | try self.addErrorDetails(.{ |
| 1755 | .err = .close_paren_expression, |
| 1756 | .type = .note, |
| 1757 | .token = self.state.token, |
| 1758 | .print_source_line = false, |
| 1759 | }); |
| 1760 | } |
| 1761 | if (is_unary_plus_expression) { |
| 1762 | try self.addErrorDetails(.{ |
| 1763 | .err = .unary_plus_expression, |
| 1764 | .type = .note, |
| 1765 | .token = self.state.token, |
| 1766 | .print_source_line = false, |
| 1767 | }); |
| 1768 | } |
| 1769 | return error.ParseError; |
| 1770 | } |
| 1771 | |
| 1772 | /// Expects the current token to have already been dealt with, and that the |
| 1773 | /// expression will start on the next token. |
| 1774 | /// After return, the current token will have been dealt with. |
| 1775 | fn parseExpression(self: *Self, options: ParseExpressionOptions) Error!*Node { |
| 1776 | if (options.nesting_context.level > max_nested_expression_level) { |
| 1777 | try self.addErrorDetails(.{ |
| 1778 | .err = .nested_expression_level_exceeds_max, |
| 1779 | .token = options.nesting_context.first_token.?, |
| 1780 | }); |
| 1781 | return self.addErrorDetailsAndFail(.{ |
| 1782 | .err = .nested_expression_level_exceeds_max, |
| 1783 | .type = .note, |
| 1784 | .token = options.nesting_context.last_token.?, |
| 1785 | }); |
| 1786 | } |
| 1787 | var expr: *Node = try self.parsePrimary(options); |
| 1788 | const first_token = expr.getFirstToken(); |
| 1789 | |
| 1790 | // Non-number expressions can't have operators, so we can just return |
| 1791 | if (!expr.isNumberExpression()) return expr; |
| 1792 | |
| 1793 | while (try self.parseOptionalTokenAdvanced(.operator, .normal_expect_operator)) { |
| 1794 | const operator = self.state.token; |
| 1795 | const rhs_node = try self.parsePrimary(.{ |
| 1796 | .is_known_to_be_number_expression = true, |
| 1797 | .can_contain_not_expressions = options.can_contain_not_expressions, |
| 1798 | .nesting_context = options.nesting_context.incremented(first_token, operator), |
| 1799 | .allowed_types = options.allowed_types, |
| 1800 | }); |
| 1801 | |
| 1802 | if (!rhs_node.isNumberExpression()) { |
| 1803 | return self.addErrorDetailsAndFail(.{ |
| 1804 | .err = .expected_something_else, |
| 1805 | .token = rhs_node.getFirstToken(), |
| 1806 | .token_span_end = rhs_node.getLastToken(), |
| 1807 | .extra = .{ .expected_types = .{ |
| 1808 | .number = true, |
| 1809 | .number_expression = true, |
| 1810 | } }, |
| 1811 | }); |
| 1812 | } |
| 1813 | |
| 1814 | const node = try self.state.arena.create(Node.BinaryExpression); |
| 1815 | node.* = .{ |
| 1816 | .left = expr, |
| 1817 | .operator = operator, |
| 1818 | .right = rhs_node, |
| 1819 | }; |
| 1820 | expr = &node.base; |
| 1821 | } |
| 1822 | |
| 1823 | return expr; |
| 1824 | } |
| 1825 | |
| 1826 | /// Skips any amount of commas (including zero) |
| 1827 | /// In other words, it will skip the regex `,*` |
| 1828 | /// Assumes the token(s) should be parsed with `.normal` as the method. |
| 1829 | fn skipAnyCommas(self: *Self) !void { |
| 1830 | while (try self.parseOptionalToken(.comma)) {} |
| 1831 | } |
| 1832 | |
| 1833 | /// Advances the current token only if the token's id matches the specified `id`. |
| 1834 | /// Assumes the token should be parsed with `.normal` as the method. |
| 1835 | /// Returns true if the token matched, false otherwise. |
| 1836 | fn parseOptionalToken(self: *Self, id: Token.Id) Error!bool { |
| 1837 | return self.parseOptionalTokenAdvanced(id, .normal); |
| 1838 | } |
| 1839 | |
| 1840 | /// Advances the current token only if the token's id matches the specified `id`. |
| 1841 | /// Returns true if the token matched, false otherwise. |
| 1842 | fn parseOptionalTokenAdvanced(self: *Self, id: Token.Id, comptime method: Lexer.LexMethod) Error!bool { |
| 1843 | const maybe_token = try self.lookaheadToken(method); |
| 1844 | if (maybe_token.id != id) return false; |
| 1845 | try self.nextToken(method); |
| 1846 | return true; |
| 1847 | } |
| 1848 | |
| 1849 | fn addErrorDetailsWithCodePage(self: *Self, details: ErrorDetails) Allocator.Error!void { |
| 1850 | try self.state.diagnostics.append(details); |
| 1851 | } |
| 1852 | |
| 1853 | fn addErrorDetailsWithCodePageAndFail(self: *Self, details: ErrorDetails) Error { |
| 1854 | try self.addErrorDetailsWithCodePage(details); |
| 1855 | return error.ParseError; |
| 1856 | } |
| 1857 | |
| 1858 | /// Code page is looked up in input_code_page_lookup using the token, meaning the token |
| 1859 | /// must come from nextToken (i.e. it can't be a lookahead token). |
| 1860 | fn addErrorDetails(self: *Self, details_without_code_page: ErrorDetailsWithoutCodePage) Allocator.Error!void { |
| 1861 | const details = ErrorDetails{ |
| 1862 | .err = details_without_code_page.err, |
| 1863 | .code_page = self.state.input_code_page_lookup.getForToken(details_without_code_page.token), |
| 1864 | .token = details_without_code_page.token, |
| 1865 | .token_span_start = details_without_code_page.token_span_start, |
| 1866 | .token_span_end = details_without_code_page.token_span_end, |
| 1867 | .type = details_without_code_page.type, |
| 1868 | .print_source_line = details_without_code_page.print_source_line, |
| 1869 | .extra = details_without_code_page.extra, |
| 1870 | }; |
| 1871 | try self.addErrorDetailsWithCodePage(details); |
| 1872 | } |
| 1873 | |
| 1874 | /// Code page is looked up in input_code_page_lookup using the token, meaning the token |
| 1875 | /// must come from nextToken (i.e. it can't be a lookahead token). |
| 1876 | fn addErrorDetailsAndFail(self: *Self, details_without_code_page: ErrorDetailsWithoutCodePage) Error { |
| 1877 | try self.addErrorDetails(details_without_code_page); |
| 1878 | return error.ParseError; |
| 1879 | } |
| 1880 | |
| 1881 | fn nextToken(self: *Self, comptime method: Lexer.LexMethod) Error!void { |
| 1882 | self.state.token = token: while (true) { |
| 1883 | const token = self.lexer.next(method) catch |err| switch (err) { |
| 1884 | error.CodePagePragmaInIncludedFile => { |
| 1885 | // The Win32 RC compiler silently ignores such `#pragma code_page` directives, |
| 1886 | // but we want to both ignore them *and* emit a warning |
| 1887 | var details = self.lexer.getErrorDetails(err); |
| 1888 | details.type = .warning; |
| 1889 | try self.addErrorDetailsWithCodePage(details); |
| 1890 | continue; |
| 1891 | }, |
| 1892 | error.CodePagePragmaInvalidCodePage => { |
| 1893 | var details = self.lexer.getErrorDetails(err); |
| 1894 | if (!self.options.warn_instead_of_error_on_invalid_code_page) { |
| 1895 | return self.addErrorDetailsWithCodePageAndFail(details); |
| 1896 | } |
| 1897 | details.type = .warning; |
| 1898 | try self.addErrorDetailsWithCodePage(details); |
| 1899 | continue; |
| 1900 | }, |
| 1901 | error.InvalidDigitCharacterInNumberLiteral => { |
| 1902 | const details = self.lexer.getErrorDetails(err); |
| 1903 | try self.addErrorDetailsWithCodePage(details); |
| 1904 | return self.addErrorDetailsWithCodePageAndFail(.{ |
| 1905 | .err = details.err, |
| 1906 | .type = .note, |
| 1907 | .code_page = self.lexer.current_code_page, |
| 1908 | .token = details.token, |
| 1909 | .print_source_line = false, |
| 1910 | }); |
| 1911 | }, |
| 1912 | else => return self.addErrorDetailsWithCodePageAndFail(self.lexer.getErrorDetails(err)), |
| 1913 | }; |
| 1914 | break :token token; |
| 1915 | }; |
| 1916 | // After every token, set the input code page for its line |
| 1917 | try self.state.input_code_page_lookup.setForToken(self.state.token, self.lexer.current_code_page); |
| 1918 | // But only set the output code page to the current code page if we are past the first code_page pragma in the file. |
| 1919 | // Otherwise, we want to fill the lookup using the default code page so that lookups still work for lines that |
| 1920 | // don't have an explicit output code page set. |
| 1921 | const is_disjoint_code_page = self.options.disjoint_code_page and self.lexer.seen_pragma_code_pages == 1; |
| 1922 | const output_code_page = if (is_disjoint_code_page) |
| 1923 | self.state.output_code_page_lookup.default_code_page |
| 1924 | else |
| 1925 | self.lexer.current_code_page; |
| 1926 | |
| 1927 | if (is_disjoint_code_page and !self.state.warned_about_disjoint_code_page) { |
| 1928 | try self.addErrorDetailsWithCodePage(.{ |
| 1929 | .err = .disjoint_code_page, |
| 1930 | .type = .warning, |
| 1931 | .code_page = self.state.input_code_page_lookup.getForLineNum(self.lexer.last_pragma_code_page_token.?.line_number), |
| 1932 | .token = self.lexer.last_pragma_code_page_token.?, |
| 1933 | }); |
| 1934 | try self.addErrorDetailsWithCodePage(.{ |
| 1935 | .err = .disjoint_code_page, |
| 1936 | .type = .note, |
| 1937 | .code_page = self.state.input_code_page_lookup.getForLineNum(self.lexer.last_pragma_code_page_token.?.line_number), |
| 1938 | .token = self.lexer.last_pragma_code_page_token.?, |
| 1939 | .print_source_line = false, |
| 1940 | }); |
| 1941 | self.state.warned_about_disjoint_code_page = true; |
| 1942 | } |
| 1943 | |
| 1944 | try self.state.output_code_page_lookup.setForToken(self.state.token, output_code_page); |
| 1945 | } |
| 1946 | |
| 1947 | fn lookaheadToken(self: *Self, comptime method: Lexer.LexMethod) Error!Token { |
| 1948 | self.state.lookahead_lexer = self.lexer.*; |
| 1949 | return token: while (true) { |
| 1950 | break :token self.state.lookahead_lexer.next(method) catch |err| switch (err) { |
| 1951 | // Ignore this error and get the next valid token, we'll deal with this |
| 1952 | // properly when getting the token for real |
| 1953 | error.CodePagePragmaInIncludedFile => continue, |
| 1954 | else => return self.addErrorDetailsWithCodePageAndFail(self.state.lookahead_lexer.getErrorDetails(err)), |
| 1955 | }; |
| 1956 | }; |
| 1957 | } |
| 1958 | |
| 1959 | fn tokenSlice(self: *Self) []const u8 { |
| 1960 | return self.state.token.slice(self.lexer.buffer); |
| 1961 | } |
| 1962 | |
| 1963 | /// Check that the current token is something that can be used as an ID |
| 1964 | fn checkId(self: *Self) !void { |
| 1965 | switch (self.state.token.id) { |
| 1966 | .literal => {}, |
| 1967 | else => { |
| 1968 | return self.addErrorDetailsAndFail(.{ |
| 1969 | .err = .expected_token, |
| 1970 | .token = self.state.token, |
| 1971 | .extra = .{ .expected = .literal }, |
| 1972 | }); |
| 1973 | }, |
| 1974 | } |
| 1975 | } |
| 1976 | |
| 1977 | fn check(self: *Self, expected_token_id: Token.Id) !void { |
| 1978 | if (self.state.token.id != expected_token_id) { |
| 1979 | return self.addErrorDetailsAndFail(.{ |
| 1980 | .err = .expected_token, |
| 1981 | .token = self.state.token, |
| 1982 | .extra = .{ .expected = expected_token_id }, |
| 1983 | }); |
| 1984 | } |
| 1985 | } |
| 1986 | |
| 1987 | fn checkResource(self: *Self) !ResourceType { |
| 1988 | switch (self.state.token.id) { |
| 1989 | .literal => return ResourceType.fromString(.{ |
| 1990 | .slice = self.state.token.slice(self.lexer.buffer), |
| 1991 | .code_page = self.lexer.current_code_page, |
| 1992 | }), |
| 1993 | else => { |
| 1994 | return self.addErrorDetailsAndFail(.{ |
| 1995 | .err = .expected_token, |
| 1996 | .token = self.state.token, |
| 1997 | .extra = .{ .expected = .literal }, |
| 1998 | }); |
| 1999 | }, |
| 2000 | } |
| 2001 | } |
| 2002 | }; |