Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions src/commonMain/kotlin/me/alllex/parsus/parser/Grammar.kt
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,7 @@ interface GrammarContext
* }
* ```
*/
abstract class Grammar<out V>(
val ignoreCase: Boolean = false,
private val debugMode: Boolean = false,
) : GrammarContext {
abstract class Grammar<out V>(val ignoreCase: Boolean = false) : GrammarContext {

private val _tokens = mutableListOf<Token>()
private var freezeTokens = false
Expand Down Expand Up @@ -162,7 +159,7 @@ abstract class Grammar<out V>(
beforeParsing()
// If tokenizer impl is changed to EagerTokenizer, then ChoiceParser impl has to be changed to EagerChoiceParser
val tokenizer = ScannerlessTokenizer(input, _tokens)
val parsingContext = ParsingContext(tokenizer, debugMode)
val parsingContext = ParsingContext(tokenizer)
return parsingContext.runParser(createUntilEofParser(parser))
}

Expand All @@ -171,7 +168,7 @@ abstract class Grammar<out V>(
beforeParsing()
// If tokenizer impl is changed to EagerTokenizer, then ChoiceParser impl has to be changed to EagerChoiceParser
val tokenizer = ScannerlessTokenizer(input, _tokens, traceTokenMatching = true)
val parsingContext = ParsingContext(tokenizer, debugMode)
val parsingContext = ParsingContext(tokenizer)
val result = parsingContext.runParser(createUntilEofParser(parser))
val trace = tokenizer.getTokenMatchingTrace() ?: error("Token matching trace is not available")
return TracedParseResult(result, trace)
Expand Down
127 changes: 17 additions & 110 deletions src/commonMain/kotlin/me/alllex/parsus/parser/ParsingContext.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,42 +4,25 @@ import me.alllex.parsus.token.Token
import me.alllex.parsus.token.TokenMatch
import me.alllex.parsus.tokenizer.Tokenizer
import kotlin.coroutines.Continuation
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlin.coroutines.intrinsics.COROUTINE_SUSPENDED
import kotlin.coroutines.intrinsics.createCoroutineUnintercepted
import kotlin.coroutines.intrinsics.suspendCoroutineUninterceptedOrReturn
import kotlin.coroutines.startCoroutine

/**
* Executes parsers, keeping track of current position in the input and error-continuations.
*
* For each [run][runParser] a new context must be created.
*/
internal class ParsingContext(
private val tokenizer: Tokenizer,
private val debugMode: Boolean = false
) : ParsingScope {
internal class ParsingContext(private val tokenizer: Tokenizer) : ParsingScope {

private val inputLength = tokenizer.input.length

private var backtrackCont: Continuation<ParseError>? = null
private var cont: Continuation<Any?>? = null
private var position: Int = 0
private var result: ParseResult<*>? = null
private var lastTokenMatchContext = LastTokenMatchContext(tokenizer.input, currentOffset = 0)
private var result: Result<Any?> = PENDING_RESULT

fun <T> runParser(parser: Parser<T>): ParseResult<T> {
withCont(createParserCoroutine(parser, continuingWith(debugName { "Root $parser" }) { parsedValue ->
this.backtrackCont = null
this.cont = null
this.result = parsedValue.map(::ParsedValue)
}))

runParseLoop()

@Suppress("UNCHECKED_CAST")
return result.getOrThrow() as ParseResult<T>
}
fun <T> runParser(parser: Parser<T>): ParseResult<T> = tryParseImpl(parser)

override val TokenMatch.text: String get() = tokenizer.input.substring(offset, offset + length)

Expand All @@ -51,7 +34,7 @@ internal class ParsingContext(

override suspend fun <R> Parser<R>.invoke(): R = parse()

override suspend fun <R> tryParse(p: Parser<R>): ParseResult<R> {
override fun <R> tryParse(p: Parser<R>): ParseResult<R> {
if (p is Token) {
val tr = tryParse(p)
@Suppress("UNCHECKED_CAST")
Expand Down Expand Up @@ -81,100 +64,24 @@ internal class ParsingContext(
}

override suspend fun fail(error: ParseError): Nothing {
suspendCoroutineUninterceptedOrReturn<Any?> {
withCont(backtrackCont) // may be null
this.result = Result.success(error) // TODO: maybe should additionally wrap into private class
COROUTINE_SUSPENDED // go back into parse loop
}
error("the coroutine must have been cancelled")
}

private suspend fun <T> tryParseImpl(parser: Parser<T>): ParseResult<T> {
return suspendCoroutineUninterceptedOrReturn { mergeCont ->
val prevBacktrack = this.backtrackCont
val curPosition = this.position

val backtrackRestoringCont = continuingWith<T>(debugName { "Forward $parser" }) { parsedValue ->
// If no exceptions and `fail` is never called while `parser` runs we get here
this.backtrackCont = prevBacktrack
// do not restore position, as the input was processed

withCont(mergeCont)
this.result = parsedValue.map { ParsedValue(it) }
}

val newCont = createParserCoroutine(parser, backtrackRestoringCont)

// backtrack path
val newBacktrack = continuingWith<ParseError>(debugName { "Backtrack[$curPosition] $parser" }) {
// We get here if `fail` is called while `parser` runs
this.backtrackCont = prevBacktrack
this.position = curPosition

withCont(mergeCont)
this.result = it
}

this.result = Result.success(Unit)

// We'll continue with the happy path
withCont(newCont)
// backtracking via `orElse` if the happy path fails
this.backtrackCont = newBacktrack

COROUTINE_SUSPENDED // go back into parse loop
}
}

private fun runParseLoop() {
while (true) {
val cont = this.cont ?: break
val resumeValue = this.result

this.cont = null
this.result = PENDING_RESULT

cont.resumeWith(resumeValue)
}
this.result = error
suspendCoroutineUninterceptedOrReturn<Nothing> { COROUTINE_SUSPENDED }
}

private fun <T> createParserCoroutine(parser: Parser<T>, then: Continuation<T>): Continuation<Unit> {
val doParse: suspend ParsingScope.() -> T = {
parser.run {
parse()
}
}

return doParse.createCoroutineUnintercepted(this, then)
}

private fun withCont(continuation: Continuation<*>?) {
// It's equivalent to: try { parser.parse() } catch { this.position = curPosition }.
// The whole suspend machinery mimics checked exceptions
private fun <T> tryParseImpl(parser: Parser<T>): ParseResult<T> {
val curPosition = this.position
val block: suspend ParsingScope.() -> ParseResult<T> = { parser.run { ParsedValue(parse()) } }
block.startCoroutine(this, Continuation(EmptyCoroutineContext) { res -> result = res.getOrThrow() })
@Suppress("UNCHECKED_CAST")
this.cont = continuation as Continuation<Any?>?
}

private inline fun debugName(block: () -> String): String? {
return if (debugMode) block() else null
val res = result!!.map { it as T }
if (res is ParseError) this.position = curPosition
return res
}

override fun toString(): String {
return "ParsingContext(position=$position, result=$result)"
}

companion object {
private val PENDING_RESULT = Result.success(COROUTINE_SUSPENDED)

private inline fun <T> continuingWith(
debugName: String? = null,
crossinline resumeWith: (Result<T>) -> Unit
): Continuation<T> {
return if (debugName == null) Continuation(EmptyCoroutineContext, resumeWith)
else object : Continuation<T> {
override val context: CoroutineContext get() = EmptyCoroutineContext
override fun resumeWith(result: Result<T>) = resumeWith(result)
override fun toString(): String = debugName
}
}
return "ParsingContext(position=$position, result=${result})"
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ interface ParsingScope {
* If this or any underlying parser fails, execution is continued here
* with a wrapped [error][ParseError].
*/
suspend fun <R> tryParse(p: Parser<R>): ParseResult<R>
fun <R> tryParse(p: Parser<R>): ParseResult<R>

/**
* Tries to parse given [token] at the current position in the input.
Expand Down
4 changes: 2 additions & 2 deletions src/commonMain/kotlin/me/alllex/parsus/parser/parsers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ suspend fun <R> ParsingScope.choose(p: Parser<R>, ps: List<Parser<R>>): R {
/**
* Returns true if the parser executes successfully (consuming input) and false otherwise (not consuming any input).
*/
suspend fun ParsingScope.has(p: Parser<Any>): Boolean = checkPresent(p)
fun ParsingScope.has(p: Parser<Any>): Boolean = checkPresent(p)

/**
* Returns true if the parser executes successfully (consuming input) and false otherwise (not consuming any input).
*/
suspend fun ParsingScope.checkPresent(p: Parser<Any>): Boolean = tryOrNull(p) != null
fun ParsingScope.checkPresent(p: Parser<Any>): Boolean = tryOrNull(p) != null

suspend fun <R : Any> ParsingScope.repeatOneOrMore(p: Parser<R>): List<R> = repeat(p, atLeast = 1)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,12 @@ fun <T : Any> optional(parser: Parser<T>): Parser<T?> = parser {
/**
* Runs the [parser] and returns its result or null in case of failure.
*/
suspend fun <R : Any> ParsingScope.tryOrNull(parser: Parser<R>): R? = tryParse(parser).getOrElse { null }
fun <R : Any> ParsingScope.tryOrNull(parser: Parser<R>): R? = tryParse(parser).getOrElse { null }

/**
* Runs the [parser] and returns its result or null in case of failure.
*/
suspend fun <R : Any> ParsingScope.poll(parser: Parser<R>): R? = tryOrNull(parser)
fun <R : Any> ParsingScope.poll(parser: Parser<R>): R? = tryOrNull(parser)

/**
* Executes given parser, ignoring the result.
Expand Down