From d7cda75a0aecfc08f1b7b52565f9a0434821bd62 Mon Sep 17 00:00:00 2001 From: Nikita Bobko Date: Thu, 7 Aug 2025 18:09:07 +0200 Subject: [PATCH] Simplify tryParseImpl & fail Effectively, the way coroutines are used here is to mimic checked exceptions. This patch makes it more obvious and simplifies the implementation `debugMode` is aparently no longer needed since we don't juggle Continuations as actively as before --- .../kotlin/me/alllex/parsus/parser/Grammar.kt | 9 +- .../me/alllex/parsus/parser/ParsingContext.kt | 127 +++--------------- .../me/alllex/parsus/parser/ParsingScope.kt | 2 +- .../kotlin/me/alllex/parsus/parser/parsers.kt | 4 +- .../alllex/parsus/parser/skipCombinators.kt | 4 +- 5 files changed, 25 insertions(+), 121 deletions(-) diff --git a/src/commonMain/kotlin/me/alllex/parsus/parser/Grammar.kt b/src/commonMain/kotlin/me/alllex/parsus/parser/Grammar.kt index 69f3595..8c45ad3 100644 --- a/src/commonMain/kotlin/me/alllex/parsus/parser/Grammar.kt +++ b/src/commonMain/kotlin/me/alllex/parsus/parser/Grammar.kt @@ -39,10 +39,7 @@ interface GrammarContext * } * ``` */ -abstract class Grammar( - val ignoreCase: Boolean = false, - private val debugMode: Boolean = false, -) : GrammarContext { +abstract class Grammar(val ignoreCase: Boolean = false) : GrammarContext { private val _tokens = mutableListOf() private var freezeTokens = false @@ -162,7 +159,7 @@ abstract class Grammar( 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)) } @@ -171,7 +168,7 @@ abstract class Grammar( 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) diff --git a/src/commonMain/kotlin/me/alllex/parsus/parser/ParsingContext.kt b/src/commonMain/kotlin/me/alllex/parsus/parser/ParsingContext.kt index c038f86..823d300 100644 --- a/src/commonMain/kotlin/me/alllex/parsus/parser/ParsingContext.kt +++ b/src/commonMain/kotlin/me/alllex/parsus/parser/ParsingContext.kt @@ -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? = null - private var cont: Continuation? = null private var position: Int = 0 + private var result: ParseResult<*>? = null private var lastTokenMatchContext = LastTokenMatchContext(tokenizer.input, currentOffset = 0) - private var result: Result = PENDING_RESULT - fun runParser(parser: Parser): ParseResult { - 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 - } + fun runParser(parser: Parser): ParseResult = tryParseImpl(parser) override val TokenMatch.text: String get() = tokenizer.input.substring(offset, offset + length) @@ -51,7 +34,7 @@ internal class ParsingContext( override suspend fun Parser.invoke(): R = parse() - override suspend fun tryParse(p: Parser): ParseResult { + override fun tryParse(p: Parser): ParseResult { if (p is Token) { val tr = tryParse(p) @Suppress("UNCHECKED_CAST") @@ -81,100 +64,24 @@ internal class ParsingContext( } override suspend fun fail(error: ParseError): Nothing { - suspendCoroutineUninterceptedOrReturn { - 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 tryParseImpl(parser: Parser): ParseResult { - return suspendCoroutineUninterceptedOrReturn { mergeCont -> - val prevBacktrack = this.backtrackCont - val curPosition = this.position - - val backtrackRestoringCont = continuingWith(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(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 { COROUTINE_SUSPENDED } } - private fun createParserCoroutine(parser: Parser, then: Continuation): Continuation { - 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 tryParseImpl(parser: Parser): ParseResult { + val curPosition = this.position + val block: suspend ParsingScope.() -> ParseResult = { parser.run { ParsedValue(parse()) } } + block.startCoroutine(this, Continuation(EmptyCoroutineContext) { res -> result = res.getOrThrow() }) @Suppress("UNCHECKED_CAST") - this.cont = continuation as Continuation? - } - - 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 continuingWith( - debugName: String? = null, - crossinline resumeWith: (Result) -> Unit - ): Continuation { - return if (debugName == null) Continuation(EmptyCoroutineContext, resumeWith) - else object : Continuation { - override val context: CoroutineContext get() = EmptyCoroutineContext - override fun resumeWith(result: Result) = resumeWith(result) - override fun toString(): String = debugName - } - } + return "ParsingContext(position=$position, result=${result})" } } diff --git a/src/commonMain/kotlin/me/alllex/parsus/parser/ParsingScope.kt b/src/commonMain/kotlin/me/alllex/parsus/parser/ParsingScope.kt index cc8c5e7..38d51aa 100644 --- a/src/commonMain/kotlin/me/alllex/parsus/parser/ParsingScope.kt +++ b/src/commonMain/kotlin/me/alllex/parsus/parser/ParsingScope.kt @@ -25,7 +25,7 @@ interface ParsingScope { * If this or any underlying parser fails, execution is continued here * with a wrapped [error][ParseError]. */ - suspend fun tryParse(p: Parser): ParseResult + fun tryParse(p: Parser): ParseResult /** * Tries to parse given [token] at the current position in the input. diff --git a/src/commonMain/kotlin/me/alllex/parsus/parser/parsers.kt b/src/commonMain/kotlin/me/alllex/parsus/parser/parsers.kt index fe39c64..13a7db9 100644 --- a/src/commonMain/kotlin/me/alllex/parsus/parser/parsers.kt +++ b/src/commonMain/kotlin/me/alllex/parsus/parser/parsers.kt @@ -32,12 +32,12 @@ suspend fun ParsingScope.choose(p: Parser, ps: List>): R { /** * Returns true if the parser executes successfully (consuming input) and false otherwise (not consuming any input). */ -suspend fun ParsingScope.has(p: Parser): Boolean = checkPresent(p) +fun ParsingScope.has(p: Parser): 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): Boolean = tryOrNull(p) != null +fun ParsingScope.checkPresent(p: Parser): Boolean = tryOrNull(p) != null suspend fun ParsingScope.repeatOneOrMore(p: Parser): List = repeat(p, atLeast = 1) diff --git a/src/commonMain/kotlin/me/alllex/parsus/parser/skipCombinators.kt b/src/commonMain/kotlin/me/alllex/parsus/parser/skipCombinators.kt index 405abcd..bbfcb6c 100644 --- a/src/commonMain/kotlin/me/alllex/parsus/parser/skipCombinators.kt +++ b/src/commonMain/kotlin/me/alllex/parsus/parser/skipCombinators.kt @@ -57,12 +57,12 @@ fun optional(parser: Parser): Parser = parser { /** * Runs the [parser] and returns its result or null in case of failure. */ -suspend fun ParsingScope.tryOrNull(parser: Parser): R? = tryParse(parser).getOrElse { null } +fun ParsingScope.tryOrNull(parser: Parser): R? = tryParse(parser).getOrElse { null } /** * Runs the [parser] and returns its result or null in case of failure. */ -suspend fun ParsingScope.poll(parser: Parser): R? = tryOrNull(parser) +fun ParsingScope.poll(parser: Parser): R? = tryOrNull(parser) /** * Executes given parser, ignoring the result.