parsley.internal.machine.Context.scala Maven / Gradle / Ivy
/* SPDX-FileCopyrightText: © 2018 Parsley Contributors
* SPDX-License-Identifier: BSD-3-Clause
*/
package parsley.internal.machine
import scala.annotation.tailrec
//import parsley.{Failure, Result, Success} // not sure why this fails scalacheck, but I guess we'll leave it until I can submit a bug report
import parsley.Failure
import parsley.Result
import parsley.Success
import parsley.errors.ErrorBuilder
import parsley.internal.errors.{ExpectItem, LineBuilder, UnexpectDesc}
import parsley.internal.machine.errors.{
ClassicExpectedError, ClassicFancyError, ClassicUnexpectedError, DefuncError,
DefuncHints, EmptyHints, ErrorItemBuilder
}
import instructions.Instr
import stacks.{ArrayStack, CallStack, CheckStack, ErrorStack, HandlerStack, HintStack, Stack, StateStack}, Stack.StackExt
private [parsley] final class Context(private [machine] var instrs: Array[Instr],
private [machine] val input: String,
numRegs: Int,
private val sourceFile: Option[String]) {
/** This is the operand stack, where results go to live */
private [machine] val stack: ArrayStack[Any] = new ArrayStack()
/** Current offset into the input */
private [machine] var offset: Int = 0
/** The length of the input, stored for whatever reason */
private [machine] val inputsz: Int = input.length
/** Call stack consisting of Frames that track the return position and the old instructions */
private var calls: CallStack = Stack.empty
/** State stack consisting of offsets and positions that can be rolled back */
private [machine] var states: StateStack = Stack.empty
/** Stack consisting of offsets at previous checkpoints, which may query to test for consumed input */
private [machine] var checkStack: CheckStack = Stack.empty
/** Current operational status of the machine */
private [machine] var good: Boolean = true
private [machine] var running: Boolean = true
/** Stack of handlers, which track the call depth, program counter and stack size of error handlers */
private [machine] var handlers: HandlerStack = Stack.empty
/** Current offset into program instruction buffer */
private [machine] var pc: Int = 0
/** Current line number */
private [machine] var line: Int = 1
/** Current column number */
private [machine] var col: Int = 1
/** State held by the registers, AnyRef to allow for `null` */
private [machine] var regs: Array[AnyRef] = new Array[AnyRef](numRegs)
/** Amount of indentation to apply to debug combinators output */
private [machine] var debuglvl: Int = 0
// NEW ERROR MECHANISMS
private var hints: DefuncHints = EmptyHints
private var hintsValidOffset = 0
private var hintStack = Stack.empty[HintStack]
private [machine] var errs: ErrorStack = Stack.empty
private [machine] def saveHints(shadow: Boolean): Unit = {
hintStack = new HintStack(hints, hintsValidOffset, hintStack)
if (!shadow) hints = EmptyHints
}
private [machine] def restoreHints(): Unit = {
val hintFrame = this.hintStack
this.hintsValidOffset = hintFrame.validOffset
this.hints = hintFrame.hints
this.commitHints()
}
private [machine] def commitHints(): Unit = {
this.hintStack = this.hintStack.tail
}
/* Error Debugging Info */
private [machine] def inFlightHints: DefuncHints = hints
private [machine] def inFlightError: DefuncError = errs.error
private [machine] def currentHintsValidOffset: Int = hintsValidOffset
/* ERROR RELABELLING BEGIN */
private [machine] def mergeHints(): Unit = {
val hintFrame = this.hintStack
if (hintFrame.validOffset == offset) this.hints = hintFrame.hints.merge(this.hints)
commitHints()
}
private [machine] def replaceHint(label: String): Unit = hints = hints.rename(label)
private [machine] def popHints(): Unit = hints = hints.pop
/* ERROR RELABELLING END */
private def addErrorToHints(): Unit = {
val err = errs.error
assume(!(!err.isExpectedEmpty) || err.isTrivialError, "not having an empty expected implies you are a trivial error")
if (/*err.isTrivialError && */ !err.isExpectedEmpty && err.offset == offset) { // scalastyle:ignore disallow.space.after.token
// If our new hints have taken place further in the input stream, then they must invalidate the old ones
if (hintsValidOffset < offset) {
hints = EmptyHints
hintsValidOffset = offset
}
hints = hints.addError(err)
}
}
private [machine] def addErrorToHintsAndPop(): Unit = {
this.addErrorToHints()
this.errs = this.errs.tail
}
private [machine] def updateCheckOffsetAndHints() = {
this.checkStack.offset = this.offset
this.hintsValidOffset = this.offset
}
// $COVERAGE-OFF$
private [machine] def pretty: String = {
s"""[
| stack = [${stack.mkString(", ")}]
| instrs = ${instrs.mkString("; ")}
| input = ${input.drop(offset).mkString}
| pos = ($line, $col)
| status = $status
| pc = $pc
| rets = ${calls.mkString(", ")}
| handlers = ${handlers.mkString(", ")}
| recstates = ${states.mkString(", ")}
| checks = ${checkStack.mkString(", ")}
| registers = ${regs.zipWithIndex.map{case (r, i) => s"r$i = $r"}.mkString("\n ")}
| errors = ${errs.mkString(", ")}
| hints = ($hintsValidOffset, ${hints.toSet}):${hintStack.mkString(", ")}
|]""".stripMargin
}
// $COVERAGE-ON$
@tailrec private [parsley] def run[Err: ErrorBuilder, A](): Result[Err, A] = {
//println(pretty)
if (running) { // this is the likeliest branch, so should be executed with fewest comparisons
instrs(pc)(this)
run[Err, A]()
}
else if (good) {
assert(stack.size == 1, "stack must end a parse with exactly one item")
assert(calls.isEmpty, "there must be no more calls to unwind on end of parser")
assert(handlers.isEmpty, "there must be no more handlers on end of parse")
assert(checkStack.isEmpty, "there must be no residual check remaining on end of parse")
assert(states.isEmpty, "there must be no residual states left at end of parse")
assert(errs.isEmpty, "there should be no parse errors remaining at end of parse")
assert(hintStack.isEmpty, "there should be no hints remaining at end of parse")
Success(stack.peek[A])
}
else {
assert(!errs.isEmpty && errs.tail.isEmpty, "there should be exactly 1 parse error remaining at end of parse")
assert(handlers.isEmpty, "there must be no more handlers on end of parse")
assert(checkStack.isEmpty, "there must be no residual check remaining on end of parse")
assert(states.isEmpty, "there must be no residual states left at end of parse")
assert(hintStack.isEmpty, "there should be no hints remaining at end of parse")
Failure(errs.error.asParseError.format(sourceFile))
}
}
private [machine] def call(newInstrs: Array[Instr]): Unit = {
call(0)
instrs = newInstrs
}
private [machine] def call(at: Int): Unit = {
calls = new CallStack(pc + 1, instrs, at, calls)
pc = at
}
private [machine] def ret(): Unit = {
assert(calls != null, "cannot return when no calls are made")
instrs = calls.instrs
pc = calls.ret
calls = calls.tail
}
private [machine] def catchNoConsumed(handler: =>Unit): Unit = {
assert(!good, "catching can only be performed in a handler")
if (offset != checkStack.offset) fail()
else {
good = true
handler
}
checkStack = checkStack.tail
}
private [machine] def pushError(err: DefuncError): Unit = this.errs = new ErrorStack(this.useHints(err), this.errs)
private [machine] def useHints(err: DefuncError): DefuncError = {
if (hintsValidOffset == err.offset) err.withHints(hints)
else {
hintsValidOffset = err.offset
hints = EmptyHints
err
}
}
private [machine] def failWithMessage(caretWidth: Int, msgs: String*): Unit = this.fail(new ClassicFancyError(offset, line, col, caretWidth, msgs: _*))
private [machine] def unexpectedFail(expected: Option[ExpectItem], unexpected: UnexpectDesc): Unit = {
this.fail(new ClassicUnexpectedError(offset, line, col, expected, unexpected))
}
/*private [machine] def expectedFail(expected: Option[ExpectItem], reason: String, size: Int): Unit = {
this.fail(new ClassicExpectedErrorWithReason(offset, line, col, expected, reason, size))
}*/
private [machine] def expectedFail(expected: Option[ExpectItem], unexpectedWidth: Int): Unit = {
this.fail(new ClassicExpectedError(offset, line, col, expected, unexpectedWidth))
}
private [machine] def fail(error: DefuncError): Unit = {
good = false
this.pushError(error)
this.fail()
}
private [machine] def fail(): Unit = {
assert(!good, "fail() may only be called in a failing context, use `fail(err)` or set `good = false`")
if (handlers.isEmpty) running = false
else {
val handler = handlers
handlers = handlers.tail
instrs = handler.instrs
calls = handler.calls
pc = handler.pc
val diffstack = stack.usize - handler.stacksz
if (diffstack > 0) stack.drop(diffstack)
}
}
private [machine] def pushAndContinue(x: Any) = {
stack.push(x)
inc()
}
private [machine] def unsafePushAndContinue(x: Any) = {
stack.upush(x)
inc()
}
private [machine] def exchangeAndContinue(x: Any) = {
stack.exchange(x)
inc()
}
private [machine] def inc(): Unit = pc += 1
private [machine] def nextChar: Char = input.charAt(offset)
private [machine] def moreInput: Boolean = offset < inputsz
private [machine] def updatePos(c: Char) = c match {
case '\n' => line += 1; col = 1
case '\t' => col = ((col + 3) & -4) | 1//((col - 1) | 3) + 2 // scalastyle:ignore magic.number
case _ => col += 1
}
private [machine] def consumeChar(): Char = {
val c = nextChar
updatePos(c)
offset += 1
c
}
private [machine] def fastUncheckedConsumeChars(n: Int) = {
offset += n
col += n
}
private [machine] def pushHandler(label: Int): Unit = handlers = new HandlerStack(calls, instrs, label, stack.usize, handlers)
private [machine] def pushCheck(): Unit = checkStack = new CheckStack(offset, checkStack)
private [machine] def saveState(): Unit = states = new StateStack(offset, line, col, states)
private [machine] def restoreState(): Unit = {
val state = states
states = states.tail
offset = state.offset
line = state.line
col = state.col
}
private [machine] def writeReg(reg: Int, x: Any): Unit = {
regs(reg) = x.asInstanceOf[AnyRef]
}
private [machine] def status: Status = {
if (running) if (good) Good else Recover
else if (good) Finished else Failed
}
private implicit val lineBuilder: LineBuilder = new LineBuilder {
def nearestNewlineBefore(off: Int): Option[Int] = {
if (off < 0) None
else Some {
val idx = Context.this.input.lastIndexOf('\n', off-1)
if (idx == -1) 0 else idx + 1
}
}
def nearestNewlineAfter(off: Int): Option[Int] = {
if (off > Context.this.inputsz) None
else Some {
val idx = Context.this.input.indexOf('\n', off)
if (idx == -1) Context.this.inputsz else idx
}
}
def segmentBetween(start: Int, end: Int): String = {
Context.this.input.substring(start, end)
}
}
private [machine] implicit val errorItemBuilder: ErrorItemBuilder = new ErrorItemBuilder {
def inRange(offset: Int): Boolean = offset < Context.this.inputsz
def codePointAt(offset: Int): Int = Context.this.input.codePointAt(offset)
//def substring(offset: Int, size: Int): String = Context.this.input.substring(offset, Math.min(offset + size, Context.this.inputsz))
def iterableFrom(offset: Int): IndexedSeq[Char] = Context.this.input.substring(offset)
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy