277 lines
8.6 KiB
Nim
277 lines
8.6 KiB
Nim
import std/[
|
|
options,
|
|
strutils,
|
|
strformat,
|
|
collections/sequtils,
|
|
sugar,
|
|
]
|
|
import fp/[
|
|
maybe,
|
|
resultM,
|
|
]
|
|
import ../utils/str
|
|
import ./parser_types
|
|
|
|
# -- Types
|
|
|
|
type parserFnT = proc(t0: Parser): ParserResult
|
|
|
|
# -- Utilities
|
|
|
|
proc isStreamCompleted*(parser: Parser): bool =
|
|
## Check if the `parser` index is at/over the stream length.
|
|
parser.state.position >= parser.state.stream.len - 1
|
|
|
|
proc isStreamCompleted*(parserResult: ParserResult): bool =
|
|
## Check if the `parserResult.state` index is at/over the stream length.
|
|
parserResult.fold(
|
|
err => false,
|
|
isStreamCompleted,
|
|
)
|
|
|
|
# -- Parsing functions
|
|
|
|
proc ch*(expectedChars: set[char]): parserFnT {.inline.} =
|
|
## Create parser function with set of `expectedChar`.
|
|
## When the parser has the character set at the following index return `ParserResult.ok`.
|
|
return proc(parser: Parser): ParserResult =
|
|
let state = parser.state
|
|
let newIndex = state.position + 1
|
|
|
|
if newIndex > (state.stream.len - 1):
|
|
return err(ParserError(
|
|
kind: endOfStringErr,
|
|
expected: expectedChars.prettyExpectedSet(),
|
|
index: newIndex,
|
|
parser: parser,
|
|
))
|
|
else:
|
|
let foundChar = state.stream[newIndex]
|
|
if foundChar in expectedChars:
|
|
return Parser(
|
|
state: ParserState(
|
|
stream: state.stream,
|
|
position: newIndex,
|
|
lastPosition: parser.state.position,
|
|
),
|
|
tokens: parser.tokens & initParserToken(foundChar)
|
|
).ok()
|
|
else:
|
|
return err(ParserError(
|
|
kind: charMismatchErr,
|
|
unexpected: $foundChar,
|
|
expected: expectedChars.prettyExpectedSet(),
|
|
index: newIndex,
|
|
parser: parser,
|
|
))
|
|
|
|
proc ch*(expectedChar: char): parserFnT {.inline.} =
|
|
## Creates parser function with `expectedChar`
|
|
## When the parser has the character at the following index return `ParserResult.ok`
|
|
return proc(parser: Parser): ParserResult =
|
|
let state = parser.state
|
|
let newIndex = state.position + 1
|
|
|
|
if newIndex > (state.stream.len - 1):
|
|
return err(ParserError(
|
|
kind: endOfStringErr,
|
|
expected: &"{expectedChar}",
|
|
index: newIndex,
|
|
parser: parser,
|
|
))
|
|
else:
|
|
let foundChar = state.stream[newIndex]
|
|
if expectedChar == foundChar:
|
|
return Parser(
|
|
state: ParserState(
|
|
stream: state.stream,
|
|
position: newIndex,
|
|
lastPosition: parser.state.position,
|
|
),
|
|
tokens: parser.tokens & initParserToken(foundChar)
|
|
).ok()
|
|
else:
|
|
return err(ParserError(
|
|
kind: charMismatchErr,
|
|
unexpected: &"{foundChar}",
|
|
expected: &"{expectedChar}",
|
|
index: newIndex,
|
|
parser: parser,
|
|
))
|
|
|
|
let anyCh* = ch(AllChars)
|
|
let digit* = ch(Digits)
|
|
|
|
proc str*(expectedString: string): parserFnT {.inline.} =
|
|
## Creates parser function with `expectedString`
|
|
## When the parser has the string at the following index return `ParserResult.ok`
|
|
return proc(parser: Parser): ParserResult =
|
|
var res: ParserResult = parser.ok()
|
|
for c in expectedString.items:
|
|
if res.isErr: break
|
|
res = res.flatMap(ch(c))
|
|
return res
|
|
|
|
# -- Parsing API
|
|
|
|
proc optional*(parserFn: parserFnT): parserFnT {.inline.} =
|
|
## Creates parser function with a nested `parserFn`:
|
|
## Continues on succesful parser
|
|
## Ignores failing parsers
|
|
return proc(parser: Parser): ParserResult =
|
|
let newParser = parserFn(parser)
|
|
if newParser.isOk():
|
|
newParser
|
|
else:
|
|
parser.ok()
|
|
|
|
proc ignore*(parserFn: parserFnT): parserFnT {.inline.} =
|
|
## Creates parser function with a nested `parserFn`:
|
|
## Parses using the `parserFn` but dont capture the resulting tokens.
|
|
return proc(parser: Parser): ParserResult =
|
|
return parserFn(parser)
|
|
.map((x: Parser) => Parser(
|
|
state: x.state,
|
|
tokens: parser.tokens,
|
|
))
|
|
|
|
proc manyUntil*(acceptFn: parserFnT, stopFn: parserFnT): parserFnT {.inline.} =
|
|
## Creates parser function with a nested `acceptFn` parser function until the `stopFn` parserFunction is met:
|
|
## Parses until the `stopFn` is reached or on an errror.
|
|
return proc(parser: Parser): ParserResult =
|
|
var res: ParserResult = parser.ok()
|
|
while res.isOk() and res.flatMap(stopFn).isErr():
|
|
res = res.flatMap(acceptFn)
|
|
return res
|
|
|
|
proc anyUntil*(stopFn: parserFnT): parserFnT {.inline.} =
|
|
## Parses any character until the `stopFn` is reached or on an errror.
|
|
## Needs at least one character match.
|
|
manyUntil(anyCh, stopFn)
|
|
|
|
proc choice*(parserFns: seq[parserFnT]): parserFnT {.inline} =
|
|
## Creates parser function that checks any of the `parserFns`.
|
|
## Needs one match for a `ParserResult.ok`.
|
|
return proc(parser: Parser): ParserResult {.closure.} =
|
|
var errors: seq[ParserResult] = newSeq[ParserResult]()
|
|
var found = Nothing[ParserResult]()
|
|
|
|
for fn in parserFns:
|
|
let fnResult: ParserResult = fn(parser)
|
|
|
|
if fnResult.isOk():
|
|
found = fnResult.just
|
|
break
|
|
else:
|
|
errors = errors & fnResult
|
|
|
|
return found
|
|
.fold(
|
|
proc(): ParserResult =
|
|
let prettyErrors = errors.map((x: ParserResult) => x.error().expected)
|
|
err(ParserError(
|
|
kind: choiceMismatchErr,
|
|
index: parser.state.position + 1,
|
|
expected: &"Choice ({prettyErrors})",
|
|
unexpected: errors[0].error().unexpected,
|
|
parser: parser,
|
|
)),
|
|
proc(x: ParserResult): ParserResult = x,
|
|
)
|
|
|
|
|
|
proc `+`*(parserFnA: parserFnT, parserFnB: parserFnT): parserFnT {.inline.} =
|
|
## Parse characters and ignore failure
|
|
return proc(parser: Parser): ParserResult =
|
|
parserFnA(parser).flatMap(parserFnB)
|
|
|
|
proc parseSeq*(parser: ParserResult, xs: seq[parserFnT]): ParserResult {.inline.} =
|
|
xs.foldl(a.flatMap(b), parser)
|
|
|
|
# -- Parsing Aliases
|
|
|
|
proc endOfStream*(parser: Parser): ParserResult =
|
|
let index = parser.state.position + 1
|
|
if index == parser.state.stream.len:
|
|
ok(parser)
|
|
else:
|
|
err(ParserError(
|
|
kind: endOfStringErr,
|
|
expected: &"EndOfString",
|
|
index: index,
|
|
parser: parser,
|
|
))
|
|
|
|
let newlineParser = choice(@[
|
|
ch(NewLines),
|
|
endOfStream,
|
|
])
|
|
|
|
proc newline*(parser: Parser): ParserResult =
|
|
newlineParser(parser)
|
|
.mapErr((x: ParserError) => x.setErrorExpectedField("Newline"))
|
|
|
|
let whitespaceParser = choice(@[
|
|
ch(Whitespace),
|
|
newlineParser,
|
|
])
|
|
proc whitespace*(parser: Parser): ParserResult =
|
|
whitespaceParser(parser)
|
|
.mapErr((x: ParserError) => x.setErrorExpectedField("Whitespace"))
|
|
|
|
# -- Parsing Helpers
|
|
|
|
let parseBetweenDelimiter* = proc(start: parserFnT, stop: parserFnT): parserFnT {.closure.} =
|
|
ignore(start) + anyUntil(stop + whitespace) + ignore(start)
|
|
|
|
let parseBetweenPair* = proc(delimiterParser: parserFnT): parserFnT {.closure.} =
|
|
parseBetweenDelimiter(delimiterParser, delimiterParser)
|
|
|
|
# -- Tests
|
|
|
|
when isMainModule:
|
|
let testParser123 = initParserResult("123")
|
|
let testAbc1Parser = initParserResult("abc1")
|
|
|
|
block testParsingFunctions:
|
|
let ch1 = ch('1')
|
|
|
|
# Success
|
|
assert testParser123.flatMap(ch1).tokensToString() == "1"
|
|
assert testParser123.flatMap(anyCh).tokensToString() == "1"
|
|
assert testParser123.flatMap(str("123")).tokensToString() == "123"
|
|
|
|
# Mismatch
|
|
assert testParser123.flatMap(ch('2')).error().kind == charMismatchErr
|
|
assert testParser123.flatMap(ch(Letters)).error().kind == charMismatchErr
|
|
assert testParser123.flatMap(str("1234")).error().kind == endOfStringErr
|
|
assert testParser123.flatMap(str("456")).error().kind == charMismatchErr
|
|
|
|
# Out of bounds
|
|
# assert initParserResult("").flatMap(ch1).error().kind == endOfStringErr
|
|
assert initParserResult("1").flatMap(ch1).flatMap(ch1).error().kind == endOfStringErr
|
|
|
|
# Stream end reached
|
|
assert initParserResult("1").flatMap(ch1).isStreamCompleted() == true
|
|
assert initParserResult("12").flatMap(ch1).isStreamCompleted() == false
|
|
assert initParserResult("").flatMap(ch1).isStreamCompleted() == false
|
|
assert testParser123.flatMap(str("123")).isStreamCompleted() == true
|
|
|
|
block testParsingApi:
|
|
# optional
|
|
assert testParser123.flatMap(optional(ch('1'))).tokensToString() == "1"
|
|
assert testParser123.flatMap(optional(ch('2'))).tokensToString() == ""
|
|
|
|
# ignore
|
|
assert testParser123.flatMap(ignore(ch('1'))).tokensToString() == ""
|
|
|
|
# manyUntil
|
|
assert testAbc1Parser.flatMap(manyUntil(anyCh, digit)).tokensToString() == "abc"
|
|
|
|
# anyUntil
|
|
assert testAbc1Parser.flatMap(anyUntil(digit)).tokensToString() == "abc"
|
|
|
|
# choice
|
|
assert testAbc1Parser.flatMap(choice(@[digit, ch('a')])).tokensToString() == "a"
|
|
assert testAbc1Parser.flatMap(choice(@[digit])).error().kind == choiceMismatchErr
|