| 1 | //===- BitstreamRemarkParser.cpp ------------------------------------------===// |
| 2 | // |
| 3 | // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
| 4 | // See https://llvm.org/LICENSE.txt for license information. |
| 5 | // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
| 6 | // |
| 7 | //===----------------------------------------------------------------------===// |
| 8 | // |
| 9 | // This file provides utility methods used by clients that want to use the |
| 10 | // parser for remark diagnostics in LLVM. |
| 11 | // |
| 12 | //===----------------------------------------------------------------------===// |
| 13 | |
| 14 | #include "BitstreamRemarkParser.h" |
| 15 | #include "llvm/Support/MemoryBuffer.h" |
| 16 | #include "llvm/Support/Path.h" |
| 17 | #include <optional> |
| 18 | |
| 19 | using namespace llvm; |
| 20 | using namespace llvm::remarks; |
| 21 | |
| 22 | namespace { |
| 23 | |
| 24 | template <typename... Ts> Error error(char const *Fmt, const Ts &...Vals) { |
| 25 | std::string Buffer; |
| 26 | raw_string_ostream OS(Buffer); |
| 27 | OS << formatv(Fmt, Vals...); |
| 28 | return make_error<StringError>( |
| 29 | Args: std::move(Buffer), |
| 30 | Args: std::make_error_code(e: std::errc::illegal_byte_sequence)); |
| 31 | } |
| 32 | |
| 33 | } // namespace |
| 34 | |
| 35 | Error BitstreamBlockParserHelperBase::(unsigned AbbrevID) { |
| 36 | return error(Fmt: "Unknown record entry ({})." , Vals: AbbrevID); |
| 37 | } |
| 38 | |
| 39 | Error BitstreamBlockParserHelperBase::(StringRef RecordName) { |
| 40 | return error(Fmt: "Unexpected record entry ({})." , Vals: RecordName); |
| 41 | } |
| 42 | |
| 43 | Error BitstreamBlockParserHelperBase::(StringRef RecordName) { |
| 44 | return error(Fmt: "Malformed record entry ({})." , Vals: RecordName); |
| 45 | } |
| 46 | |
| 47 | Error BitstreamBlockParserHelperBase::(unsigned Code) { |
| 48 | return error(Fmt: "Unexpected subblock ({})." , Vals: Code); |
| 49 | } |
| 50 | |
| 51 | static Expected<unsigned> expectSubBlock(BitstreamCursor &Stream) { |
| 52 | Expected<BitstreamEntry> Next = Stream.advance(); |
| 53 | if (!Next) |
| 54 | return Next.takeError(); |
| 55 | switch (Next->Kind) { |
| 56 | case BitstreamEntry::SubBlock: |
| 57 | return Next->ID; |
| 58 | case BitstreamEntry::Record: |
| 59 | case BitstreamEntry::EndBlock: |
| 60 | return error(Fmt: "Expected subblock, but got unexpected record." ); |
| 61 | case BitstreamEntry::Error: |
| 62 | return error(Fmt: "Expected subblock, but got unexpected end of bitstream." ); |
| 63 | } |
| 64 | llvm_unreachable("Unexpected BitstreamEntry" ); |
| 65 | } |
| 66 | |
| 67 | Error BitstreamBlockParserHelperBase::() { |
| 68 | auto MaybeBlockID = expectSubBlock(Stream); |
| 69 | if (!MaybeBlockID) |
| 70 | return MaybeBlockID.takeError(); |
| 71 | if (*MaybeBlockID != BlockID) |
| 72 | return error(Fmt: "Expected {} block, but got unexpected block ({})." , Vals: BlockName, |
| 73 | Vals: *MaybeBlockID); |
| 74 | return Error::success(); |
| 75 | } |
| 76 | |
| 77 | Error BitstreamBlockParserHelperBase::() { |
| 78 | if (Stream.EnterSubBlock(BlockID)) |
| 79 | return error(Fmt: "Error while entering {} block." , Vals: BlockName); |
| 80 | return Error::success(); |
| 81 | } |
| 82 | |
| 83 | Error BitstreamMetaParserHelper::(unsigned Code) { |
| 84 | // Note: 2 is used here because it's the max number of fields we have per |
| 85 | // record. |
| 86 | SmallVector<uint64_t, 2> Record; |
| 87 | StringRef Blob; |
| 88 | Expected<unsigned> RecordID = Stream.readRecord(AbbrevID: Code, Vals&: Record, Blob: &Blob); |
| 89 | if (!RecordID) |
| 90 | return RecordID.takeError(); |
| 91 | |
| 92 | switch (*RecordID) { |
| 93 | case RECORD_META_CONTAINER_INFO: { |
| 94 | if (Record.size() != 2) |
| 95 | return malformedRecord(RecordName: MetaContainerInfoName); |
| 96 | Container = {.Version: Record[0], .Type: Record[1]}; |
| 97 | // Error immediately if container version is outdated, so the user sees an |
| 98 | // explanation instead of a parser error. |
| 99 | if (Container->Version != CurrentContainerVersion) { |
| 100 | return ::error( |
| 101 | Fmt: "Unsupported remark container version (expected: {}, read: {}). " |
| 102 | "Please upgrade/downgrade your toolchain to read this container." , |
| 103 | Vals: CurrentContainerVersion, Vals: Container->Version); |
| 104 | } |
| 105 | break; |
| 106 | } |
| 107 | case RECORD_META_REMARK_VERSION: { |
| 108 | if (Record.size() != 1) |
| 109 | return malformedRecord(RecordName: MetaRemarkVersionName); |
| 110 | RemarkVersion = Record[0]; |
| 111 | // Error immediately if remark version is outdated, so the user sees an |
| 112 | // explanation instead of a parser error. |
| 113 | if (*RemarkVersion != CurrentRemarkVersion) { |
| 114 | return ::error( |
| 115 | Fmt: "Unsupported remark version in container (expected: {}, read: {}). " |
| 116 | "Please upgrade/downgrade your toolchain to read this container." , |
| 117 | Vals: CurrentRemarkVersion, Vals: *RemarkVersion); |
| 118 | } |
| 119 | break; |
| 120 | } |
| 121 | case RECORD_META_STRTAB: { |
| 122 | if (Record.size() != 0) |
| 123 | return malformedRecord(RecordName: MetaStrTabName); |
| 124 | StrTabBuf = Blob; |
| 125 | break; |
| 126 | } |
| 127 | case RECORD_META_EXTERNAL_FILE: { |
| 128 | if (Record.size() != 0) |
| 129 | return malformedRecord(RecordName: MetaExternalFileName); |
| 130 | ExternalFilePath = Blob; |
| 131 | break; |
| 132 | } |
| 133 | default: |
| 134 | return unknownRecord(AbbrevID: *RecordID); |
| 135 | } |
| 136 | return Error::success(); |
| 137 | } |
| 138 | |
| 139 | Error BitstreamRemarkParserHelper::(unsigned Code) { |
| 140 | Record.clear(); |
| 141 | Expected<unsigned> MaybeRecordID = |
| 142 | Stream.readRecord(AbbrevID: Code, Vals&: Record, Blob: &RecordBlob); |
| 143 | if (!MaybeRecordID) |
| 144 | return MaybeRecordID.takeError(); |
| 145 | RecordID = *MaybeRecordID; |
| 146 | return handleRecord(); |
| 147 | } |
| 148 | |
| 149 | Error BitstreamRemarkParserHelper::handleRecord() { |
| 150 | switch (RecordID) { |
| 151 | case RECORD_REMARK_HEADER: { |
| 152 | if (Record.size() != 4) |
| 153 | return malformedRecord(RecordName: RemarkHeaderName); |
| 154 | Type = Record[0]; |
| 155 | RemarkNameIdx = Record[1]; |
| 156 | PassNameIdx = Record[2]; |
| 157 | FunctionNameIdx = Record[3]; |
| 158 | break; |
| 159 | } |
| 160 | case RECORD_REMARK_DEBUG_LOC: { |
| 161 | if (Record.size() != 3) |
| 162 | return malformedRecord(RecordName: RemarkDebugLocName); |
| 163 | Loc = {.SourceFileNameIdx: Record[0], .SourceLine: Record[1], .SourceColumn: Record[2]}; |
| 164 | break; |
| 165 | } |
| 166 | case RECORD_REMARK_HOTNESS: { |
| 167 | if (Record.size() != 1) |
| 168 | return malformedRecord(RecordName: RemarkHotnessName); |
| 169 | Hotness = Record[0]; |
| 170 | break; |
| 171 | } |
| 172 | case RECORD_REMARK_ARG_WITH_DEBUGLOC: { |
| 173 | if (Record.size() != 5) |
| 174 | return malformedRecord(RecordName: RemarkArgWithDebugLocName); |
| 175 | auto &Arg = Args.emplace_back(Args&: Record[0], Args&: Record[1]); |
| 176 | Arg.Loc = {.SourceFileNameIdx: Record[2], .SourceLine: Record[3], .SourceColumn: Record[4]}; |
| 177 | break; |
| 178 | } |
| 179 | case RECORD_REMARK_ARG_WITHOUT_DEBUGLOC: { |
| 180 | if (Record.size() != 2) |
| 181 | return malformedRecord(RecordName: RemarkArgWithoutDebugLocName); |
| 182 | Args.emplace_back(Args&: Record[0], Args&: Record[1]); |
| 183 | break; |
| 184 | } |
| 185 | default: |
| 186 | return unknownRecord(AbbrevID: RecordID); |
| 187 | } |
| 188 | return Error::success(); |
| 189 | } |
| 190 | |
| 191 | Error BitstreamRemarkParserHelper::() { |
| 192 | Type.reset(); |
| 193 | RemarkNameIdx.reset(); |
| 194 | PassNameIdx.reset(); |
| 195 | FunctionNameIdx.reset(); |
| 196 | Hotness.reset(); |
| 197 | Loc.reset(); |
| 198 | Args.clear(); |
| 199 | |
| 200 | return parseBlock(); |
| 201 | } |
| 202 | |
| 203 | Error BitstreamParserHelper::() { |
| 204 | std::array<char, 4> Result; |
| 205 | for (unsigned I = 0; I < 4; ++I) |
| 206 | if (Expected<unsigned> R = Stream.Read(NumBits: 8)) |
| 207 | Result[I] = *R; |
| 208 | else |
| 209 | return R.takeError(); |
| 210 | |
| 211 | StringRef MagicNumber{Result.data(), Result.size()}; |
| 212 | if (MagicNumber != remarks::ContainerMagic) |
| 213 | return error(Fmt: "Unknown magic number: expecting {}, got {}." , |
| 214 | Vals: remarks::ContainerMagic, Vals: MagicNumber); |
| 215 | return Error::success(); |
| 216 | } |
| 217 | |
| 218 | Error BitstreamParserHelper::() { |
| 219 | Expected<BitstreamEntry> Next = Stream.advance(); |
| 220 | if (!Next) |
| 221 | return Next.takeError(); |
| 222 | if (Next->Kind != BitstreamEntry::SubBlock || |
| 223 | Next->ID != llvm::bitc::BLOCKINFO_BLOCK_ID) |
| 224 | return error( |
| 225 | Fmt: "Error while parsing BLOCKINFO_BLOCK: expecting [ENTER_SUBBLOCK, " |
| 226 | "BLOCKINFO_BLOCK, ...]." ); |
| 227 | |
| 228 | Expected<std::optional<BitstreamBlockInfo>> MaybeBlockInfo = |
| 229 | Stream.ReadBlockInfoBlock(); |
| 230 | if (!MaybeBlockInfo) |
| 231 | return MaybeBlockInfo.takeError(); |
| 232 | |
| 233 | if (!*MaybeBlockInfo) |
| 234 | return error(Fmt: "Missing BLOCKINFO_BLOCK." ); |
| 235 | |
| 236 | BlockInfo = **MaybeBlockInfo; |
| 237 | |
| 238 | Stream.setBlockInfo(&BlockInfo); |
| 239 | return Error::success(); |
| 240 | } |
| 241 | |
| 242 | Error BitstreamParserHelper::() { |
| 243 | if (Error E = expectMagic()) |
| 244 | return E; |
| 245 | if (Error E = parseBlockInfoBlock()) |
| 246 | return E; |
| 247 | |
| 248 | // Parse early meta block. |
| 249 | if (Error E = MetaHelper.expectBlock()) |
| 250 | return E; |
| 251 | if (Error E = MetaHelper.parseBlock()) |
| 252 | return E; |
| 253 | |
| 254 | // Skip all Remarks blocks. |
| 255 | while (!Stream.AtEndOfStream()) { |
| 256 | auto MaybeBlockID = expectSubBlock(Stream); |
| 257 | if (!MaybeBlockID) |
| 258 | return MaybeBlockID.takeError(); |
| 259 | if (*MaybeBlockID == META_BLOCK_ID) |
| 260 | break; |
| 261 | if (*MaybeBlockID != REMARK_BLOCK_ID) |
| 262 | return error(Fmt: "Unexpected block between meta blocks." ); |
| 263 | // Remember first remark block. |
| 264 | if (!RemarkStartBitPos) |
| 265 | RemarkStartBitPos = Stream.GetCurrentBitNo(); |
| 266 | if (Error E = Stream.SkipBlock()) |
| 267 | return E; |
| 268 | } |
| 269 | |
| 270 | // Late meta block is optional if there are no remarks. |
| 271 | if (Stream.AtEndOfStream()) |
| 272 | return Error::success(); |
| 273 | |
| 274 | // Parse late meta block. |
| 275 | if (Error E = MetaHelper.parseBlock()) |
| 276 | return E; |
| 277 | return Error::success(); |
| 278 | } |
| 279 | |
| 280 | Error BitstreamParserHelper::() { |
| 281 | if (RemarkStartBitPos) { |
| 282 | RemarkStartBitPos.reset(); |
| 283 | } else { |
| 284 | auto MaybeBlockID = expectSubBlock(Stream); |
| 285 | if (!MaybeBlockID) |
| 286 | return MaybeBlockID.takeError(); |
| 287 | if (*MaybeBlockID != REMARK_BLOCK_ID) |
| 288 | return make_error<EndOfFileError>(); |
| 289 | } |
| 290 | return RemarksHelper->parseNext(); |
| 291 | } |
| 292 | |
| 293 | Expected<std::unique_ptr<BitstreamRemarkParser>> |
| 294 | remarks::( |
| 295 | StringRef Buf, std::optional<StringRef> ExternalFilePrependPath) { |
| 296 | auto Parser = std::make_unique<BitstreamRemarkParser>(args&: Buf); |
| 297 | |
| 298 | if (ExternalFilePrependPath) |
| 299 | Parser->ExternalFilePrependPath = std::string(*ExternalFilePrependPath); |
| 300 | |
| 301 | return std::move(Parser); |
| 302 | } |
| 303 | |
| 304 | BitstreamRemarkParser::(StringRef Buf) |
| 305 | : RemarkParser(Format::Bitstream), ParserHelper(Buf) {} |
| 306 | |
| 307 | Expected<std::unique_ptr<Remark>> BitstreamRemarkParser::() { |
| 308 | if (!IsMetaReady) { |
| 309 | // Container is completely empty. |
| 310 | if (ParserHelper->Stream.AtEndOfStream()) |
| 311 | return make_error<EndOfFileError>(); |
| 312 | |
| 313 | if (Error E = parseMeta()) |
| 314 | return std::move(E); |
| 315 | IsMetaReady = true; |
| 316 | |
| 317 | // Container has meta, but no remarks blocks. |
| 318 | if (!ParserHelper->RemarkStartBitPos) |
| 319 | return error( |
| 320 | Fmt: "Container is non-empty, but does not contain any remarks blocks." ); |
| 321 | |
| 322 | if (Error E = |
| 323 | ParserHelper->Stream.JumpToBit(BitNo: *ParserHelper->RemarkStartBitPos)) |
| 324 | return std::move(E); |
| 325 | ParserHelper->RemarksHelper.emplace(args&: ParserHelper->Stream); |
| 326 | } |
| 327 | |
| 328 | if (Error E = ParserHelper->parseRemark()) |
| 329 | return std::move(E); |
| 330 | return processRemark(); |
| 331 | } |
| 332 | |
| 333 | Error BitstreamRemarkParser::() { |
| 334 | if (Error E = ParserHelper->parseMeta()) |
| 335 | return E; |
| 336 | if (Error E = processCommonMeta()) |
| 337 | return E; |
| 338 | |
| 339 | switch (ContainerType) { |
| 340 | case BitstreamRemarkContainerType::RemarksFileExternal: |
| 341 | return processExternalFilePath(); |
| 342 | case BitstreamRemarkContainerType::RemarksFile: |
| 343 | return processFileContainerMeta(); |
| 344 | } |
| 345 | llvm_unreachable("Unknown BitstreamRemarkContainerType enum" ); |
| 346 | } |
| 347 | |
| 348 | Error BitstreamRemarkParser::() { |
| 349 | auto &Helper = ParserHelper->MetaHelper; |
| 350 | if (!Helper.Container) |
| 351 | return Helper.error(Fmt: "Missing container info." ); |
| 352 | auto &Container = *Helper.Container; |
| 353 | ContainerVersion = Container.Version; |
| 354 | // Always >= BitstreamRemarkContainerType::First since it's unsigned. |
| 355 | if (Container.Type > static_cast<uint8_t>(BitstreamRemarkContainerType::Last)) |
| 356 | return Helper.error(Fmt: "Invalid container type." ); |
| 357 | ContainerType = static_cast<BitstreamRemarkContainerType>(Container.Type); |
| 358 | return Error::success(); |
| 359 | } |
| 360 | |
| 361 | Error BitstreamRemarkParser::() { |
| 362 | if (Error E = processRemarkVersion()) |
| 363 | return E; |
| 364 | if (Error E = processStrTab()) |
| 365 | return E; |
| 366 | return Error::success(); |
| 367 | } |
| 368 | |
| 369 | Error BitstreamRemarkParser::() { |
| 370 | auto &Helper = ParserHelper->MetaHelper; |
| 371 | if (!Helper.StrTabBuf) |
| 372 | return Helper.error(Fmt: "Missing string table." ); |
| 373 | // Parse and assign the string table. |
| 374 | StrTab.emplace(args&: *Helper.StrTabBuf); |
| 375 | return Error::success(); |
| 376 | } |
| 377 | |
| 378 | Error BitstreamRemarkParser::() { |
| 379 | auto &Helper = ParserHelper->MetaHelper; |
| 380 | if (!Helper.RemarkVersion) |
| 381 | return Helper.error(Fmt: "Missing remark version." ); |
| 382 | RemarkVersion = *Helper.RemarkVersion; |
| 383 | return Error::success(); |
| 384 | } |
| 385 | |
| 386 | Error BitstreamRemarkParser::() { |
| 387 | auto &Helper = ParserHelper->MetaHelper; |
| 388 | if (!Helper.ExternalFilePath) |
| 389 | return Helper.error(Fmt: "Missing external file path." ); |
| 390 | |
| 391 | SmallString<80> FullPath(ExternalFilePrependPath); |
| 392 | sys::path::append(path&: FullPath, a: *Helper.ExternalFilePath); |
| 393 | |
| 394 | // External file: open the external file, parse it, check if its metadata |
| 395 | // matches the one from the separate metadata, then replace the current |
| 396 | // parser with the one parsing the remarks. |
| 397 | ErrorOr<std::unique_ptr<MemoryBuffer>> BufferOrErr = |
| 398 | MemoryBuffer::getFile(Filename: FullPath); |
| 399 | if (std::error_code EC = BufferOrErr.getError()) |
| 400 | return createFileError(F: FullPath, EC); |
| 401 | |
| 402 | TmpRemarkBuffer = std::move(*BufferOrErr); |
| 403 | |
| 404 | // Don't try to parse the file if it's empty. |
| 405 | if (TmpRemarkBuffer->getBufferSize() == 0) |
| 406 | return make_error<EndOfFileError>(); |
| 407 | |
| 408 | // Create a separate parser used for parsing the separate file. |
| 409 | ParserHelper.emplace(args: TmpRemarkBuffer->getBuffer()); |
| 410 | if (Error E = parseMeta()) |
| 411 | return E; |
| 412 | |
| 413 | if (ContainerType != BitstreamRemarkContainerType::RemarksFile) |
| 414 | return ParserHelper->MetaHelper.error( |
| 415 | Fmt: "Wrong container type in external file." ); |
| 416 | |
| 417 | return Error::success(); |
| 418 | } |
| 419 | |
| 420 | Expected<std::unique_ptr<Remark>> BitstreamRemarkParser::() { |
| 421 | auto &Helper = *ParserHelper->RemarksHelper; |
| 422 | std::unique_ptr<Remark> Result = std::make_unique<Remark>(); |
| 423 | Remark &R = *Result; |
| 424 | |
| 425 | if (!StrTab) |
| 426 | return Helper.error(Fmt: "Missing string table." ); |
| 427 | |
| 428 | if (!Helper.Type) |
| 429 | return Helper.error(Fmt: "Missing remark type." ); |
| 430 | |
| 431 | // Always >= Type::First since it's unsigned. |
| 432 | if (*Helper.Type > static_cast<uint8_t>(Type::Last)) |
| 433 | return Helper.error(Fmt: "Unknown remark type." ); |
| 434 | |
| 435 | R.RemarkType = static_cast<Type>(*Helper.Type); |
| 436 | |
| 437 | if (!Helper.RemarkNameIdx) |
| 438 | return Helper.error(Fmt: "Missing remark name." ); |
| 439 | |
| 440 | if (Expected<StringRef> = (*StrTab)[*Helper.RemarkNameIdx]) |
| 441 | R.RemarkName = *RemarkName; |
| 442 | else |
| 443 | return RemarkName.takeError(); |
| 444 | |
| 445 | if (!Helper.PassNameIdx) |
| 446 | return Helper.error(Fmt: "Missing remark pass." ); |
| 447 | |
| 448 | if (Expected<StringRef> PassName = (*StrTab)[*Helper.PassNameIdx]) |
| 449 | R.PassName = *PassName; |
| 450 | else |
| 451 | return PassName.takeError(); |
| 452 | |
| 453 | if (!Helper.FunctionNameIdx) |
| 454 | return Helper.error(Fmt: "Missing remark function name." ); |
| 455 | |
| 456 | if (Expected<StringRef> FunctionName = (*StrTab)[*Helper.FunctionNameIdx]) |
| 457 | R.FunctionName = *FunctionName; |
| 458 | else |
| 459 | return FunctionName.takeError(); |
| 460 | |
| 461 | if (Helper.Loc) { |
| 462 | Expected<StringRef> SourceFileName = |
| 463 | (*StrTab)[Helper.Loc->SourceFileNameIdx]; |
| 464 | if (!SourceFileName) |
| 465 | return SourceFileName.takeError(); |
| 466 | R.Loc.emplace(); |
| 467 | R.Loc->SourceFilePath = *SourceFileName; |
| 468 | R.Loc->SourceLine = Helper.Loc->SourceLine; |
| 469 | R.Loc->SourceColumn = Helper.Loc->SourceColumn; |
| 470 | } |
| 471 | |
| 472 | if (Helper.Hotness) |
| 473 | R.Hotness = *Helper.Hotness; |
| 474 | |
| 475 | for (const BitstreamRemarkParserHelper::Argument &Arg : Helper.Args) { |
| 476 | if (!Arg.KeyIdx) |
| 477 | return Helper.error(Fmt: "Missing key in remark argument." ); |
| 478 | if (!Arg.ValueIdx) |
| 479 | return Helper.error(Fmt: "Missing value in remark argument." ); |
| 480 | |
| 481 | // We have at least a key and a value, create an entry. |
| 482 | auto &RArg = R.Args.emplace_back(); |
| 483 | |
| 484 | if (Expected<StringRef> Key = (*StrTab)[*Arg.KeyIdx]) |
| 485 | RArg.Key = *Key; |
| 486 | else |
| 487 | return Key.takeError(); |
| 488 | |
| 489 | if (Expected<StringRef> Value = (*StrTab)[*Arg.ValueIdx]) |
| 490 | RArg.Val = *Value; |
| 491 | else |
| 492 | return Value.takeError(); |
| 493 | |
| 494 | if (Arg.Loc) { |
| 495 | if (Expected<StringRef> SourceFileName = |
| 496 | (*StrTab)[Arg.Loc->SourceFileNameIdx]) { |
| 497 | RArg.Loc.emplace(); |
| 498 | RArg.Loc->SourceFilePath = *SourceFileName; |
| 499 | RArg.Loc->SourceLine = Arg.Loc->SourceLine; |
| 500 | RArg.Loc->SourceColumn = Arg.Loc->SourceColumn; |
| 501 | } else |
| 502 | return SourceFileName.takeError(); |
| 503 | } |
| 504 | } |
| 505 | |
| 506 | return std::move(Result); |
| 507 | } |
| 508 | |