diff --git a/parsley/shared/src/main/scala/parsley/Parsley.scala b/parsley/shared/src/main/scala/parsley/Parsley.scala index 84698ea89..f3bfa02d1 100644 --- a/parsley/shared/src/main/scala/parsley/Parsley.scala +++ b/parsley/shared/src/main/scala/parsley/Parsley.scala @@ -11,6 +11,7 @@ import parsley.combinator.option import parsley.errors.ErrorBuilder import parsley.expr.{chain, infix} +import parsley.internal.diagnostics.UserException import parsley.internal.deepembedding.{frontend, singletons} import parsley.internal.machine.Context @@ -128,7 +129,14 @@ final class Parsley[+A] private [parsley] (private [parsley] val internal: front * @since 3.0.0 * @group run */ - def parse[Err: ErrorBuilder](input: String): Result[Err, A] = new Context(internal.instrs, input, internal.numRegs, None).run() + def parse[Err: ErrorBuilder](input: String): Result[Err, A] = { + try new Context(internal.instrs, input, internal.numRegs, None).run() + catch { + // $COVERAGE-OFF$ + case UserException(err) => throw err // scalastyle:ignore throw + // $COVERAGE-ON$ + } + } // RESULT CHANGING COMBINATORS /** This combinator allows the result of this parser to be changed using a given function. diff --git a/parsley/shared/src/main/scala/parsley/exceptions/CorruptedReferenceException.scala b/parsley/shared/src/main/scala/parsley/exceptions/CorruptedReferenceException.scala new file mode 100644 index 000000000..584b487a6 --- /dev/null +++ b/parsley/shared/src/main/scala/parsley/exceptions/CorruptedReferenceException.scala @@ -0,0 +1,11 @@ +/* + * Copyright 2020 Parsley Contributors + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package parsley.exceptions + +// $COVERAGE-OFF$ +private [parsley] class CorruptedReferenceException + extends ParsleyException("A reference has been used across two different parsers in separate calls to parse, causing it to be misallocated") +// $COVERAGE-ON$ diff --git a/parsley/shared/src/main/scala/parsley/exceptions/UnfilledRegisterException.scala b/parsley/shared/src/main/scala/parsley/exceptions/UnfilledReferenceException.scala similarity index 58% rename from parsley/shared/src/main/scala/parsley/exceptions/UnfilledRegisterException.scala rename to parsley/shared/src/main/scala/parsley/exceptions/UnfilledReferenceException.scala index edef50748..82760354e 100644 --- a/parsley/shared/src/main/scala/parsley/exceptions/UnfilledRegisterException.scala +++ b/parsley/shared/src/main/scala/parsley/exceptions/UnfilledReferenceException.scala @@ -6,6 +6,6 @@ package parsley.exceptions // $COVERAGE-OFF$ -private [parsley] class UnfilledRegisterException - extends ParsleyException("A parser uses a register that has not been initialised by a `put`") +private [parsley] class UnfilledReferenceException + extends ParsleyException("A parser uses a reference that has not been initialised by a `set`") // $COVERAGE-ON$ diff --git a/parsley/shared/src/main/scala/parsley/internal/deepembedding/frontend/LazyParsley.scala b/parsley/shared/src/main/scala/parsley/internal/deepembedding/frontend/LazyParsley.scala index 2f00887c5..8b9c5557b 100644 --- a/parsley/shared/src/main/scala/parsley/internal/deepembedding/frontend/LazyParsley.scala +++ b/parsley/shared/src/main/scala/parsley/internal/deepembedding/frontend/LazyParsley.scala @@ -9,11 +9,11 @@ import scala.annotation.nowarn import scala.collection.mutable import parsley.XAssert._ -import parsley.exceptions.BadLazinessException import parsley.state.Ref import parsley.internal.deepembedding.{Cont, ContOps, Id}, ContOps.{perform, result, ContAdapter} import parsley.internal.deepembedding.backend, backend.StrictParsley +import parsley.internal.diagnostics.NullParserException import parsley.internal.machine.instructions, instructions.Instr /** This is the root type of the parsley "frontend": it represents a combinator tree @@ -135,7 +135,6 @@ private [parsley] abstract class LazyParsley[+A] private [deepembedding] { * @param seen the set of all nodes that have previously been seen by the let-finding * @param state stores all the information of the let-finding process */ - @throws[BadLazinessException]("if this parser references another parser before it has been initialised") final protected [frontend] def findLets[M[_, +_]: ContOps, R](seen: Set[LazyParsley[_]])(implicit state: LetFinderState): M[R, Unit] = { state.addPred(this) if (seen.contains(this)) result(state.addRec(this)) @@ -148,7 +147,7 @@ private [parsley] abstract class LazyParsley[+A] private [deepembedding] { try findLetsAux(seen + this) catch { // $COVERAGE-OFF$ - case _: NullPointerException => throw new BadLazinessException // scalastyle:ignore throw + case NullParserException(err) => throw err // scalastyle:ignore throw // $COVERAGE-ON$ } } diff --git a/parsley/shared/src/main/scala/parsley/internal/diagnostics.scala b/parsley/shared/src/main/scala/parsley/internal/diagnostics.scala new file mode 100644 index 000000000..d75ab09ca --- /dev/null +++ b/parsley/shared/src/main/scala/parsley/internal/diagnostics.scala @@ -0,0 +1,51 @@ +/* + * Copyright 2020 Parsley Contributors + * + * SPDX-License-Identifier: BSD-3-Clause + */ +package parsley.internal.diagnostics + +import parsley.exceptions.{BadLazinessException, CorruptedReferenceException, ParsleyException} + +private [parsley] object UserException { + def unapply(e: Throwable): Option[Throwable] = e match { + case _: ParsleyException => None + case e if userStackTrace(e.getStackTrace) => + e.setStackTrace(pruneParsley(e.getStackTrace)) + Some(e) + case _ => None + } + + def userStackTrace(e: Array[StackTraceElement]) = e.view.takeWhile(!_.getClassName.startsWith("parsley.internal")).exists { ste => + !ste.getClassName.startsWith("scala") || !ste.getClassName.startsWith("java") + } + def pruneParsley(e: Array[StackTraceElement]): Array[StackTraceElement] = { + val (userBits, parsleyTrace) = e.span(!_.getClassName.startsWith("parsley.internal")) + userBits ++ parsleyTrace.dropWhile(_.getClassName.startsWith("parsley.internal")) + } +} + +private [parsley] object RegisterOutOfBoundsException { + def unapply(e: Throwable): Option[Throwable] = e match { + case e: ArrayIndexOutOfBoundsException => e.getStackTrace.headOption.collect { + // this exception was thrown plainly during the execution of an instruction + // only register arrays are accessed raw like this: therefore it must be an + // out of bounds register. + case ste if ste.getMethodName == "apply" + && ste.getClassName.startsWith("parsley.internal.machine.instructions") => + val err = new CorruptedReferenceException + err.addSuppressed(e) + err + } + case _ => None + } +} + +private [parsley] object NullParserException { + def unapply(e: Throwable): Option[Throwable] = e match { + // this should only be true when the null was tripped from within the parsley namespace, + // not the user one + case e: NullPointerException if !UserException.userStackTrace(e.getStackTrace) => Some(new BadLazinessException) + case _ => None + } +} diff --git a/parsley/shared/src/main/scala/parsley/internal/machine/Context.scala b/parsley/shared/src/main/scala/parsley/internal/machine/Context.scala index 4d2465072..9f4411671 100644 --- a/parsley/shared/src/main/scala/parsley/internal/machine/Context.scala +++ b/parsley/shared/src/main/scala/parsley/internal/machine/Context.scala @@ -14,6 +14,7 @@ import parsley.Success import parsley.XAssert._ import parsley.errors.ErrorBuilder +import parsley.internal.diagnostics.RegisterOutOfBoundsException import parsley.internal.errors.{CaretWidth, ExpectItem, LineBuilder, UnexpectDesc} import parsley.internal.machine.errors.{ClassicFancyError, DefuncError, DefuncHints, EmptyHints, ErrorItemBuilder, ExpectedError, ExpectedErrorWithReason, UnexpectedError} @@ -124,11 +125,20 @@ private [parsley] final class Context(private [machine] var instrs: Array[Instr] } // $COVERAGE-ON$ - @tailrec private [parsley] def run[Err: ErrorBuilder, A](): Result[Err, A] = { + private [parsley] def run[Err: ErrorBuilder, A](): Result[Err, A] = { + try go[Err, A]() + catch { + // additional diagnostic checks + // $COVERAGE-OFF$ + case RegisterOutOfBoundsException(err) => throw err // scalastyle:ignore throw + // $COVERAGE-ON$ + } + } + @tailrec private def go[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]() + go[Err, A]() } else if (good) { assert(stack.size == 1, s"stack must end a parse with exactly one item, it has ${stack.size}") diff --git a/parsley/shared/src/main/scala/parsley/internal/machine/instructions/PrimitiveInstrs.scala b/parsley/shared/src/main/scala/parsley/internal/machine/instructions/PrimitiveInstrs.scala index 47e514b01..ef11d72c0 100644 --- a/parsley/shared/src/main/scala/parsley/internal/machine/instructions/PrimitiveInstrs.scala +++ b/parsley/shared/src/main/scala/parsley/internal/machine/instructions/PrimitiveInstrs.scala @@ -12,8 +12,6 @@ import parsley.internal.errors.ExpectDesc import parsley.internal.machine.Context import parsley.internal.machine.XAssert._ -import org.typelevel.scalaccompat.annotation.nowarn3 - private [internal] final class Satisfies(f: Char => Boolean, expected: Iterable[ExpectDesc]) extends Instr { def this(f: Char => Boolean, expected: LabelConfig) = this(f, expected.asExpectDescs) override def apply(ctx: Context): Unit = { @@ -157,7 +155,7 @@ private [internal] object Span extends Instr { } // This instruction holds mutate state, but it is safe to do so, because it's always the first instruction of a DynCall. -private [parsley] final class CalleeSave(var label: Int, localRegs: Set[Ref[_]] @nowarn3, reqSize: Int, slots: List[(Int, Int)], saveArray: Array[AnyRef]) +private [parsley] final class CalleeSave(var label: Int, localRegs: Set[Ref[_]], reqSize: Int, slots: List[(Int, Int)], saveArray: Array[AnyRef]) extends InstrWithLabel { private def this(label: Int, localRegs: Set[Ref[_]], reqSize: Int, slots: List[Int]) = this(label, localRegs, reqSize, slots.zipWithIndex, new Array[AnyRef](slots.length)) @@ -190,7 +188,8 @@ private [parsley] final class CalleeSave(var label: Int, localRegs: Set[Ref[_]] saveArray(idx) = null } // This is the only way to get them reallocated on the next invocation - localRegs.foreach(_.deallocate()): @nowarn3 + // FIXME: I think this isn't thread-safe, because two flatMaps can simulataneously reallocate? + localRegs.foreach(_.deallocate()) } private def continue(ctx: Context): Unit = { diff --git a/parsley/shared/src/main/scala/parsley/state.scala b/parsley/shared/src/main/scala/parsley/state.scala index 5eeb6cd12..a5886b551 100644 --- a/parsley/shared/src/main/scala/parsley/state.scala +++ b/parsley/shared/src/main/scala/parsley/state.scala @@ -10,7 +10,7 @@ import scala.collection.Factory import parsley.XAssert._ import parsley.combinator.{whenS, whileS} import parsley.syntax.zipped.Zipped2 -import parsley.exceptions.UnfilledRegisterException +import parsley.exceptions.UnfilledReferenceException import parsley.internal.deepembedding.{frontend, singletons} @@ -277,7 +277,7 @@ object state { private [this] var _v: Int = -1 private [parsley] def addr: Int = { - if (!allocated) throw new UnfilledRegisterException // scalastyle:ignore throw + if (!allocated) throw new UnfilledReferenceException // scalastyle:ignore throw _v } private [parsley] def allocated: Boolean = _v != -1 @@ -285,8 +285,10 @@ object state { assert(!allocated) this._v = v } - // This must ONLY be used by CalleeSave in flatMap - private [parsley] def deallocate(): Unit = _v = -1 + private [parsley] def deallocate(): Unit = { + assert((new Throwable).getStackTrace.exists(_.getClassName == "parsley.internal.machine.instructions.CalleeSave")) + _v = -1 + } //override def toString: String = s"Reg(${if (allocated) addr else "unallocated"})" } diff --git a/parsley/shared/src/test/scala/parsley/token/SpaceTests.scala b/parsley/shared/src/test/scala/parsley/token/SpaceTests.scala index 7ca2e6ee0..7adf71a3c 100644 --- a/parsley/shared/src/test/scala/parsley/token/SpaceTests.scala +++ b/parsley/shared/src/test/scala/parsley/token/SpaceTests.scala @@ -12,7 +12,7 @@ import parsley.Parsley.atomic import descriptions.{SpaceDesc, LexicalDesc} import parsley.character.{string, char} -import parsley.exceptions.UnfilledRegisterException +import parsley.exceptions.UnfilledReferenceException import parsley.VanillaError import parsley.Failure import parsley.TestError @@ -231,7 +231,7 @@ class SpaceTests extends ParsleyTest { val basicDependent = basicMixed.copy(whitespaceIsContextDependent = true) "context-dependent whitespace" must "be initialised" in { - a [UnfilledRegisterException] must be thrownBy { + a [UnfilledReferenceException] must be thrownBy { makeSpace(basicDependent).whiteSpace.parse(" ") } }