swift-parsing icon indicating copy to clipboard operation
swift-parsing copied to clipboard

Failure to correctly differentiate between Int and Double parser

Open rex-remind101 opened this issue 3 years ago • 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 avatar Sep 23 '22 03:09 rex-remind101

@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)

iampatbrown avatar Sep 23 '22 04:09 iampatbrown

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 avatar Sep 23 '22 05:09 rex-remind101

@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))
}

tgrapperon avatar Sep 23 '22 05:09 tgrapperon

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

rex-remind101 avatar Sep 23 '22 19:09 rex-remind101

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

rex-remind101 avatar Sep 23 '22 19:09 rex-remind101