From b32317cf31aa291a9fce4cd5ea4ee6e113a2ce9f Mon Sep 17 00:00:00 2001 From: Alex Hoppen Date: Sun, 12 Feb 2023 21:29:47 +0100 Subject: [PATCH] Enable a mode in the parser in which it inspects alternative token choices to mutate test cases MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The basic idea is that the parser records which `TokenSpec`s it checked for at individual offsets in the source. We can then use that information to generate new, interesting test cases by replacing a token by the one of the `TokenSpec` we checked for. This technique has found 11 bugs in the parser and I’m expecting it to find quite a few more once we assert that tokens have one of the expected kinds. Gathering of that information is hidden behind a conditional compilation flag because just performing the check of whether we want to record alternative token choices inflicts a 6% performance regression, which doesn’t provide any value except when we are running SwiftParserTest. --- Package.swift | 14 ++- .../SwiftParser/Lexer/LexemeSequence.swift | 14 +++ Sources/SwiftParser/Lookahead.swift | 6 + Sources/SwiftParser/Parser.swift | 28 +++++ Sources/SwiftParser/Recovery.swift | 10 ++ Sources/SwiftParser/TokenConsumer.swift | 38 +++++- Sources/SwiftParser/TokenSpec.swift | 26 +++- Tests/SwiftParserTest/Assertions.swift | 111 ++++++++++++------ Tests/SwiftParserTest/Parser+EntryTests.swift | 20 ++-- build-script.py | 46 ++++++-- 10 files changed, 258 insertions(+), 55 deletions(-) diff --git a/Package.swift b/Package.swift index a1570007ae7..c4d895fb947 100644 --- a/Package.swift +++ b/Package.swift @@ -28,6 +28,13 @@ if ProcessInfo.processInfo.environment["SWIFTSYNTAX_ENABLE_RAWSYNTAX_VALIDATION" ] } +var swiftParserSwiftSettings: [SwiftSetting] = [] +if ProcessInfo.processInfo.environment["SWIFTPARSER_ENABLE_ALTERNATE_TOKEN_INTROSPECTION"] != nil { + swiftParserSwiftSettings += [ + .define("SWIFTPARSER_ENABLE_ALTERNATE_TOKEN_INTROSPECTION") + ] +} + let package = Package( name: "SwiftSyntax", platforms: [ @@ -170,12 +177,15 @@ let package = Package( .target( name: "SwiftParser", dependencies: ["SwiftSyntax"], - exclude: ["CMakeLists.txt", "README.md"] + exclude: ["CMakeLists.txt", "README.md"], + swiftSettings: swiftParserSwiftSettings + ), .testTarget( name: "SwiftParserTest", - dependencies: ["_SwiftSyntaxTestSupport", "SwiftDiagnostics", "SwiftOperators", "SwiftParser", "SwiftSyntaxBuilder"] + dependencies: ["_SwiftSyntaxTestSupport", "SwiftDiagnostics", "SwiftOperators", "SwiftParser", "SwiftSyntaxBuilder"], + swiftSettings: swiftParserSwiftSettings ), // MARK: SwiftParserDiagnostics diff --git a/Sources/SwiftParser/Lexer/LexemeSequence.swift b/Sources/SwiftParser/Lexer/LexemeSequence.swift index 266cbe4db9c..1c961853a77 100644 --- a/Sources/SwiftParser/Lexer/LexemeSequence.swift +++ b/Sources/SwiftParser/Lexer/LexemeSequence.swift @@ -81,6 +81,20 @@ extension Lexer { return remainingText } } + + #if SWIFTPARSER_ENABLE_ALTERNATE_TOKEN_INTROSPECTION + /// If `pointer` is in the source buffer of this `LexemeSequence`, return + /// its offset, otherwise `nil`. Should only be used for the parser's + /// alternate token introspection + func offset(of pointer: UnsafePointer) -> Int? { + let offset = pointer - self.sourceBufferStart.input.baseAddress! + if offset <= self.sourceBufferStart.input.count { + return offset + } else { + return nil + } + } + #endif } @_spi(RawSyntax) diff --git a/Sources/SwiftParser/Lookahead.swift b/Sources/SwiftParser/Lookahead.swift index 94d687afe1f..94a8f8d5b62 100644 --- a/Sources/SwiftParser/Lookahead.swift +++ b/Sources/SwiftParser/Lookahead.swift @@ -70,6 +70,12 @@ extension Parser.Lookahead: TokenConsumer { mutating func eat(_ spec: TokenSpec) -> Token { return self.consume(if: spec)! } + + #if SWIFTPARSER_ENABLE_ALTERNATE_TOKEN_INTROSPECTION + var shouldRecordAlternativeTokenChoices: Bool { false } + + mutating public func recordAlternativeTokenChoice(for lexeme: Lexer.Lexeme, choices: [TokenSpec]) {} + #endif } extension Parser.Lookahead { diff --git a/Sources/SwiftParser/Parser.swift b/Sources/SwiftParser/Parser.swift index c692c6dbe98..dc743d7af70 100644 --- a/Sources/SwiftParser/Parser.swift +++ b/Sources/SwiftParser/Parser.swift @@ -207,6 +207,34 @@ public struct Parser { break } } + + #if SWIFTPARSER_ENABLE_ALTERNATE_TOKEN_INTROSPECTION + var shouldRecordAlternativeTokenChoices: Bool = false + + public mutating func enableAlternativeTokenChoices() { + shouldRecordAlternativeTokenChoices = true + } + + /// When compiled with `SWIFTPARSER_ENABLE_ALTERNATE_TOKEN_INTROSPECTION`, and + /// `shouldRecordAlternativeTokenChoices` is `true` the parser records which + /// `TokenSpec`s it checked for a token at a specific offset in the source + /// file. The offsets are the location of the token text's start (excluding + /// leading trivia). + /// + /// This information allows testing techniques to replace tokens by these + /// alternate token choices to generate new, interesting test cases + @_spi(RawSyntax) + public var alternativeTokenChoices: [Int: [TokenSpec]] = [:] + + mutating func recordAlternativeTokenChoice(for lexeme: Lexer.Lexeme, choices: [TokenSpec]) { + guard let lexemeBaseAddress = lexeme.tokenText.baseAddress, + let offset = lexemes.offset(of: lexemeBaseAddress) + else { + return + } + alternativeTokenChoices[offset, default: []].append(contentsOf: choices) + } + #endif } // MARK: Inspecting Tokens diff --git a/Sources/SwiftParser/Recovery.swift b/Sources/SwiftParser/Recovery.swift index c999fddf636..08ee33bf046 100644 --- a/Sources/SwiftParser/Recovery.swift +++ b/Sources/SwiftParser/Recovery.swift @@ -67,6 +67,11 @@ extension Parser.Lookahead { _ spec2: TokenSpec, _ spec3: TokenSpec ) -> RecoveryConsumptionHandle? { + #if SWIFTPARSER_ENABLE_ALTERNATE_TOKEN_INTROSPECTION + if shouldRecordAlternativeTokenChoices { + recordAlternativeTokenChoice(for: self.currentToken, choices: [spec1, spec2, spec3]) + } + #endif let initialTokensConsumed = self.tokensConsumed let recoveryPrecedence = min(spec1.recoveryPrecedence, spec2.recoveryPrecedence, spec3.recoveryPrecedence) @@ -119,6 +124,11 @@ extension Parser.Lookahead { anyIn specSet: SpecSet.Type, overrideRecoveryPrecedence: TokenPrecedence? = nil ) -> (match: SpecSet, handle: RecoveryConsumptionHandle)? { + #if SWIFTPARSER_ENABLE_ALTERNATE_TOKEN_INTROSPECTION + if shouldRecordAlternativeTokenChoices { + recordAlternativeTokenChoice(for: self.currentToken, choices: specSet.allCases.map(\.spec)) + } + #endif let initialTokensConsumed = self.tokensConsumed precondition(!specSet.allCases.isEmpty, "SpecSet must have at least one case") diff --git a/Sources/SwiftParser/TokenConsumer.swift b/Sources/SwiftParser/TokenConsumer.swift index 824675c11e4..b7c8bb5d2a6 100644 --- a/Sources/SwiftParser/TokenConsumer.swift +++ b/Sources/SwiftParser/TokenConsumer.swift @@ -13,8 +13,7 @@ @_spi(RawSyntax) import SwiftSyntax /// A type that consumes instances of `TokenSyntax`. -@_spi(RawSyntax) -public protocol TokenConsumer { +protocol TokenConsumer { associatedtype Token /// The current token syntax being examined by the consumer var currentToken: Lexer.Lexeme { get } @@ -32,6 +31,21 @@ public protocol TokenConsumer { func peek() -> Lexer.Lexeme func lookahead() -> Parser.Lookahead + + #if SWIFTPARSER_ENABLE_ALTERNATE_TOKEN_INTROSPECTION + var shouldRecordAlternativeTokenChoices: Bool { get } + + /// When compiled with `SWIFTPARSER_ENABLE_ALTERNATE_TOKEN_INTROSPECTION`, + /// record alternative tokens that the parser was looking for at the offset of + /// `lexeme`. + /// + /// E.g. if at offset 33, we issue an `at(.leftParen)` call, this will record + /// that `.leftParen` is an interesting token at offset 33. This allows the + /// test case mutators to prefer replacing the current token at offset 33 by a + /// left paren, because apparently this would be a code path that the parser + /// is interested in. + mutating func recordAlternativeTokenChoice(for lexeme: Lexer.Lexeme, choices: [TokenSpec]) + #endif } // MARK: Checking if we are at one specific token (`at`) @@ -51,6 +65,11 @@ extension TokenConsumer { /// Returns whether the the current token matches `spec` @inline(__always) mutating func at(_ spec: TokenSpec) -> Bool { + #if SWIFTPARSER_ENABLE_ALTERNATE_TOKEN_INTROSPECTION + if shouldRecordAlternativeTokenChoices { + recordAlternativeTokenChoice(for: self.currentToken, choices: [spec]) + } + #endif return spec ~= self.currentToken } @@ -60,6 +79,11 @@ extension TokenConsumer { _ spec1: TokenSpec, _ spec2: TokenSpec ) -> Bool { + #if SWIFTPARSER_ENABLE_ALTERNATE_TOKEN_INTROSPECTION + if shouldRecordAlternativeTokenChoices { + recordAlternativeTokenChoice(for: self.currentToken, choices: [spec1, spec2]) + } + #endif switch self.currentToken { case spec1: return true case spec2: return true @@ -74,6 +98,11 @@ extension TokenConsumer { _ spec2: TokenSpec, _ spec3: TokenSpec ) -> Bool { + #if SWIFTPARSER_ENABLE_ALTERNATE_TOKEN_INTROSPECTION + if shouldRecordAlternativeTokenChoices { + recordAlternativeTokenChoice(for: self.currentToken, choices: [spec1, spec2, spec3]) + } + #endif switch self.currentToken { case spec1: return true case spec2: return true @@ -93,6 +122,11 @@ extension TokenConsumer { /// as well as a handle to consume that token. @inline(__always) mutating func at(anyIn specSet: SpecSet.Type) -> (SpecSet, TokenConsumptionHandle)? { + #if SWIFTPARSER_ENABLE_ALTERNATE_TOKEN_INTROSPECTION + if shouldRecordAlternativeTokenChoices { + recordAlternativeTokenChoice(for: self.currentToken, choices: specSet.allCases.map(\.spec)) + } + #endif if let matchedKind = SpecSet(lexeme: self.currentToken) { precondition(matchedKind.spec ~= self.currentToken) return ( diff --git a/Sources/SwiftParser/TokenSpec.swift b/Sources/SwiftParser/TokenSpec.swift index 642a92daa1d..79f76a8b619 100644 --- a/Sources/SwiftParser/TokenSpec.swift +++ b/Sources/SwiftParser/TokenSpec.swift @@ -44,7 +44,8 @@ struct PrepareForKeywordMatch { /// marked `@inline(__always)` so the compiler inlines the `RawTokenKind` we are /// matching against and is thus able to rule out one of the branches in /// `matches(rawTokenKind:text:)` based on the matched kind. -struct TokenSpec { +@_spi(RawSyntax) +public struct TokenSpec { /// The kind we expect the token that we want to consume to have. /// This can be a keyword, in which case the `TokenSpec` will also match an /// identifier with the same text as the keyword and remap it to that keyword @@ -160,6 +161,29 @@ struct TokenSpec { atStartOfLine: lexeme.isAtStartOfLine ) } + + /// Returns a `TokenKind` that will most likely be parsed as a token that + /// matches this `TokenSpec`. + /// + /// IMPORTANT: Should only be used when generating tokens during the + /// modification of test cases. This should never be used in the parser itself. + public var synthesizedTokenKind: TokenKind { + switch rawTokenKind { + case .binaryOperator: return .binaryOperator("+") + case .dollarIdentifier: return .dollarIdentifier("$0") + case .extendedRegexDelimiter: return .extendedRegexDelimiter("#") + case .floatingLiteral: return .floatingLiteral("1.0") + case .identifier: return .identifier("myIdent") + case .integerLiteral: return .integerLiteral("1") + case .keyword: return .keyword(keyword!) + case .postfixOperator: return .postfixOperator("++") + case .prefixOperator: return .prefixOperator("!") + case .rawStringDelimiter: return .rawStringDelimiter("#") + case .regexLiteralPattern: return .regexLiteralPattern(".*") + case .stringSegment: return .stringSegment("abc") + default: return TokenKind.fromRaw(kind: rawTokenKind, text: "") + } + } } extension TokenConsumer { diff --git a/Tests/SwiftParserTest/Assertions.swift b/Tests/SwiftParserTest/Assertions.swift index 2a4343d69ee..d55e9505aed 100644 --- a/Tests/SwiftParserTest/Assertions.swift +++ b/Tests/SwiftParserTest/Assertions.swift @@ -454,6 +454,39 @@ func assertDiagnostic( } } +class MutatedTreePrinter: SyntaxVisitor { + private var mutations: [Int: TokenSpec] = [:] + private var printedSource: [UInt8] = [] + + /// Prints `tree` by replacing the tokens whose offset is in `mutations` by + /// a token that matches the corresponding `TokenSpec`. + static func print(tree: Syntax, mutations: [Int: TokenSpec]) -> [UInt8] { + let printer = MutatedTreePrinter(mutations: mutations) + printer.walk(tree) + return printer.printedSource + } + + private init(mutations: [Int: TokenSpec]) { + self.mutations = mutations + super.init(viewMode: .sourceAccurate) + } + + override func visit(_ node: TokenSyntax) -> SyntaxVisitorContinueKind { + if let mutation = mutations[node.positionAfterSkippingLeadingTrivia.utf8Offset] { + let token = TokenSyntax( + mutation.synthesizedTokenKind, + leadingTrivia: node.leadingTrivia, + trailingTrivia: node.trailingTrivia, + presence: .present + ) + printedSource.append(contentsOf: token.syntaxTextBytes) + return .skipChildren + } + printedSource.append(contentsOf: node.syntaxTextBytes) + return .skipChildren + } +} + public struct AssertParseOptions: OptionSet { public var rawValue: UInt8 @@ -497,38 +530,6 @@ func assertParse( ) } -/// Same as `assertParse` overload with a `(String) -> S` `parse`, -/// constructing a `Parser` from the given `String` and passing that to -/// `parse` instead. -func assertParse( - _ markedSource: String, - _ parse: (inout Parser) -> S, - substructure expectedSubstructure: Syntax? = nil, - substructureAfterMarker: String = "START", - diagnostics expectedDiagnostics: [DiagnosticSpec] = [], - applyFixIts: [String]? = nil, - fixedSource expectedFixedSource: String? = nil, - options: AssertParseOptions = [], - file: StaticString = #file, - line: UInt = #line -) { - assertParse( - markedSource, - { (source: String) -> S in - var parser = Parser(source) - return parse(&parser) - }, - substructure: expectedSubstructure, - substructureAfterMarker: substructureAfterMarker, - diagnostics: expectedDiagnostics, - applyFixIts: applyFixIts, - fixedSource: expectedFixedSource, - options: options, - file: file, - line: line - ) -} - /// Removes any test markers from `markedSource` (1) and parses the result /// using `parse`. By default it only checks if the parsed syntax tree is /// printable back to the origin source, ie. it round trips. @@ -549,7 +550,7 @@ func assertParse( /// this string. func assertParse( _ markedSource: String, - _ parse: (String) -> S, + _ parse: (inout Parser) -> S, substructure expectedSubstructure: Syntax? = nil, substructureAfterMarker: String = "START", diagnostics expectedDiagnostics: [DiagnosticSpec] = [], @@ -563,7 +564,15 @@ func assertParse( var (markerLocations, source) = extractMarkers(markedSource) markerLocations["START"] = 0 - let tree: S = parse(source) + var parser = Parser(source) + #if SWIFTPARSER_ENABLE_ALTERNATE_TOKEN_INTROSPECTION + let enableTestCaseMutation = ProcessInfo.processInfo.environment["SKIP_LONG_TESTS"] != "1" + + if enableTestCaseMutation { + parser.enableAlternativeTokenChoices() + } + #endif + let tree: S = parse(&parser) // Round-trip assertStringsEqualWithDiff( @@ -623,4 +632,38 @@ func assertParse( line: line ) } + + #if SWIFTPARSER_ENABLE_ALTERNATE_TOKEN_INTROSPECTION + if enableTestCaseMutation { + let mutations: [(offset: Int, replacement: TokenSpec)] = parser.alternativeTokenChoices.flatMap { offset, replacements in + return replacements.map { (offset, $0) } + } + DispatchQueue.concurrentPerform(iterations: mutations.count) { index in + let mutation = mutations[index] + let alternateSource = MutatedTreePrinter.print(tree: Syntax(tree), mutations: [mutation.offset: mutation.replacement]) + alternateSource.withUnsafeBufferPointer { buf in + let mutatedSource = String(decoding: buf, as: UTF8.self) + // Check that we don't hit any assertions in the parser while parsing + // the mutated source and that it round-trips + var mutatedParser = Parser(buf) + let mutatedTree = parse(&mutatedParser) + assertStringsEqualWithDiff( + "\(mutatedTree)", + mutatedSource, + additionalInfo: """ + Mutated source failed to round-trip. + + Mutated source: + \(mutatedSource) + + Actual syntax tree: + \(mutatedTree.debugDescription) + """, + file: file, + line: line + ) + } + } + } + #endif } diff --git a/Tests/SwiftParserTest/Parser+EntryTests.swift b/Tests/SwiftParserTest/Parser+EntryTests.swift index a0b04ecf4ce..054d38784bd 100644 --- a/Tests/SwiftParserTest/Parser+EntryTests.swift +++ b/Tests/SwiftParserTest/Parser+EntryTests.swift @@ -16,18 +16,20 @@ import XCTest public class EntryTests: XCTestCase { func testTopLevelStringParse() throws { - assertParse("func test() {}", { Parser.parse(source: $0) }) + let source = "func test() {}" + let tree = Parser.parse(source: source) + XCTAssert(tree.is(SourceFileSyntax.self)) + XCTAssert(!tree.hasError) + XCTAssertEqual(tree.description, source) } func testTopLevelBufferParse() throws { - assertParse( - "func test() {}", - { (source: String) -> SourceFileSyntax in - var source = source - source.makeContiguousUTF8() - return source.withUTF8 { Parser.parse(source: $0) } - } - ) + var source = "func test() {}" + source.makeContiguousUTF8() + let tree = source.withUTF8 { Parser.parse(source: $0) } + XCTAssert(tree.is(SourceFileSyntax.self)) + XCTAssert(!tree.hasError) + XCTAssertEqual(tree.description, source) } func testSyntaxParse() throws { diff --git a/build-script.py b/build-script.py index 7e8daa5549d..017311297e9 100755 --- a/build-script.py +++ b/build-script.py @@ -175,6 +175,7 @@ class Builder(object): multiroot_data_file: Optional[str] release: bool enable_rawsyntax_validation: bool + enable_test_fuzzing: bool disable_sandbox: bool def __init__( @@ -184,6 +185,7 @@ def __init__( multiroot_data_file: Optional[str], release: bool, enable_rawsyntax_validation: bool, + enable_test_fuzzing: bool, verbose: bool, disable_sandbox: bool = False, ) -> None: @@ -191,6 +193,7 @@ def __init__( self.multiroot_data_file = multiroot_data_file self.release = release self.enable_rawsyntax_validation = enable_rawsyntax_validation + self.enable_test_fuzzing = enable_test_fuzzing self.disable_sandbox = disable_sandbox self.verbose = verbose self.toolchain = toolchain @@ -226,6 +229,8 @@ def __build(self, package_dir: str, product_name: str) -> None: env["SWIFT_BUILD_SCRIPT_ENVIRONMENT"] = "1" if self.enable_rawsyntax_validation: env["SWIFTSYNTAX_ENABLE_RAWSYNTAX_VALIDATION"] = "1" + if self.enable_test_fuzzing: + env["SWIFTPARSER_ENABLE_ALTERNATE_TOKEN_INTROSPECTION"] = "1" # Tell other projects in the unified build to use local dependencies env["SWIFTCI_USE_LOCAL_DEPS"] = "1" env["SWIFT_SYNTAX_PARSER_LIB_SEARCH_PATH"] = \ @@ -278,9 +283,15 @@ def check_generated_files_match(self_generated_dir: str, def run_tests( - toolchain: str, build_dir: Optional[str], multiroot_data_file: Optional[str], - release: bool, enable_rawsyntax_validation: bool, filecheck_exec: Optional[str], - skip_lit_tests: bool, verbose: bool + toolchain: str, + build_dir: Optional[str], + multiroot_data_file: Optional[str], + release: bool, + enable_rawsyntax_validation: bool, + enable_test_fuzzing: bool, + filecheck_exec: Optional[str], + skip_lit_tests: bool, + verbose: bool ) -> None: print("** Running SwiftSyntax Tests **") @@ -299,6 +310,7 @@ def run_tests( multiroot_data_file=multiroot_data_file, release=release, enable_rawsyntax_validation=enable_rawsyntax_validation, + enable_test_fuzzing=enable_test_fuzzing, verbose=verbose, ) @@ -401,10 +413,15 @@ def run_lit_tests(toolchain: str, build_dir: Optional[str], release: bool, # XCTest Tests -def run_xctests(toolchain: str, build_dir: Optional[str], - multiroot_data_file: Optional[str], release: bool, - enable_rawsyntax_validation: bool, - verbose: bool) -> None: +def run_xctests( + toolchain: str, + build_dir: Optional[str], + multiroot_data_file: Optional[str], + release: bool, + enable_rawsyntax_validation: bool, + enable_test_fuzzing: bool, + verbose: bool +) -> None: print("** Running XCTests **") swiftpm_call = get_swiftpm_invocation( toolchain=toolchain, @@ -424,6 +441,8 @@ def run_xctests(toolchain: str, build_dir: Optional[str], env["SWIFT_BUILD_SCRIPT_ENVIRONMENT"] = "1" if enable_rawsyntax_validation: env["SWIFTSYNTAX_ENABLE_RAWSYNTAX_VALIDATION"] = "1" + if enable_test_fuzzing: + env["SWIFTPARSER_ENABLE_ALTERNATE_TOKEN_INTROSPECTION"] = "1" # Tell other projects in the unified build to use local dependencies env["SWIFTCI_USE_LOCAL_DEPS"] = "1" env["SWIFT_SYNTAX_PARSER_LIB_SEARCH_PATH"] = \ @@ -476,6 +495,7 @@ def build_command(args: argparse.Namespace) -> None: multiroot_data_file=args.multiroot_data_file, release=args.release, enable_rawsyntax_validation=args.enable_rawsyntax_validation, + enable_test_fuzzing=args.enable_test_fuzzing, verbose=args.verbose, disable_sandbox=args.disable_sandbox, ) @@ -500,6 +520,7 @@ def test_command(args: argparse.Namespace) -> None: multiroot_data_file=args.multiroot_data_file, release=args.release, enable_rawsyntax_validation=args.enable_rawsyntax_validation, + enable_test_fuzzing=args.enable_test_fuzzing, verbose=args.verbose, disable_sandbox=args.disable_sandbox, ) @@ -513,6 +534,7 @@ def test_command(args: argparse.Namespace) -> None: multiroot_data_file=args.multiroot_data_file, release=args.release, enable_rawsyntax_validation=args.enable_rawsyntax_validation, + enable_test_fuzzing=args.enable_test_fuzzing, filecheck_exec=realpath(args.filecheck_exec), skip_lit_tests=args.skip_lit_tests, verbose=args.verbose, @@ -575,6 +597,16 @@ def add_default_build_arguments(parser: argparse.ArgumentParser) -> None: """ ) + parser.add_argument( + "--enable-test-fuzzing", + action="store_true", + help=""" + For each `assertParse` test, perform mutations of the test case based on + alternate token choices that the parser checks, validating that there are + no round-trip or assertion failures. + """ + ) + parser.add_argument( "--toolchain", required=True,