swiftlang / swiftlang/swift-experimental-string-processing

RegexBuilder w/ custom component "fails to cast," crashes

Open
#805 2 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

bug
Dominant language
Swift
Stars
308
Forks
52
Avg merge
12h 45m
Merged PRs (30d)
2

Description

Describe the bug
It's possible to write a RegexBuilder (provided below) using the DSL and a CustomConsumingRegexComponent which Swift type-checks and compiles, but then crashes at runtime, in the internals of the regex library itself, claiming that it can't cast the output of a regex to the expected value. I may have missed something in the documentation, but my understanding was that one of the big selling points of the RegexBuilder library is that this is impossible.

As you'll see below, the custom component I've written (ZeroOrMoreOf) is a fairly trivial one: it takes a RegexComponent as input, and matches as many times as possible, eventually producing an Array of the output of the regex it was given.

Steps To Reproduce
You should see this behavior when you compile the following code with Swift 5.7 and run it:

    struct ZeroOrMoreOf<Part>: CustomConsumingRegexComponent where Part: RegexComponent {
      
      init(@RegexComponentBuilder input: () -> Part) {
        self.part = input()
      }
      
      func consuming(
        _ input: String,
        startingAt index: String.Index,
        in bounds: Range<String.Index>
      ) throws -> (upperBound: String.Index, output: RegexOutput)? {
        var output: [Part.RegexOutput] = []
        var bounds = bounds
        var upperBound = index
        if let match = input[bounds].firstMatch(of: part) {
          output.append(match.output)
          bounds = match.range
          upperBound = match.range.upperBound
        }
        if output.count > 0 {
          return (upperBound, output)
        } else {
          throw Error("No matches found")
        }
      }
      
      let part: Part
      
      typealias RegexOutput = [Part.RegexOutput]
        
    }
    @RegexComponentBuilder
    func whiteSpace() -> some RegexComponent<Substring> {
      ZeroOrMore {
        ChoiceOf {
          CharacterClass.whitespace
          CharacterClass.verticalWhitespace
        }
      }
    }
    struct Output {
      let a: Substring
      let b: Substring
    }
    let r = Regex {
      ZeroOrMoreOf {
        ChoiceOf {
          ZeroOrMoreOf {
            Capture {
              Capture {
                CharacterClass.word
              }
              Capture {
                CharacterClass.word
              }
            } transform: {
              Output(a: $0.1, b: $0.2)
            }
          }
          whiteSpace()
        }
      }
    }
    let m = "tt".matches(of: r)
    for o in m {
      print(o.output)
    }

Expected behavior
A regex written with the DSL that compiles, and does not use force casts, should not crash in the manner that this Regex crashes: inside the library, seemingly complaining that it doesn't have the type that it says it does.

Actual behavior: I get the following crash:

Could not cast value of type 'Swift.Substring' (0x1b8cdc268) to '(Swift.Substring, Swift.Substring, Swift.Substring)' (0x1b6f797c0).
2022-10-29 17:08:23.778967-0700 xctest[33061:1058964] Could not cast value of type 'Swift.Substring' (0x1b8cdc268) to '(Swift.Substring, Swift.Substring, Swift.Substring)' (0x1b6f797c0).
Could not cast value of type 'Swift.Substring' (0x1b8cdc268) to '(Swift.Substring, Swift.Substring, Swift.Substring)' (0x1b6f797c0).

Environment (please fill out the following information)

  • OS: iOS 16
  • Xcode Version/Tag/Branch: Xcode 14.0 (14A309), presumably Swift 5.7

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by reproducing the sample with Swift 5.7, then trace how CustomConsumingRegexComponent.consuming propagates its output through the nested RegexBuilder captures used by matches(of:). Add regression coverage for this composition and verify that the compiled regex no longer crashes at runtime and returns the expected output.

Written by the indexing model from the issue text.

Assessment

Tech stack
swift
Domain
tooling
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.