diff --git a/parsley/shared/src/main/scala/parsley/internal/errors/CaretWidth.scala b/parsley/shared/src/main/scala/parsley/internal/errors/CaretWidth.scala index 4ac5a5d91..4afbd54c8 100644 --- a/parsley/shared/src/main/scala/parsley/internal/errors/CaretWidth.scala +++ b/parsley/shared/src/main/scala/parsley/internal/errors/CaretWidth.scala @@ -5,6 +5,11 @@ package parsley.internal.errors private [parsley] sealed abstract class CaretWidth { def width: Int + def isFlexible: Boolean +} +private [parsley] class FlexibleCaret(val width: Int) extends CaretWidth { + def isFlexible: Boolean = true +} +private [parsley] class RigidCaret(val width: Int) extends CaretWidth { + def isFlexible: Boolean = false } -private [parsley] class FlexibleCaret(val width: Int) extends CaretWidth -private [parsley] class RigidCaret(val width: Int) extends CaretWidth diff --git a/parsley/shared/src/main/scala/parsley/internal/machine/errors/DefuncBuilders.scala b/parsley/shared/src/main/scala/parsley/internal/machine/errors/DefuncBuilders.scala index 9c30d08f7..af1cc4bd4 100644 --- a/parsley/shared/src/main/scala/parsley/internal/machine/errors/DefuncBuilders.scala +++ b/parsley/shared/src/main/scala/parsley/internal/machine/errors/DefuncBuilders.scala @@ -5,7 +5,9 @@ package parsley.internal.machine.errors import scala.collection.mutable -import parsley.internal.errors.{EndOfInput, ExpectItem, FancyError, TrivialError, UnexpectDesc, UnexpectItem} +import parsley.XAssert._ + +import parsley.internal.errors.{CaretWidth, EndOfInput, ExpectItem, FancyError, TrivialError, UnexpectDesc, UnexpectItem} import TrivialErrorBuilder.{BuilderUnexpectItem, NoItem, Other, Raw} @@ -145,6 +147,7 @@ private [errors] final class FancyErrorBuilder(offset: Int, lexicalError: Boolea private var line: Int = _ private var col: Int = _ private var caretWidth: Int = 0 + private var flexibleCaret: Boolean = true private val msgs = mutable.ListBuffer.empty[String] /** Updates the position of the error message. @@ -157,7 +160,19 @@ private [errors] final class FancyErrorBuilder(offset: Int, lexicalError: Boolea this.col = col } - def updateCaretWidth(width: Int): Unit = this.caretWidth = math.max(this.caretWidth, width) + def updateCaretWidth(width: Int): Unit = { + assume(flexibleCaret, "if we are updating direct from a TrivialError, we better be flexible!") + this.caretWidth = math.max(this.caretWidth, width) + } + def updateCaretWidth(width: CaretWidth): Unit = { + // if they match, just take the max + if (width.isFlexible == this.flexibleCaret) this.caretWidth = math.max(this.caretWidth, width.width) + // if they don't match and we are rigid, then we override the flexible caret, otherwise do nothing + else if (!width.isFlexible) { + this.caretWidth = width.width + this.flexibleCaret = false + } + } /** Adds a collection of new error message lines into this error. * diff --git a/parsley/shared/src/main/scala/parsley/internal/machine/errors/DefuncError.scala b/parsley/shared/src/main/scala/parsley/internal/machine/errors/DefuncError.scala index 26152d56e..9954ad915 100644 --- a/parsley/shared/src/main/scala/parsley/internal/machine/errors/DefuncError.scala +++ b/parsley/shared/src/main/scala/parsley/internal/machine/errors/DefuncError.scala @@ -30,6 +30,8 @@ private [parsley] sealed abstract class DefuncError { private [machine] final def entrenched: Boolean = (flags & DefuncError.EntrenchedMask) != 0 /** Is this error created while parsing a lexical token? */ private [machine] final def lexicalError: Boolean = (flags & DefuncError.LexicalErrorMask) != 0 + /** Is the caret on this error flexible (only relevant for fancy errors) **/ + private [machine] final def flexibleCaret: Boolean = (flags & DefuncError.FlexibleCaretMask) != 0 /** The offset at which this error occured */ private [machine] val offset: Int /** This function forces the lazy defunctionalised structure into a final `ParseError` value. */ @@ -117,6 +119,7 @@ private [errors] object DefuncError { private [errors] final val ExpectedEmptyMask: Byte = 1 << 1 private [errors] final val EntrenchedMask: Byte = 1 << 2 private [errors] final val LexicalErrorMask: Byte = 1 << 3 + private [errors] final val FlexibleCaretMask: Byte = 1 << 4 } /** Represents partially evaluated trivial errors */ @@ -137,23 +140,26 @@ private [errors] sealed abstract class TrivialDefuncError extends DefuncError { * @param state the hint state that is collecting up the expected items * @note this function should be tail-recursive! */ - // TODO: Factor all the duplicated cases out? private [errors] final def collectHints(collector: HintCollector): Unit = this match { - case self: BaseError => + case self: BaseError => collector ++= self.expectedIterable collector.updateWidth(self.unexpectedWidth) - case self: WithLabel => if (self.label.nonEmpty) collector += ExpectDesc(self.label) - case self: WithReason => self.err.collectHints(collector) - case self: WithHints => + case self: WithLabel => if (self.label.nonEmpty) collector += ExpectDesc(self.label) + case self: WithHints => self.hints.collect(collector) self.err.collectHints(collector) + case self: TrivialTransitive => self.err.collectHints(collector) // the WithLabel and WithHint cases must be above case self: TrivialMergedErrors => self.err1.collectHints(collector) self.err2.collectHints(collector) - case self: TrivialAmended => self.err.collectHints(collector) - case self: TrivialEntrenched => self.err.collectHints(collector) - case self: TrivialDislodged => self.err.collectHints(collector) - case self: TrivialLexical => self.err.collectHints(collector) + } + + private [errors] final def adjustCaret(builder: FancyErrorBuilder): Unit = this match { + case self: BaseError => builder.updateCaretWidth(self.unexpectedWidth) + case self: TrivialTransitive => self.err.adjustCaret(builder) + case self: TrivialMergedErrors => + self.err1.adjustCaret(builder) + self.err2.adjustCaret(builder) } private [machine] final override def merge(err: DefuncError): DefuncError = { @@ -209,8 +215,9 @@ private [errors] sealed abstract class FancyDefuncError extends DefuncError { if (cmp > 0) this else if (cmp < 0) err else err match { - case err: FancyDefuncError => new FancyMergedErrors(this, err) - case _ => this + case err: FancyDefuncError => new FancyMergedErrors(this, err) + case err: TrivialDefuncError if flexibleCaret => new FancyAdjustCaret(this, err) + case _ => this } } @@ -237,6 +244,9 @@ private [errors] sealed abstract class FancyDefuncError extends DefuncError { } } +private [errors] sealed abstract class TrivialTransitive extends TrivialDefuncError { + val err: TrivialDefuncError +} /** This is the common supertype of all "regular" trivial errors: those that result from failures as opposed to operations on errors. */ private [errors] sealed abstract class BaseError extends TrivialDefuncError { /** The line number the error occurred at */ @@ -245,7 +255,7 @@ private [errors] sealed abstract class BaseError extends TrivialDefuncError { private [errors] val col: Int /** The size of the unexpected token demanded by this error */ private [errors] def unexpectedWidth: Int - // def expected: IterableOnce[ErrorItem] // TODO: when 2.12 is dropped this will work better + // private [errors] def expected: IterableOnce[ErrorItem] // TODO: when 2.12 is dropped this will work better /** The error items produced by this error */ private [errors] def expectedIterable: Iterable[ExpectItem] @@ -295,11 +305,12 @@ private [parsley] final class ClassicUnexpectedError(val offset: Int, val line: } private [parsley] final class ClassicFancyError(val offset: Int, val line: Int, val col: Int, caretWidth: CaretWidth, val msgs: String*) extends FancyDefuncError { - override final val flags = DefuncError.ExpectedEmptyMask + override final val flags = + if (caretWidth.isFlexible) (DefuncError.ExpectedEmptyMask | DefuncError.FlexibleCaretMask).toByte else DefuncError.ExpectedEmptyMask override def makeFancy(builder: FancyErrorBuilder): Unit = { builder.pos_=(line, col) builder ++= msgs - builder.updateCaretWidth(caretWidth.width) + builder.updateCaretWidth(caretWidth) } } private [parsley] final class EmptyError(val offset: Int, val line: Int, val col: Int, val unexpectedWidth: Int) extends BaseError { @@ -346,7 +357,17 @@ private [errors] final class FancyMergedErrors private [errors] (val err1: Fancy } } -private [errors] final class WithHints private [errors] (val err: TrivialDefuncError, val hints: DefuncHints) extends TrivialDefuncError { +private [errors] final class FancyAdjustCaret private [errors] (val err: FancyDefuncError, val caretAdjuster: TrivialDefuncError) extends FancyDefuncError { + override final val flags = err.flags + assume(err.offset == caretAdjuster.offset, "two errors only merge when they have matching offsets") + override val offset = err.offset + override def makeFancy(builder: FancyErrorBuilder): Unit = { + err.makeFancy(builder) + caretAdjuster.adjustCaret(builder) + } +} + +private [errors] final class WithHints private [errors] (val err: TrivialDefuncError, val hints: DefuncHints) extends TrivialTransitive { assume(!hints.isEmpty, "WithHints will always have non-empty hints") override final val flags = (err.flags & ~DefuncError.ExpectedEmptyMask).toByte //err.isExpectedEmpty && hints.isEmpty override val offset = err.offset @@ -358,7 +379,7 @@ private [errors] final class WithHints private [errors] (val err: TrivialDefuncE } } -private [errors] final class WithReason private [errors] (val err: TrivialDefuncError, val reason: String) extends TrivialDefuncError { +private [errors] final class WithReason private [errors] (val err: TrivialDefuncError, val reason: String) extends TrivialTransitive { override final val flags = err.flags val offset = err.offset override def makeTrivial(builder: TrivialErrorBuilder): Unit = { @@ -367,7 +388,7 @@ private [errors] final class WithReason private [errors] (val err: TrivialDefunc } } -private [errors] final class WithLabel private [errors] (val err: TrivialDefuncError, val label: String) extends TrivialDefuncError { +private [errors] final class WithLabel private [errors] (val err: TrivialDefuncError, val label: String) extends TrivialTransitive { override final val flags = { if (label.isEmpty) (err.flags | DefuncError.ExpectedEmptyMask).toByte else (err.flags & ~DefuncError.ExpectedEmptyMask).toByte @@ -382,7 +403,7 @@ private [errors] final class WithLabel private [errors] (val err: TrivialDefuncE } private [errors] final class TrivialAmended private [errors] (val offset: Int, val line: Int, val col: Int, val err: TrivialDefuncError) - extends TrivialDefuncError { + extends TrivialTransitive { assume(!err.entrenched, "an amendment will only occur on unentrenched errors") override final val flags = err.flags override def makeTrivial(builder: TrivialErrorBuilder): Unit = { @@ -401,13 +422,13 @@ private [errors] final class FancyAmended private [errors] (val offset: Int, val } } -private [errors] final class TrivialEntrenched private [errors] (val err: TrivialDefuncError) extends TrivialDefuncError { +private [errors] final class TrivialEntrenched private [errors] (val err: TrivialDefuncError) extends TrivialTransitive { override final val flags = (err.flags | DefuncError.EntrenchedMask).toByte override val offset = err.offset override def makeTrivial(builder: TrivialErrorBuilder): Unit = err.makeTrivial(builder) } -private [errors] final class TrivialDislodged private [errors] (val err: TrivialDefuncError) extends TrivialDefuncError { +private [errors] final class TrivialDislodged private [errors] (val err: TrivialDefuncError) extends TrivialTransitive { override final val flags = (err.flags & ~DefuncError.EntrenchedMask).toByte override val offset = err.offset override def makeTrivial(builder: TrivialErrorBuilder): Unit = err.makeTrivial(builder) @@ -425,7 +446,7 @@ private [errors] final class FancyDislodged private [errors] (val err: FancyDefu override def makeFancy(builder: FancyErrorBuilder): Unit = err.makeFancy(builder) } -private [errors] final class TrivialLexical private [errors] (val err: TrivialDefuncError) extends TrivialDefuncError { +private [errors] final class TrivialLexical private [errors] (val err: TrivialDefuncError) extends TrivialTransitive { override final val flags = (err.flags | DefuncError.LexicalErrorMask).toByte override val offset = err.offset override def makeTrivial(builder: TrivialErrorBuilder): Unit = err.makeTrivial(builder) diff --git a/parsley/shared/src/test/scala/parsley/internal/machine/errors/DefuncErrorTests.scala b/parsley/shared/src/test/scala/parsley/internal/machine/errors/DefuncErrorTests.scala index 4587e5270..32d64f3d4 100644 --- a/parsley/shared/src/test/scala/parsley/internal/machine/errors/DefuncErrorTests.scala +++ b/parsley/shared/src/test/scala/parsley/internal/machine/errors/DefuncErrorTests.scala @@ -5,7 +5,7 @@ package parsley.internal.machine.errors import parsley.ParsleyTest -import parsley.internal.errors.{TrivialError, FancyError, ExpectRaw, ExpectDesc, EndOfInput, RigidCaret, UnexpectDesc} +import parsley.internal.errors.{TrivialError, FancyError, FlexibleCaret, ExpectRaw, ExpectDesc, EndOfInput, RigidCaret, UnexpectDesc} import MockedBuilders.mockedErrorItemBuilder @@ -85,15 +85,20 @@ class DefuncErrorTests extends ParsleyTest { err2.isTrivialError shouldBe true err2.asParseError shouldBe a [TrivialError] } - // TODO: tests for flexible carets they should "be a fancy error in any other case" in { - val err1 = new EmptyError(0, 0, 0, 0).merge(new ClassicFancyError(0, 0, 0, new RigidCaret(1), "")) + val err1 = new EmptyError(0, 0, 0, 4).merge(new ClassicFancyError(0, 0, 0, new RigidCaret(1), "")) err1.isTrivialError shouldBe false - err1.asParseError shouldBe a [FancyError] + err1.flexibleCaret shouldBe false + val perr1 = err1.asParseError + perr1 shouldBe a [FancyError] + perr1.asInstanceOf[FancyError].caretWidth shouldBe 1 - val err2 = new ClassicFancyError(0, 0, 0, new RigidCaret(1), "").merge(new EmptyError(0, 0, 0, 0)) + val err2 = new ClassicFancyError(0, 0, 0, new FlexibleCaret(1), "").merge(new EmptyError(0, 0, 0, 4)) err2.isTrivialError shouldBe false - err2.asParseError shouldBe a [FancyError] + err2.flexibleCaret shouldBe true + val perr2 = err2.asParseError + perr2 shouldBe a [FancyError] + perr2.asInstanceOf[FancyError].caretWidth shouldBe 4 val err3 = new EmptyError(0, 0, 0, 0).merge(new ClassicFancyError(1, 0, 0, new RigidCaret(1), "")) err3.isTrivialError shouldBe false @@ -103,9 +108,12 @@ class DefuncErrorTests extends ParsleyTest { err4.isTrivialError shouldBe false err4.asParseError shouldBe a [FancyError] - val err5 = new ClassicFancyError(0, 0, 0, new RigidCaret(1), "").merge(new ClassicFancyError(0, 0, 0, new RigidCaret(1), "")) + val err5 = new ClassicFancyError(0, 0, 0, new FlexibleCaret(3), "").merge(new ClassicFancyError(0, 0, 0, new RigidCaret(1), "")) err5.isTrivialError shouldBe false - err5.asParseError shouldBe a [FancyError] + err5.flexibleCaret shouldBe false + val perr5 = err5.asParseError + perr5 shouldBe a [FancyError] + perr5.asInstanceOf[FancyError].caretWidth shouldBe 1 val err6 = new ClassicFancyError(1, 0, 0, new RigidCaret(1), "").merge(new ClassicFancyError(0, 0, 0, new RigidCaret(1), "")) err6.isTrivialError shouldBe false