pointfreeco/swift-parsing

Failure to correctly differentiate between Int and Double parser

rex-remind101 opened this issue · 5 comments

When using OneOf there is failure to differentiate between an Int and Double.

Examples:

let parser = OneOf {
    Double.parser(of: Substring.self).map(.case(Value.float))
    Int.parser().map(.case(Value.int))
}

let input = "1.0"
let argument = try parser.parse(input)
XCTAssertEqual(argument, .float(1.0))

passes


let parser = OneOf {
    Int.parser().map(.case(Value.int))
    Double.parser(of: Substring.self).map(.case(Value.float))
}

let input = "1.0"
let argument = try parser.parse(input)
XCTAssertEqual(argument, .float(1.0))

fails:

caught error: "error: unexpected input
 --> input:1:2
1 | 1.0
  |  ^ expected end of input"

let parser = OneOf {
    Int.parser().map(.case(Value.int))
    Double.parser(of: Substring.self).map(.case(Value.float))
}

let input = "1.0"
let argument = try parser.parse(input)
XCTAssertEqual(argument, .int(1))

passes


let parser = OneOf {
    Double.parser(of: Substring.self).map(.case(Value.float))
    Int.parser().map(.case(Value.int))
}

let input = "1.0"
let argument = try parser.parse(input)
XCTAssertEqual(argument, .int(1))

fails:

XCTAssertEqual failed: ("float(1.0)") is not equal to ("int(1)")

@rex-remind101 OneOf attempts each parser until one succeeds.

When the input is "1.0":

  • Int.parser() successfully parses 1 and leaves a remaining input of ".0"
  • Double.parser() successfully parses 1.0 and leaves a remaining input of "" (no remaining input)

What might be causing some confusions is that try parser.parse(input) will fail if the parser does not consume the entirety of the input.

You can use a mutable value, eg: try parser.parse(&input), to parse a value incrementally. The documentation for Parser/parse(_:) goes into this in more detail.

So for your failing examples:

let parser = OneOf {
    Int.parser().map(.case(Value.int))
    Double.parser(of: Substring.self).map(.case(Value.float))
}

let input = "1.0"
let argument = try parser.parse(input)
XCTAssertEqual(argument, .float(1.0))
  1. Int.parser() succeeds and parses .int(1)
  2. The parser fails because there is a remaining input of ".0"
let parser = OneOf {
    Double.parser(of: Substring.self).map(.case(Value.float))
    Int.parser().map(.case(Value.int))
}

let input = "1.0"
let argument = try parser.parse(input)
XCTAssertEqual(argument, .int(1))
  1. Double.parser() succeeds and parses .float(1.0)
  2. The parser succeeds because all input was consumed
  3. The assertion fails because .float(1.0) does not equal .int(1)

I guess that's just not the semantics I want, a decimal point implies a float number for my use case and I believe that is typical. I ended up doing a copy-paste and slight refactor of IntParser so that it fails on look-ahead if it finds ",", then by organizing MyIntParser before Double.parser it appears to succeed in all my tested cases.

Fwiw, I don't see how the current semantics could allow clear disambiguation between Int and Double at all in a OneOf, unless I'm missing something, and this seems to me like something people may want in general.

@rex-remind101 You can probably implement the lookahead with a Not { "." } parser:

let parser = OneOf {
  Parse(.case(Value.int)) {
    Int.parser()
    Not { "." }
  }
  Double.parser()
    .map(.case(Value.float))
}

Amazing, I'll give that a shot later but makes sense.

May be useful to add this pattern to the Backtracking section https://pointfreeco.github.io/swift-parsing/0.9.0/documentation/parsing/backtracking