Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EXC_BREAKPOINT crash when using ReplaceError parser #328

Open
russellyeo opened this issue Dec 3, 2023 · 5 comments
Open

EXC_BREAKPOINT crash when using ReplaceError parser #328

russellyeo opened this issue Dec 3, 2023 · 5 comments

Comments

@russellyeo
Copy link

I have encountered a crash when using replaceError(with:), and can reproduce it using the example documented in the OneOf parser.

enum Currency { case eur, gbp, usd, unknown }

let currency = OneOf {
    "".map { Currency.eur }
    "£".map { Currency.gbp }
    "$".map { Currency.usd }
}
.replaceError(with: Currency.unknown)

print(currency.parse("$")) // Currency.usd
print(currency.parse("฿")) // Thread 1: EXC_BREAKPOINT (code=1, subcode=0x100004650)

Using a SPM executable package on Xcode 15.0.1 (15A507)

I tried stepping through the library code to try see if I could find the root cause, but I think it is beyond my ability/understanding!

Thanks,
Russell

@griffrawk
Copy link

Xcode 16.1 and swift-parsing 0.13.0

Also receiving the same error in the same example. I was testing using the example because I was getting something strange in my own code where .replaceError appeared to do nothing. I thought if the example was Ok, then it was something I'm doing wrong, but now I'm not sure...

func oneOfExample() {
    enum Currency { case eur, gbp, usd, unknown }

    let currency = OneOf {
      "".map { Currency.eur }
      "£".map { Currency.gbp }
      "$".map { Currency.usd }
    }
    .replaceError(with: Currency.unknown)

    print(currency.parse("$"))  // Currency.usd
    print(currency.parse("฿"))  // Currency.unknown Thread 1: EXC_BREAKPOINT (code=1, subcode=0x103370d74)
}

@mbrandonw
Copy link
Member

Hi @griffrawk & @russellyeo, this unfortunately seems to be a Swift compiler bug in @rethrows. That is a mechanism to allow one to have throwing requirements in protocols such that if a conformance does not actually throw then you don't need try. That is why you don't have to try the .parse even though typically parsing is a failable operation.

This may be reason enough to remove @rethrows from the library, which we can do someday (or if someone wants to PR it!), but in the meantime you could also just copy-paste ReplaceError into your own project and add a throws to the parse. That alone will fix the issue.

@griffrawk
Copy link

griffrawk commented Nov 14, 2024

Hi @mbrandonw, thanks for the explanation, very helpful.

Do you have an example for the fix you outline in the last para above please? I'm very new to the package and Swift in general, so still trying to figure out some of the nuances around error handling in general. An example at the simplest level on your original example will probably go a long way to me figuring out my particular use. Thx

In my own code (catching up on last year's AoC) I'm trying to do something like this:

import Parsing

enum Value {
    case Number(Int)
    case Symbol(String)
    case Empty
}

func aocParse() {
    print("\nParse AoC input\n")

    let aocInput = """
        467..114..
        ...*......
        ..35..633.
        ......#...
        617*......
        .....+.58.
        ..592.....
        ......755.
        ...$.*....
        .664.598..
        """

    let choice = OneOf {
        "*".map { Value.Symbol("*") }
        Int.parser().map { Value.Number($0) }
    } .replaceError(with: Value.Empty)

    let setParser = Parse(input: Substring.self) {
        Many {
            choice
        }
    }

    do {
        let parsed = try setParser.parse(aocInput)
        print(parsed)
    } catch {
        print("\(error)")
    }
    
}

At the moment it ignores .replaceError and expects 'input to be consumed' at the first period instead. I'm at a loss as to how I'd split that up such that I can catch errors from the OneOf and still provide a sensible default into Many and back to the output of the parser.

@mbrandonw
Copy link
Member

Hi @griffrawk, if you look at the implementation of replaceError you will see that when an error is thrown, the original input is restored. This means replaceError does not consume any of the input string, and that is why you are getting the "infinite loop" error. Many has detected it will loop forever since each time it runs it does not consume any input.

The way your parser is constructed now it appears that you want to process numbers and "*" and then process and ignore any other characters? If so, you can do that like so:

let choice = OneOf {
  "*".map { Value.Symbol("*") }
  Int.parser().map { Value.Number($0) }
  First().map { _ in Value.Empty }
}

This is a parser that will first try to parse "*" if possible, otherwise will try to parse an integer if possible, otherwise will just consume the next character regardless of what it is.

However, there is one other thing to be aware of here. I'm not sure which AoC problem you are solving, and so not sure if this is intended, but currently the Int.parser() will consume all of "467" at once and turn it into the number 467. Is that what you want? Or do you want to only consume a single digit and turn it into a number?

If the latter, then you want something like this:

let choice = OneOf {
  "*".map { Value.Symbol("*") }

  First()
    .compactMap { Int("\($0)") }
    .map(Value.Number)

  First().map { _ in Value.Empty }
}

That will consume a single character, try to convert it to an integer (the parser will fail if it cannot), and then bundle it into a Value.

@griffrawk
Copy link

griffrawk commented Nov 14, 2024

Aha! Excellent, thank you. It works. And one of those occasions when the order of parsers in OneOf is important.

Yes, I am expecting the whole number 467 to be consumed. It's the example input from AoC 2023 Day 3.

I'll further checkout .replaceError

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants