diff --git a/.github/PULL_REQUEST_TEMPLATE/version_staging.md b/.github/PULL_REQUEST_TEMPLATE/version_staging.md index 7f1601587..5f2bf90c2 100644 --- a/.github/PULL_REQUEST_TEMPLATE/version_staging.md +++ b/.github/PULL_REQUEST_TEMPLATE/version_staging.md @@ -40,7 +40,7 @@ None. * [ ] documentation checked to ensure no leakage of `private [parsley]` members or `parsley.internal`. ## Milestone Migration Guide -As each milestone release may choose to make binary incompatible changes, any necessary migration requires to get from one milestone to the next will be tracked here. +As each milestone release may choose to make binary incompatible changes, any necessary migration required to get from one milestone to the next will be tracked here. _Nothing to see here!_ diff --git a/README.md b/README.md index 4a6bef534..754d58569 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ Parsley is a fast and modern parser combinator library for Scala based loosely o Parsley is distributed on Maven Central, and can be added to your project via: ```scala -libraryDependencies += "com.github.j-mie6" %% "parsley" % "4.0.4" +libraryDependencies += "com.github.j-mie6" %% "parsley" % "4.1.0" ``` Documentation can be found [**here**](https://javadoc.io/doc/com.github.j-mie6/parsley_2.13/latest/index.html) @@ -154,7 +154,8 @@ _An exception to this policy is made for any version `3.x.y`, which reaches EoL | Version | Released On | EoL Status | |:-------:|:-------------------|:----------------------------| | `3.3.0` | January 7th 2022 | EoL reached | -| `4.0.0` | November 30th 2022 | Enjoying indefinite support | +| `4.0.0` | November 30th 2022 | EoL reached | +| `4.1.0` | January 18th 2023 | Enjoying indefinite support | ## Bug Reports [![Percentage of issues still open](https://isitmaintained.com/badge/open/j-mie6/Parsley.svg)](https://isitmaintained.com/project/j-mie6/Parsley "Percentage of issues still open") [![Maintainability](https://img.shields.io/codeclimate/maintainability/j-mie6/parsley)](https://codeclimate.com/github/j-mie6/Parsley) [![Test Coverage](https://img.shields.io/codeclimate/coverage-letter/j-mie6/parsley)](https://codeclimate.com/github/j-mie6/Parsley) diff --git a/build.sbt b/build.sbt index 3bc484720..2bb6e3214 100644 --- a/build.sbt +++ b/build.sbt @@ -15,7 +15,7 @@ val isInPublish = Option(System.getenv("GITHUB_JOB")).contains("publish") val releaseFlags = Seq("-Xdisable-assertions", "-opt:l:method,inline", "-opt-inline-from", "parsley.**", "-opt-warnings:at-inline-failed") inThisBuild(List( - tlBaseVersion := "4.0", + tlBaseVersion := "4.1", organization := "com.github.j-mie6", startYear := Some(2018), homepage := Some(url("https://github.com/j-mie6/parsley")), @@ -29,6 +29,15 @@ inThisBuild(List( mimaBinaryIssueFilters ++= Seq( ProblemFilters.exclude[Problem]("parsley.internal.*"), ProblemFilters.exclude[Problem]("parsley.X*"), + // Until 5.0 (these are all misreported package private members) + ProblemFilters.exclude[DirectMissingMethodProblem]("parsley.token.numeric.Combined.this"), + ProblemFilters.exclude[MissingClassProblem]("parsley.token.text.RawCharacter$"), + ProblemFilters.exclude[DirectMissingMethodProblem]("parsley.token.symbol.Symbol.this"), + ProblemFilters.exclude[ReversedMissingMethodProblem]("parsley.token.numeric.Integer.bounded"), + ProblemFilters.exclude[MissingClassProblem]("parsley.token.numeric.Generic$"), + ProblemFilters.exclude[MissingClassProblem]("parsley.token.predicate$_CharSet$"), + ProblemFilters.exclude[MissingFieldProblem]("parsley.token.predicate._CharSet"), + ProblemFilters.exclude[MissingClassProblem]("parsley.token.errors.ErrorConfig$"), ), tlVersionIntroduced := Map( "2.13" -> "1.5.0", diff --git a/parsley/shared/src/main/scala-2.12/parsley/XCompat.scala b/parsley/shared/src/main/scala-2.12/parsley/XCompat.scala index a6df723c5..dd9ccccb7 100644 --- a/parsley/shared/src/main/scala-2.12/parsley/XCompat.scala +++ b/parsley/shared/src/main/scala-2.12/parsley/XCompat.scala @@ -28,16 +28,6 @@ private [parsley] object XCompat { def mapValuesInPlaceCompat(f: (K, V) => V): mutable.Map[K, V] = m.transform(f) } - def codePoints(str: String): Iterator[Int] = new Iterator[Int] { - var idx = 0 - def hasNext: Boolean = idx < str.length - def next(): Int = { - val c = str.codePointAt(idx) - idx += Character.charCount(c) - c - } - } - @meta.getter @meta.setter class unused(message: String) extends scala.annotation.StaticAnnotation { def this() = this("") diff --git a/parsley/shared/src/main/scala-2.13+/parsley/XCompat.scala b/parsley/shared/src/main/scala-2.13+/parsley/XCompat.scala index af56c33c7..05cb8624e 100644 --- a/parsley/shared/src/main/scala-2.13+/parsley/XCompat.scala +++ b/parsley/shared/src/main/scala-2.13+/parsley/XCompat.scala @@ -19,7 +19,5 @@ private [parsley] object XCompat { def mapValuesInPlaceCompat(f: (K, V) => V): mutable.Map[K, V] = m.mapValuesInPlace(f) } - def codePoints(str: String): Iterator[Int] = str.codePointStepper.iterator - type unused = scala.annotation.unused } diff --git a/parsley/shared/src/main/scala/parsley/Parsley.scala b/parsley/shared/src/main/scala/parsley/Parsley.scala index 419d1add1..7a373fa70 100644 --- a/parsley/shared/src/main/scala/parsley/Parsley.scala +++ b/parsley/shared/src/main/scala/parsley/Parsley.scala @@ -1237,7 +1237,7 @@ object Parsley { * @return a parser that returns the line number the parser is currently at. * @group pos */ - val line: Parsley[Int] = new Parsley(singletons.Line) + def line: Parsley[Int] = position.line /** This parser returns the current column number of the input without having any other effect. * * When this combinator is ran, no input is required, nor consumed, and @@ -1258,7 +1258,7 @@ object Parsley { * @note in the presence of wide unicode characters, the value returned may be inaccurate. * @group pos */ - val col: Parsley[Int] = new Parsley(singletons.Col) + def col: Parsley[Int] = position.col /** This parser returns the current line and column numbers of the input without having any other effect. * * When this combinator is ran, no input is required, nor consumed, and @@ -1279,5 +1279,5 @@ object Parsley { * @note in the presence of wide unicode characters, the column value returned may be inaccurate. * @group pos */ - val pos: Parsley[(Int, Int)] = line <~> col + def pos: Parsley[(Int, Int)] = position.pos } diff --git a/parsley/shared/src/main/scala/parsley/character.scala b/parsley/shared/src/main/scala/parsley/character.scala index 43471de1d..1045a414e 100644 --- a/parsley/shared/src/main/scala/parsley/character.scala +++ b/parsley/shared/src/main/scala/parsley/character.scala @@ -8,7 +8,8 @@ import scala.collection.immutable.NumericRange import parsley.Parsley.{attempt, empty, fresh, pure} import parsley.combinator.{choice, skipMany} -import parsley.errors.combinator.ErrorMethods +import parsley.errors.combinator.{amend, ErrorMethods} +import parsley.token.errors.NotConfigured import parsley.internal.deepembedding.singletons @@ -99,7 +100,7 @@ object character { * @note this combinator can only handle 16-bit characters: for larger codepoints, consider using [[string `string`]]. * @group core */ - def char(c: Char): Parsley[Char] = new Parsley(new singletons.CharTok(c, None)) + def char(c: Char): Parsley[Char] = new Parsley(new singletons.CharTok(c, NotConfigured)) /** This combinator tries to parse a single specific codepoint `c` from the input. * @@ -123,7 +124,7 @@ object character { * @group core */ private [parsley] def charUtf16(c: Int): Parsley[Int] = { //TODO: release along with the utf combinators - if (Character.isBmpCodePoint(c)) char(c.toChar).map(_.toInt) + if (Character.isBmpCodePoint(c)) char(c.toChar) #> c else attempt(string(Character.toChars(c).mkString)) #> c } @@ -149,18 +150,26 @@ object character { * @note this combinator can only handle 16-bit characters. * @group core */ - def satisfy(pred: Char => Boolean): Parsley[Char] = new Parsley(new singletons.Satisfy(pred, None)) + def satisfy(pred: Char => Boolean): Parsley[Char] = new Parsley(new singletons.Satisfy(pred, NotConfigured)) // TODO: document and optimise - private [parsley] def satisfyUtf16(pred: Int => Boolean): Parsley[Int] = attempt { - item.flatMap { - case h if h.isHighSurrogate => item.collect { - case l if Character.isSurrogatePair(h, l) && pred(Character.toCodePoint(h, l)) => Character.toCodePoint(h, l) + private [parsley] def satisfyUtf16(pred: Int => Boolean): Parsley[Int] = amend { + attempt { + item.hide.flatMap { + case h if h.isHighSurrogate => + // Our policy is that the user can parse high-surrogates if they wish, it's just evil + /*item.collect { + case l if Character.isSurrogatePair(h, l) && pred(Character.toCodePoint(h, l)) => Character.toCodePoint(h, l) + }*/ + satisfy(l => Character.isSurrogatePair(h, l) && pred(Character.toCodePoint(h, l))).map(Character.toCodePoint(h, _)) <|> { + val c = h.toInt + if (pred(c)) pure(c) else empty + } + case c if pred(c.toInt) => pure(c.toInt) + case _ => empty } - case c if pred(c.toInt) => pure(c.toInt) - case _ => empty } - } + } <|> (satisfy(_ => false) *> empty) // I need an unexpected width of 1, and this is the only way I know how... sad times /** This combinator attempts to parse a given string from the input, and fails otherwise. * @@ -189,7 +198,7 @@ object character { */ def string(s: String): Parsley[String] = { require(s.nonEmpty, "`string` may not be passed the empty string (`string(\"\")` is meaningless, perhaps you meant `pure(\"\")`?)") - new Parsley(new singletons.StringTok(s, None)) + new Parsley(new singletons.StringTok(s, NotConfigured)) } /** $oneOf @@ -680,7 +689,7 @@ object character { * @see [[isHexDigit ``isHexDigit``]] * @group spec */ - val hexDigit: Parsley[Char] = satisfy(isHexDigit) + val hexDigit: Parsley[Char] = satisfy(isHexDigit).label("hexdecimal digit") /** This parser tries to parse an octal digit, and returns it if successful. * diff --git a/parsley/shared/src/main/scala/parsley/errors/combinator.scala b/parsley/shared/src/main/scala/parsley/errors/combinator.scala index 8f22244bc..1357299a8 100644 --- a/parsley/shared/src/main/scala/parsley/errors/combinator.scala +++ b/parsley/shared/src/main/scala/parsley/errors/combinator.scala @@ -3,7 +3,7 @@ */ package parsley.errors -import parsley.Parsley +import parsley.Parsley, Parsley.attempt import parsley.internal.deepembedding.{frontend, singletons} @@ -112,7 +112,7 @@ object combinator { * @since 3.1.0 * @group adj */ - def amend[A](p: Parsley[A]): Parsley[A] = new Parsley(new frontend.ErrorAmend(p.internal)) + def amend[A](p: Parsley[A]): Parsley[A] = new Parsley(new frontend.ErrorAmend(p.internal, partial = false)) /** This combinator prevents the action of any enclosing `amend` on the errors generated by the given * parser. @@ -141,6 +141,12 @@ object combinator { */ def entrench[A](p: Parsley[A]): Parsley[A] = new Parsley(new frontend.ErrorEntrench(p.internal)) + // TODO: Documentation and testing ahead of future release + private [parsley] def dislodge[A](p: Parsley[A]): Parsley[A] = new Parsley(new frontend.ErrorDislodge(p.internal)) + private [parsley] def amendThenDislodge[A](p: Parsley[A]): Parsley[A] = dislodge(amend(p)) + private [parsley] def partialAmend[A](p: Parsley[A]): Parsley[A] = new Parsley(new frontend.ErrorAmend(p.internal, partial = true)) + private [parsley] def partialAmendThenDislodge[A](p: Parsley[A]): Parsley[A] = dislodge(partialAmend(p)) + /** This combinator marks any errors within the given parser as being ''lexical errors''. * * When an error is marked as a ''lexical error'', it sets a flag within the error that is @@ -437,6 +443,21 @@ object combinator { */ def hide: Parsley[A] = this.label("") + // TODO: move all of these to a `VerifiedErrorWidgets` class? + // TODO: it should have the partial amend semantics, because `amendAndDislodge` can restore the other semantics anyway + // Document that `attempt` may be used when this is an informative but not terminal error. + private [parsley] def fail(msggen: A => Seq[String]): Parsley[Nothing] = { + // holy hell, the hoops I jump through to be able to implement things + val r = parsley.registers.Reg.make[(Int, A, Int)] + val fails = Parsley.notFollowedBy(r.put(parsley.position.internalOffsetSpan(this.hide))) + (fails <|> r.get.flatMap { case (os, x, oe) => + val msg0 +: msgs = msggen(x) + combinator.fail(oe - os, msg0, msgs: _*) + }) *> Parsley.empty + } + private [parsley] def fail(msg: String, msgs: String*): Parsley[Nothing] = attempt(this.hide).fail(_ => msg +: msgs) + + // TODO: deprecate, and stress there is no _direct_ equivalent available moving forward /** This combinator parses this parser and then fails, using the result of this parser to customise the error message. * * Similar to `fail`, but first parses this parser: if it succeeded, then its result `x` is used to form the error @@ -450,6 +471,8 @@ object combinator { */ def !(msggen: A => String): Parsley[Nothing] = new Parsley(new frontend.FastFail(con(p).internal, msggen)) + // TODO: I think this can probably be deprecated for future removal soon... + // It will be replaced by one that generates reasons too! /** This combinator parses this parser and then fails, using the result of this parser to customise the unexpected component * of the error message. * @@ -463,5 +486,19 @@ object combinator { * @group fail */ def unexpected(msggen: A => String): Parsley[Nothing] = new Parsley(new frontend.FastUnexpected(con(p).internal, msggen)) + + // TODO: Documentation and testing ahead of future release + // like notFollowedBy, but does consume input on "success" and always fails (FIXME: this needs intrinsic support to get right) + // it should also have the partial amend semantics, because `amendAndDislodge` can restore the other semantics anyway + // Document that `attempt` may be used when this is an informative but not terminal error. + private def unexpected(reason: Option[A => String]) = { + // holy hell, the hoops I jump through to be able to implement things + val r = parsley.registers.Reg.make[A] + val fails = Parsley.notFollowedBy(r.put(this.hide)) + reason.fold(fails)(rgen => fails <|> r.get.flatMap(x => Parsley.empty.explain(rgen(x)))) *> Parsley.empty + } + private [parsley] def unexpected: Parsley[Nothing] = this.unexpected(None) + private [parsley] def unexpected(reason: String): Parsley[Nothing] = this._unexpected(_ => reason) + private [parsley] def _unexpected(reason: A => String): Parsley[Nothing] = this.unexpected(Some(reason)) } } diff --git a/parsley/shared/src/main/scala/parsley/internal/deepembedding/backend/AlternativeEmbedding.scala b/parsley/shared/src/main/scala/parsley/internal/deepembedding/backend/AlternativeEmbedding.scala index c98025af7..a86cff98b 100644 --- a/parsley/shared/src/main/scala/parsley/internal/deepembedding/backend/AlternativeEmbedding.scala +++ b/parsley/shared/src/main/scala/parsley/internal/deepembedding/backend/AlternativeEmbedding.scala @@ -11,7 +11,7 @@ import parsley.XAssert._ import parsley.internal.collection.mutable.SinglyLinkedList, SinglyLinkedList.LinkedListIterator import parsley.internal.deepembedding.ContOps, ContOps.{result, suspend, ContAdapter} import parsley.internal.deepembedding.singletons._ -import parsley.internal.errors.{ExpectDesc, ExpectItem, ExpectRaw} +import parsley.internal.errors.{ExpectDesc, ExpectItem} import parsley.internal.machine.instructions // scalastyle:off underscore.import @@ -227,14 +227,12 @@ private [backend] object Choice { @tailrec private def tablable(p: StrictParsley[_], backtracks: Boolean): Option[(Char, Option[ExpectItem], Int, Boolean)] = p match { // CODO: Numeric parsers by leading digit (This one would require changing the foldTablified function a bit) - case ct@CharTok(d) => - Some((d, ct.expected.fold[Option[ExpectItem]](Some(ExpectRaw(d)))(n => if (n.nonEmpty) Some(ExpectDesc(n)) else None), 1, backtracks)) - case st@StringTok(s) => - Some((s.head, st.expected.fold[Option[ExpectItem]](Some(ExpectRaw(s)))(n => if (n.nonEmpty) Some(ExpectDesc(n)) else None), s.size, backtracks)) + case ct@CharTok(d) => Some((d, ct.expected.asExpectItem(d), 1, backtracks)) + case st@StringTok(s) => Some((s.head, st.expected.asExpectItem(s), s.codePointCount(0, s.length), backtracks)) //case op@MaxOp(o) => Some((o.head, Some(Desc(o)), o.size, backtracks)) //case _: StringLiteral | RawStringLiteral => Some(('"', Some(Desc("string")), 1, backtracks)) // TODO: This can be done for case insensitive things too, but with duplicated branching - case t@Specific(s) if t.caseSensitive => Some((s.head, Some(ExpectDesc(s)), s.size, backtracks)) + case t@Specific(s) if t.caseSensitive => Some((s.head, Some(ExpectDesc(s)), s.codePointCount(0, s.length), backtracks)) case Attempt(t) => tablable(t, backtracks = true) case (_: Pure[_]) <*> t => tablable(t, backtracks) case Lift2(_, t, _) => tablable(t, backtracks) diff --git a/parsley/shared/src/main/scala/parsley/internal/deepembedding/backend/ErrorEmbedding.scala b/parsley/shared/src/main/scala/parsley/internal/deepembedding/backend/ErrorEmbedding.scala index 189a10dee..f41a86256 100644 --- a/parsley/shared/src/main/scala/parsley/internal/deepembedding/backend/ErrorEmbedding.scala +++ b/parsley/shared/src/main/scala/parsley/internal/deepembedding/backend/ErrorEmbedding.scala @@ -3,6 +3,8 @@ */ package parsley.internal.deepembedding.backend +import parsley.token.errors.{Hidden, Label} + import parsley.internal.deepembedding.singletons._ import parsley.internal.machine.instructions private [deepembedding] final class ErrorLabel[A](val p: StrictParsley[A], private [ErrorLabel] val label: String) extends ScopedUnary[A, A] { @@ -13,11 +15,11 @@ private [deepembedding] final class ErrorLabel[A](val p: StrictParsley[A], priva override def instrNeedsLabel: Boolean = false override def handlerLabel(state: CodeGenState): Int = state.getLabelForRelabelError(label) final override def optimise: StrictParsley[A] = p match { - case ct@CharTok(c) if !ct.expected.contains("") => new CharTok(c, Some(label)).asInstanceOf[StrictParsley[A]] - case st@StringTok(s) if !st.expected.contains("") => new StringTok(s, Some(label)).asInstanceOf[StrictParsley[A]] - case sat@Satisfy(f) if !sat.expected.contains("") => new Satisfy(f, Some(label)).asInstanceOf[StrictParsley[A]] + case ct@CharTok(c) if ct.expected ne Hidden => new CharTok(c, Label(label)).asInstanceOf[StrictParsley[A]] + case st@StringTok(s) if st.expected ne Hidden => new StringTok(s, Label(label)).asInstanceOf[StrictParsley[A]] + case sat@Satisfy(f) if sat.expected ne Hidden => new Satisfy(f, Label(label)).asInstanceOf[StrictParsley[A]] // TODO: The hide property is required to be checked, but there is no test for it - case ErrorLabel(p, label2) if label2 != "" => ErrorLabel(p, label) + case ErrorLabel(p, label2) if label2.nonEmpty => ErrorLabel(p, label) case _ => this } @@ -35,10 +37,10 @@ private [deepembedding] final class ErrorExplain[A](val p: StrictParsley[A], rea // $COVERAGE-ON$ } -private [deepembedding] final class ErrorAmend[A](val p: StrictParsley[A]) extends ScopedUnaryWithState[A, A](false) { +private [deepembedding] final class ErrorAmend[A](val p: StrictParsley[A], partial: Boolean) extends ScopedUnaryWithState[A, A](false) { override val instr: instructions.Instr = instructions.PopHandlerAndState override def instrNeedsLabel: Boolean = false - override def handlerLabel(state: CodeGenState): Int = state.getLabel(instructions.AmendAndFail) + override def handlerLabel(state: CodeGenState): Int = state.getLabel(instructions.AmendAndFail(partial)) // $COVERAGE-OFF$ final override def pretty(p: String): String = s"amend($p)" // $COVERAGE-ON$ @@ -52,6 +54,15 @@ private [deepembedding] final class ErrorEntrench[A](val p: StrictParsley[A]) ex final override def pretty(p: String): String = s"entrench($p)" // $COVERAGE-ON$ } +private [deepembedding] final class ErrorDislodge[A](val p: StrictParsley[A]) extends ScopedUnary[A, A] { + override def setup(label: Int): instructions.Instr = new instructions.PushHandler(label) + override val instr: instructions.Instr = instructions.PopHandler + override def instrNeedsLabel: Boolean = false + override def handlerLabel(state: CodeGenState): Int = state.getLabel(instructions.DislodgeAndFail) + // $COVERAGE-OFF$ + final override def pretty(p: String): String = s"dislodge($p)" + // $COVERAGE-ON$ +} private [deepembedding] final class ErrorLexical[A](val p: StrictParsley[A]) extends ScopedUnary[A, A] { // This needs to save the hints because error label will relabel the first hint, which because the list is ordered would be the hints that came _before_ diff --git a/parsley/shared/src/main/scala/parsley/internal/deepembedding/frontend/ErrorEmbedding.scala b/parsley/shared/src/main/scala/parsley/internal/deepembedding/frontend/ErrorEmbedding.scala index 940e3fef5..517c10369 100644 --- a/parsley/shared/src/main/scala/parsley/internal/deepembedding/frontend/ErrorEmbedding.scala +++ b/parsley/shared/src/main/scala/parsley/internal/deepembedding/frontend/ErrorEmbedding.scala @@ -12,12 +12,15 @@ private [parsley] final class ErrorExplain[A](p: LazyParsley[A], reason: String) override def make(p: StrictParsley[A]): StrictParsley[A] = new backend.ErrorExplain(p, reason) } -private [parsley] final class ErrorAmend[A](p: LazyParsley[A]) extends Unary[A, A](p) { - override def make(p: StrictParsley[A]): StrictParsley[A] = new backend.ErrorAmend(p) +private [parsley] final class ErrorAmend[A](p: LazyParsley[A], partial: Boolean) extends Unary[A, A](p) { + override def make(p: StrictParsley[A]): StrictParsley[A] = new backend.ErrorAmend(p, partial) } private [parsley] final class ErrorEntrench[A](p: LazyParsley[A]) extends Unary[A, A](p) { override def make(p: StrictParsley[A]): StrictParsley[A] = new backend.ErrorEntrench(p) } +private [parsley] final class ErrorDislodge[A](p: LazyParsley[A]) extends Unary[A, A](p) { + override def make(p: StrictParsley[A]): StrictParsley[A] = new backend.ErrorDislodge(p) +} private [parsley] final class ErrorLexical[A](p: LazyParsley[A]) extends Unary[A, A](p) { override def make(p: StrictParsley[A]): StrictParsley[A] = new backend.ErrorLexical(p) diff --git a/parsley/shared/src/main/scala/parsley/internal/deepembedding/singletons/IntrinsicEmbedding.scala b/parsley/shared/src/main/scala/parsley/internal/deepembedding/singletons/IntrinsicEmbedding.scala index 4a64b27bc..164e775ea 100644 --- a/parsley/shared/src/main/scala/parsley/internal/deepembedding/singletons/IntrinsicEmbedding.scala +++ b/parsley/shared/src/main/scala/parsley/internal/deepembedding/singletons/IntrinsicEmbedding.scala @@ -4,18 +4,19 @@ package parsley.internal.deepembedding.singletons import parsley.registers.Reg +import parsley.token.errors.LabelConfig import parsley.internal.deepembedding.frontend.UsesRegister import parsley.internal.machine.instructions -private [parsley] final class CharTok(private [CharTok] val c: Char, val expected: Option[String]) extends Singleton[Char] { +private [parsley] final class CharTok(private [CharTok] val c: Char, val expected: LabelConfig) extends Singleton[Char] { // $COVERAGE-OFF$ override def pretty: String = s"char($c)" // $COVERAGE-ON$ override def instr: instructions.Instr = instructions.CharTok(c, expected) } -private [parsley] final class StringTok(private [StringTok] val s: String, val expected: Option[String]) extends Singleton[String] { +private [parsley] final class StringTok(private [StringTok] val s: String, val expected: LabelConfig) extends Singleton[String] { // $COVERAGE-OFF$ override def pretty: String = s"string($s)" // $COVERAGE-ON$ diff --git a/parsley/shared/src/main/scala/parsley/internal/deepembedding/singletons/PrimitiveEmbedding.scala b/parsley/shared/src/main/scala/parsley/internal/deepembedding/singletons/PrimitiveEmbedding.scala index 9f4d7081c..de3976ad1 100644 --- a/parsley/shared/src/main/scala/parsley/internal/deepembedding/singletons/PrimitiveEmbedding.scala +++ b/parsley/shared/src/main/scala/parsley/internal/deepembedding/singletons/PrimitiveEmbedding.scala @@ -4,14 +4,15 @@ package parsley.internal.deepembedding.singletons import parsley.registers.Reg +import parsley.token.errors.LabelConfig import parsley.internal.machine.instructions -private [parsley] final class Satisfy(private [Satisfy] val f: Char => Boolean, val expected: Option[String]) extends Singleton[Char] { +private [parsley] final class Satisfy(private [Satisfy] val f: Char => Boolean, val expected: LabelConfig) extends Singleton[Char] { // $COVERAGE-OFF$ override val pretty: String = "satisfy(f)" // $COVERAGE-ON$ - override def instr: instructions.Instr = new instructions.Satisfies(f, expected) + override def instr: instructions.Instr = instructions.Satisfies(f, expected) } private [parsley] object Line extends Singleton[Int] { @@ -26,6 +27,13 @@ private [parsley] object Col extends Singleton[Int] { // $COVERAGE-ON$ override val instr: instructions.Instr = instructions.Col } +private [parsley] object Offset extends Singleton[Int] { + // $COVERAGE-OFF$ + override val pretty: String = "offset" + // $COVERAGE-ON$ + override val instr: instructions.Instr = instructions.Offset +} + // This should really have UsesRegister, however, if it doesn't, this has the nice effect of catching // registers that have never been filled in some way! private [parsley] final class Get[S](reg: Reg[S]) extends Singleton[S] { diff --git a/parsley/shared/src/main/scala/parsley/internal/deepembedding/singletons/TokenEmbedding.scala b/parsley/shared/src/main/scala/parsley/internal/deepembedding/singletons/TokenEmbedding.scala index 47f1fd970..1ef077d25 100644 --- a/parsley/shared/src/main/scala/parsley/internal/deepembedding/singletons/TokenEmbedding.scala +++ b/parsley/shared/src/main/scala/parsley/internal/deepembedding/singletons/TokenEmbedding.scala @@ -3,28 +3,30 @@ */ package parsley.internal.deepembedding.singletons +import parsley.token.descriptions.SpaceDesc import parsley.token.descriptions.numeric.PlusSignPresence +import parsley.token.errors.{ErrorConfig, LabelConfig} import parsley.internal.deepembedding.Sign.SignType import parsley.internal.machine.instructions -private [parsley] final class WhiteSpace(ws: Char => Boolean, start: String, end: String, line: String, nested: Boolean, eofAllowed: Boolean) +private [parsley] final class WhiteSpace(ws: Char => Boolean, desc: SpaceDesc, errConfig: ErrorConfig) extends Singleton[Unit] { // $COVERAGE-OFF$ override val pretty: String = "whiteSpace" - override def instr: instructions.Instr = new instructions.TokenWhiteSpace(ws, start, end, line, nested, eofAllowed) + override def instr: instructions.Instr = new instructions.TokenWhiteSpace(ws, desc, errConfig) } -private [parsley] final class SkipComments(start: String, end: String, line: String, nested: Boolean, eofAllowed: Boolean) extends Singleton[Unit] { +private [parsley] final class SkipComments(desc: SpaceDesc, errConfig: ErrorConfig) extends Singleton[Unit] { // $COVERAGE-OFF$ override val pretty: String = "skipComments" - override def instr: instructions.Instr = new instructions.TokenSkipComments(start, end, line, nested, eofAllowed) + override def instr: instructions.Instr = new instructions.TokenSkipComments(desc, errConfig) } -private [parsley] final class Comment(start: String, end: String, line: String, nested: Boolean, eofAllowed: Boolean) extends Singleton[Unit] { +private [parsley] final class Comment(desc: SpaceDesc, errConfig: ErrorConfig) extends Singleton[Unit] { // $COVERAGE-OFF$ override val pretty: String = "comment" - override def instr: instructions.Instr = new instructions.TokenComment(start, end, line, nested, eofAllowed) + override def instr: instructions.Instr = new instructions.TokenComment(desc, errConfig) } private [parsley] final class Sign[A](ty: SignType, signPresence: PlusSignPresence) extends Singleton[A => A] { @@ -33,17 +35,20 @@ private [parsley] final class Sign[A](ty: SignType, signPresence: PlusSignPresen override def instr: instructions.Instr = new instructions.TokenSign(ty, signPresence) } -private [parsley] class NonSpecific(override val pretty: String, name: String, illegalName: String, +private [parsley] class NonSpecific(name: String, unexpectedIllegal: String => String, start: Char => Boolean, letter: Char => Boolean, illegal: String => Boolean) extends Singleton[String] { - override def instr: instructions.Instr = new instructions.TokenNonSpecific(name, illegalName)(start, letter, illegal) + // $COVERAGE-OFF$ + override def pretty: String = "nonspecificName" + // $COVERAGE-ON$ + override def instr: instructions.Instr = new instructions.TokenNonSpecific(name, unexpectedIllegal)(start, letter, illegal) } -private [parsley] final class Specific(name: String, private [Specific] val specific: String, letter: Char => Boolean, val caseSensitive: Boolean) - extends Singleton[Unit] { +private [parsley] final class Specific(private [Specific] val specific: String, expected: LabelConfig, + expectedEnd: String, letter: Char => Boolean, val caseSensitive: Boolean) extends Singleton[Unit] { // $COVERAGE-OFF$ - override def pretty: String = s"$name($specific)" + override def pretty: String = s"specific($specific)" // $COVERAGE-ON$ - override def instr: instructions.Instr = new instructions.TokenSpecific(specific, letter, caseSensitive) + override def instr: instructions.Instr = new instructions.TokenSpecific(specific, expected, expectedEnd, letter, caseSensitive) } /* diff --git a/parsley/shared/src/main/scala/parsley/internal/errors/ErrorItem.scala b/parsley/shared/src/main/scala/parsley/internal/errors/ErrorItem.scala index 45236bfcf..6d5b82804 100644 --- a/parsley/shared/src/main/scala/parsley/internal/errors/ErrorItem.scala +++ b/parsley/shared/src/main/scala/parsley/internal/errors/ErrorItem.scala @@ -8,62 +8,53 @@ import parsley.errors, errors.{ErrorBuilder, Token, TokenSpan} private [internal] sealed abstract class ErrorItem private [internal] sealed trait UnexpectItem extends ErrorItem { - def formatUnexpect(lexicalError: Boolean)(implicit builder: ErrorBuilder[_]): (builder.Item, TokenSpan) - def higherPriority(other: UnexpectItem): Boolean + private [internal] def formatUnexpect(lexicalError: Boolean)(implicit builder: ErrorBuilder[_]): (builder.Item, TokenSpan) + private [internal] def higherPriority(other: UnexpectItem): Boolean protected [errors] def lowerThanRaw(other: UnexpectRaw): Boolean protected [errors] def lowerThanDesc(other: UnexpectDesc): Boolean } -private [internal] sealed trait ExpectItem extends ErrorItem { - def formatExpect(implicit builder: ErrorBuilder[_]): builder.Item - def higherPriority(other: ExpectItem): Boolean - protected [errors] def lowerThanRaw(other: ExpectRaw): Boolean - protected [errors] def lowerThanDesc: Boolean +private [parsley] sealed trait ExpectItem extends ErrorItem { + private [internal] def formatExpect(implicit builder: ErrorBuilder[_]): builder.Item } private [internal] final case class UnexpectRaw(cs: Iterable[Char], amountOfInputParserWanted: Int) extends UnexpectItem { assert(cs.nonEmpty, "we promise that unexpectedToken never receives empty input") - def formatUnexpect(lexicalError: Boolean)(implicit builder: ErrorBuilder[_]): (builder.Item, TokenSpan) = { + private [internal] def formatUnexpect(lexicalError: Boolean)(implicit builder: ErrorBuilder[_]): (builder.Item, TokenSpan) = { builder.unexpectedToken(cs, amountOfInputParserWanted, lexicalError) match { case t@Token.Raw(tok) => (builder.raw(tok), t.span) case Token.Named(name, span) => (builder.named(name), span) } } - override def higherPriority(other: UnexpectItem): Boolean = other.lowerThanRaw(this) - override def lowerThanRaw(other: UnexpectRaw): Boolean = this.amountOfInputParserWanted < other.amountOfInputParserWanted - override def lowerThanDesc(other: UnexpectDesc): Boolean = true + private [internal] override def higherPriority(other: UnexpectItem): Boolean = other.lowerThanRaw(this) + protected [errors] override def lowerThanRaw(other: UnexpectRaw): Boolean = this.amountOfInputParserWanted < other.amountOfInputParserWanted + protected [errors] override def lowerThanDesc(other: UnexpectDesc): Boolean = true } -private [internal] final case class ExpectRaw(cs: String) extends ExpectItem { - def formatExpect(implicit builder: ErrorBuilder[_]): builder.Item = builder.raw(cs) - override def higherPriority(other: ExpectItem): Boolean = other.lowerThanRaw(this) - override def lowerThanRaw(other: ExpectRaw): Boolean = this.cs.length < other.cs.length - override def lowerThanDesc: Boolean = true +private [parsley] final case class ExpectRaw(cs: String) extends ExpectItem { + private [internal] def formatExpect(implicit builder: ErrorBuilder[_]): builder.Item = builder.raw(cs) } -private [internal] object ExpectRaw { +private [parsley] object ExpectRaw { def apply(c: Char): ExpectRaw = new ExpectRaw(s"$c") } -private [internal] final case class ExpectDesc(msg: String) extends ExpectItem { +private [parsley] final case class ExpectDesc(msg: String) extends ExpectItem { assert(msg.nonEmpty, "Desc cannot contain empty things!") - def formatExpect(implicit builder: ErrorBuilder[_]): builder.Item = builder.named(msg) - override def higherPriority(other: ExpectItem): Boolean = other.lowerThanDesc - override def lowerThanRaw(other: ExpectRaw): Boolean = false - override def lowerThanDesc: Boolean = true + private [internal] def formatExpect(implicit builder: ErrorBuilder[_]): builder.Item = builder.named(msg) } + private [internal] final case class UnexpectDesc(msg: String, width: Int) extends UnexpectItem { assert(msg.nonEmpty, "Desc cannot contain empty things!") // FIXME: When this is formatted, the width should really be normalised to the number of code points... this information is not readily available - def formatUnexpect(lexicalError: Boolean)(implicit builder: ErrorBuilder[_]): (builder.Item, TokenSpan) = (builder.named(msg), TokenSpan.Width(width)) - override def higherPriority(other: UnexpectItem): Boolean = other.lowerThanDesc(this) - override def lowerThanRaw(other: UnexpectRaw): Boolean = false - override def lowerThanDesc(other: UnexpectDesc): Boolean = this.width < other.width + private [internal] def formatUnexpect(lexicalError: Boolean)(implicit builder: ErrorBuilder[_]): (builder.Item, TokenSpan) = + (builder.named(msg), TokenSpan.Width(width)) + private [internal] override def higherPriority(other: UnexpectItem): Boolean = other.lowerThanDesc(this) + protected [errors] override def lowerThanRaw(other: UnexpectRaw): Boolean = false + protected [errors] override def lowerThanDesc(other: UnexpectDesc): Boolean = this.width < other.width } private [internal] case object EndOfInput extends UnexpectItem with ExpectItem { - def formatExpect(implicit builder: ErrorBuilder[_]): builder.Item = builder.endOfInput - def formatUnexpect(lexicalError: Boolean)(implicit builder: ErrorBuilder[_]): (builder.Item, TokenSpan) = (builder.endOfInput, TokenSpan.Width(1)) - override def higherPriority(other: ExpectItem): Boolean = true - override def higherPriority(other: UnexpectItem): Boolean = true - override def lowerThanRaw(other: ExpectRaw): Boolean = false - override def lowerThanRaw(other: UnexpectRaw): Boolean = false - override def lowerThanDesc: Boolean = false - override def lowerThanDesc(other: UnexpectDesc): Boolean = false + private [internal] def formatExpect(implicit builder: ErrorBuilder[_]): builder.Item = builder.endOfInput + private [internal] def formatUnexpect(lexicalError: Boolean)(implicit builder: ErrorBuilder[_]): (builder.Item, TokenSpan) = + (builder.endOfInput, TokenSpan.Width(1)) + private [internal] override def higherPriority(other: UnexpectItem): Boolean = true + protected [errors] override def lowerThanRaw(other: UnexpectRaw): Boolean = false + protected [errors] override def lowerThanDesc(other: UnexpectDesc): Boolean = false } 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 1b047e399..6cbfbaa5b 100644 --- a/parsley/shared/src/main/scala/parsley/internal/machine/Context.scala +++ b/parsley/shared/src/main/scala/parsley/internal/machine/Context.scala @@ -41,8 +41,6 @@ private [parsley] final class Context(private [machine] var instrs: Array[Instr] 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 size of the call stack */ - private var depth: Int = 0 /** Current offset into program instruction buffer */ private [machine] var pc: Int = 0 /** Current line number */ @@ -86,7 +84,7 @@ private [parsley] final class Context(private [machine] var instrs: Array[Instr] commitHints() } private [machine] def replaceHint(label: String): Unit = hints = hints.rename(label) - private [machine] def popHints: Unit = hints = hints.pop + private [machine] def popHints(): Unit = hints = hints.pop /* ERROR RELABELLING END */ private def addErrorToHints(): Unit = { @@ -120,7 +118,6 @@ private [parsley] final class Context(private [machine] var instrs: Array[Instr] | pos = ($line, $col) | status = $status | pc = $pc - | depth = $depth | rets = ${calls.mkString(", ")} | handlers = ${handlers.mkString(", ")} | recstates = ${states.mkString(", ")} @@ -166,35 +163,13 @@ private [parsley] final class Context(private [machine] var instrs: Array[Instr] private [machine] def call(at: Int): Unit = { calls = new CallStack(pc + 1, instrs, at, calls) pc = at - depth += 1 } private [machine] def ret(): Unit = { - assert(depth >= 1, "cannot return when no calls are made") + assert(calls != null, "cannot return when no calls are made") instrs = calls.instrs pc = calls.ret calls = calls.tail - depth -= 1 - } - - /** This method returns multiple times (in the case of a failure handler back many call-frames). - * - * @param n the number of frames to unwind. - */ - private def multiRet(n: Int): Unit = if (n > 0) { - if (n == 1) ret() - else { - var m = n - 1 // scalastyle:ignore var.local - assert(depth >= m, "cannot return when no calls are made") - depth -= m - // the rollback can safely discard n-1 frames immediately, as stateful instructions are no longer a thing! - while (m > 0) { - calls = calls.tail - m -= 1 - } - // this does the final, not shortcutted return - ret() - } } private [machine] def catchNoConsumed(handler: =>Unit): Unit = { @@ -239,7 +214,8 @@ private [parsley] final class Context(private [machine] var instrs: Array[Instr] else { val handler = handlers handlers = handlers.tail - multiRet(depth - handler.depth) + instrs = handler.instrs + calls = handler.calls pc = handler.pc val diffstack = stack.usize - handler.stacksz if (diffstack > 0) stack.drop(diffstack) @@ -276,7 +252,7 @@ private [parsley] final class Context(private [machine] var instrs: Array[Instr] offset += n col += n } - private [machine] def pushHandler(label: Int): Unit = handlers = new HandlerStack(depth, label, stack.usize, handlers) + 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 = { 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 75e20ddcf..67aac7745 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 @@ -95,6 +95,11 @@ private [machine] sealed abstract class DefuncError { * @return an entrenched error message */ private [machine] def entrench: DefuncError + /** This operation undoes the `amend` protection provided by an underlying entrenched error. + * + * @return a non-entrenched error message + */ + private [machine] def dislodge: DefuncError /** This operation sets this error message to be considered as a lexical * error message, which means that it will not perform lexical extraction * within the builder, instead opting to extract a token via raw input. @@ -132,6 +137,7 @@ 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 => collector ++= self.expectedIterable @@ -146,6 +152,7 @@ private [errors] sealed abstract class TrivialDefuncError extends DefuncError { 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) } @@ -172,9 +179,15 @@ private [errors] sealed abstract class TrivialDefuncError extends DefuncError { if (!this.entrenched) new TrivialAmended(offset, line, col, this) else this } - private [machine] final override def entrench: TrivialDefuncError = { - if (!this.entrenched) new TrivialEntrenched(this) - else this + private [machine] final override def entrench: TrivialDefuncError = this match { + case self: TrivialDislodged => new TrivialEntrenched(self.err) + case self if !self.entrenched => new TrivialEntrenched(this) + case self => self + } + private [machine] final override def dislodge: TrivialDefuncError = this match { + case self: TrivialEntrenched => self.err + case self if self.entrenched => new TrivialDislodged(this) + case self => self } private [machine] final override def markAsLexical(offset: Int): TrivialDefuncError = { if (Integer.compareUnsigned(this.offset, offset) > 0) new TrivialLexical(this) @@ -208,9 +221,15 @@ private [errors] sealed abstract class FancyDefuncError extends DefuncError { if (!this.entrenched) new FancyAmended(offset, line, col, this) else this } - private [machine] final override def entrench: FancyDefuncError = { - if (!this.entrenched) new FancyEntrenched(this) - else this + private [machine] final override def entrench: FancyDefuncError = this match { + case self: FancyDislodged => new FancyEntrenched(self.err) + case self if !self.entrenched => new FancyEntrenched(this) + case self => self + } + private [machine] final override def dislodge: FancyDefuncError = this match { + case self: FancyEntrenched => self.err + case self if self.entrenched => new FancyDislodged(this) + case self => self } private [machine] final override def markAsLexical(offset: Int): FancyDefuncError = { if (Integer.compareUnsigned(this.offset, offset) > 0) new FancyLexical(this) @@ -387,12 +406,24 @@ private [errors] final class TrivialEntrenched private [errors] (val err: Trivia override def makeTrivial(builder: TrivialErrorBuilder): Unit = err.makeTrivial(builder) } +private [errors] final class TrivialDislodged private [errors] (val err: TrivialDefuncError) extends TrivialDefuncError { + 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 FancyEntrenched private [errors] (val err: FancyDefuncError) extends FancyDefuncError { override final val flags = (err.flags | DefuncError.EntrenchedMask).toByte override val offset = err.offset override def makeFancy(builder: FancyErrorBuilder): Unit = err.makeFancy(builder) } +private [errors] final class FancyDislodged private [errors] (val err: FancyDefuncError) extends FancyDefuncError { + override final val flags = (err.flags & ~DefuncError.EntrenchedMask).toByte + override val offset = err.offset + override def makeFancy(builder: FancyErrorBuilder): Unit = err.makeFancy(builder) +} + private [errors] final class TrivialLexical private [errors] (val err: TrivialDefuncError) extends TrivialDefuncError { override final val flags = (err.flags | DefuncError.LexicalErrorMask).toByte override val offset = err.offset diff --git a/parsley/shared/src/main/scala/parsley/internal/machine/instructions/CoreInstrs.scala b/parsley/shared/src/main/scala/parsley/internal/machine/instructions/CoreInstrs.scala index db7b9477c..2ffc3ec6f 100644 --- a/parsley/shared/src/main/scala/parsley/internal/machine/instructions/CoreInstrs.scala +++ b/parsley/shared/src/main/scala/parsley/internal/machine/instructions/CoreInstrs.scala @@ -90,7 +90,7 @@ private [internal] object Halt extends Instr { } private [internal] final class Call(var label: Int) extends InstrWithLabel { - private var isSet: Boolean = false + private [this] var isSet: Boolean = false override def relabel(labels: Array[Int]): this.type = { if (!isSet) { label = labels(label) diff --git a/parsley/shared/src/main/scala/parsley/internal/machine/instructions/ErrorInstrs.scala b/parsley/shared/src/main/scala/parsley/internal/machine/instructions/ErrorInstrs.scala index db4da44f6..bae8c05ae 100644 --- a/parsley/shared/src/main/scala/parsley/internal/machine/instructions/ErrorInstrs.scala +++ b/parsley/shared/src/main/scala/parsley/internal/machine/instructions/ErrorInstrs.scala @@ -9,11 +9,11 @@ import parsley.internal.machine.XAssert._ import parsley.internal.machine.errors.{ClassicFancyError, ClassicUnexpectedError} private [internal] final class RelabelHints(label: String) extends Instr { - val isHide: Boolean = label.isEmpty + private [this] val isHide: Boolean = label.isEmpty override def apply(ctx: Context): Unit = { ensureRegularInstruction(ctx) // if this was a hide, pop the hints if possible - if (isHide) ctx.popHints + if (isHide) ctx.popHints() // EOK // replace the head of the hints with the singleton for our label else if (ctx.offset == ctx.checkStack.offset) ctx.replaceHint(label) @@ -30,7 +30,6 @@ private [internal] final class RelabelHints(label: String) extends Instr { } private [internal] final class RelabelErrorAndFail(label: String) extends Instr { - val isHide: Boolean = label.isEmpty override def apply(ctx: Context): Unit = { ensureHandlerInstruction(ctx) ctx.restoreHints() @@ -87,10 +86,10 @@ private [internal] class ApplyReasonAndFail(reason: String) extends Instr { // $COVERAGE-ON$ } -private [internal] object AmendAndFail extends Instr { +private [internal] class AmendAndFail private (partial: Boolean) extends Instr { override def apply(ctx: Context): Unit = { ensureHandlerInstruction(ctx) - ctx.errs.error = ctx.errs.error.amend(ctx.states.offset, ctx.states.line, ctx.states.col) + ctx.errs.error = ctx.errs.error.amend(if (partial) ctx.offset else ctx.states.offset, ctx.states.line, ctx.states.col) ctx.states = ctx.states.tail ctx.fail() } @@ -99,6 +98,11 @@ private [internal] object AmendAndFail extends Instr { override def toString: String = "AmendAndFail" // $COVERAGE-ON$ } +private [internal] object AmendAndFail { + private [this] val partial = new AmendAndFail(partial = true) + private [this] val full = new AmendAndFail(partial = false) + def apply(partial: Boolean): AmendAndFail = if (partial) this.partial else this.full +} private [internal] object EntrenchAndFail extends Instr { override def apply(ctx: Context): Unit = { @@ -112,6 +116,18 @@ private [internal] object EntrenchAndFail extends Instr { // $COVERAGE-ON$ } +private [internal] object DislodgeAndFail extends Instr { + override def apply(ctx: Context): Unit = { + ensureHandlerInstruction(ctx) + ctx.errs.error = ctx.errs.error.dislodge + ctx.fail() + } + + // $COVERAGE-OFF$ + override def toString: String = "DislodgeAndFail" + // $COVERAGE-ON$ +} + private [internal] object SetLexicalAndFail extends Instr { override def apply(ctx: Context): Unit = { ensureHandlerInstruction(ctx) diff --git a/parsley/shared/src/main/scala/parsley/internal/machine/instructions/IntrinsicInstrs.scala b/parsley/shared/src/main/scala/parsley/internal/machine/instructions/IntrinsicInstrs.scala index 2e233ec80..98ac804cc 100644 --- a/parsley/shared/src/main/scala/parsley/internal/machine/instructions/IntrinsicInstrs.scala +++ b/parsley/shared/src/main/scala/parsley/internal/machine/instructions/IntrinsicInstrs.scala @@ -5,7 +5,9 @@ package parsley.internal.machine.instructions import scala.annotation.tailrec -import parsley.internal.errors.{EndOfInput, ExpectDesc, ExpectItem, ExpectRaw, UnexpectDesc} +import parsley.token.errors.LabelConfig + +import parsley.internal.errors.{EndOfInput, ExpectItem, UnexpectDesc} import parsley.internal.machine.Context import parsley.internal.machine.XAssert._ import parsley.internal.machine.errors.{ClassicFancyError, ClassicUnexpectedError, EmptyError, EmptyErrorWithReason} @@ -81,7 +83,8 @@ private [internal] final class StringTok(s: String, x: Any, errorItem: Option[Ex if (j < sz && i < ctx.inputsz && ctx.input.charAt(i) == s.charAt(j)) go(ctx, i + 1, j + 1) else if (j < sz) { // The offset, line and column haven't been edited yet, so are in the right place - ctx.expectedFail(errorItem, codePointLength) + // FIXME: this might be a more appropriate way of capping off the demand for the error? + ctx.expectedFail(errorItem, /*s.codePointCount(0, j+1)*/codePointLength) ctx.offset = i // These help maintain a consistent internal state, this makes the debuggers // output less confusing in the string case in particular. @@ -131,8 +134,6 @@ private [internal] final class Case(var label: Int) extends InstrWithLabel { // $COVERAGE-ON$ } -// TODO: I think all three of these _shouldn't_ generate the -// unexpected item, but should generate a caret, fix this! private [internal] final class Filter[A](_pred: A => Boolean) extends Instr { private [this] val pred = _pred.asInstanceOf[Any => Boolean] override def apply(ctx: Context): Unit = { @@ -301,21 +302,13 @@ private [internal] final class SwapAndPut(reg: Int) extends Instr { // Companion Objects private [internal] object CharTok { - def apply(c: Char, expected: Option[String]): CharTok = apply(c, c, expected) - def apply(c: Char, x: Any, expected: Option[String]): CharTok = new CharTok(c, x, expected match { - case Some("") => None - case Some(e) => Some(ExpectDesc(e)) - case None => Some(ExpectRaw(c)) - }) + def apply(c: Char, expected: LabelConfig): CharTok = apply(c, c, expected) + def apply(c: Char, x: Any, expected: LabelConfig): CharTok = new CharTok(c, x, expected.asExpectItem(s"$c")) } private [internal] object StringTok { - def apply(s: String, expected: Option[String]): StringTok = apply(s, s, expected) - def apply(s: String, x: Any, expected: Option[String]): StringTok = new StringTok(s, x, expected match { - case Some("") => None - case Some(e) => Some(ExpectDesc(e)) - case None => Some(ExpectRaw(s)) - }) + def apply(s: String, expected: LabelConfig): StringTok = apply(s, s, expected) + def apply(s: String, x: Any, expected: LabelConfig): StringTok = new StringTok(s, x, expected.asExpectItem(s)) private [StringTok] abstract class Adjust { private [StringTok] def tab: Adjust @@ -359,9 +352,9 @@ private [internal] object StringTok { } private [internal] object CharTokFastPerform { - def apply[A >: Char, B](c: Char, f: A => B, expected: Option[String]): CharTok = CharTok(c, f(c), expected) + def apply[A >: Char, B](c: Char, f: A => B, expected: LabelConfig): CharTok = CharTok(c, f(c), expected) } private [internal] object StringTokFastPerform { - def apply(s: String, f: String => Any, expected: Option[String]): StringTok = StringTok(s, f(s), expected) + def apply(s: String, f: String => Any, expected: LabelConfig): StringTok = StringTok(s, f(s), expected) } diff --git a/parsley/shared/src/main/scala/parsley/internal/machine/instructions/OptInstrs.scala b/parsley/shared/src/main/scala/parsley/internal/machine/instructions/OptInstrs.scala index b47d49582..df3f2faf4 100644 --- a/parsley/shared/src/main/scala/parsley/internal/machine/instructions/OptInstrs.scala +++ b/parsley/shared/src/main/scala/parsley/internal/machine/instructions/OptInstrs.scala @@ -5,9 +5,10 @@ package parsley.internal.machine.instructions import scala.collection.mutable -import parsley.XCompat._ //mapValuesInPlace +import parsley.XCompat._ +import parsley.token.errors.LabelConfig -import parsley.internal.errors.{ExpectDesc, ExpectItem} +import parsley.internal.errors.ExpectItem import parsley.internal.machine.Context import parsley.internal.machine.XAssert._ import parsley.internal.machine.errors.MultiExpectedError @@ -36,8 +37,8 @@ private [internal] final class Exchange[A](private [Exchange] val x: A) extends // $COVERAGE-ON$ } -private [internal] final class SatisfyExchange[A](f: Char => Boolean, x: A, _expected: Option[String]) extends Instr { - private [this] final val expected = _expected.map(ExpectDesc(_)) +private [internal] final class SatisfyExchange[A](f: Char => Boolean, x: A, _expected: LabelConfig) extends Instr { + private [this] final val expected = _expected.asExpectDesc override def apply(ctx: Context): Unit = { ensureRegularInstruction(ctx) if (ctx.moreInput && f(ctx.nextChar)) { @@ -109,7 +110,8 @@ private [internal] final class JumpTable(jumpTable: mutable.LongMap[(Int, Set[Ex } private def addErrors(ctx: Context, errorItems: Set[ExpectItem]): Unit = { - ctx.errs = new ErrorStack(new MultiExpectedError(ctx.offset, ctx.line, ctx.col, errorItems, size), ctx.errs) + // FIXME: the more appropriate way of demanding input may be to pick 1 character, for same rationale with StringTok + ctx.errs = new ErrorStack(new MultiExpectedError(ctx.offset, ctx.line, ctx.col, errorItems, unexpectedWidth = size), ctx.errs) ctx.pushHandler(merge) } 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 fa4cf5093..6541b4677 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 @@ -3,12 +3,13 @@ */ package parsley.internal.machine.instructions +import parsley.token.errors.LabelConfig + import parsley.internal.errors.ExpectDesc import parsley.internal.machine.Context import parsley.internal.machine.XAssert._ -private [internal] final class Satisfies(f: Char => Boolean, _expected: Option[String]) extends Instr { - private [this] final val expected = _expected.flatMap(label => if (label.isEmpty) None else Some(ExpectDesc(label))) +private [internal] final class Satisfies(f: Char => Boolean, expected: Option[ExpectDesc]) extends Instr { override def apply(ctx: Context): Unit = { ensureRegularInstruction(ctx) if (ctx.moreInput && f(ctx.nextChar)) ctx.pushAndContinue(ctx.consumeChar()) @@ -18,6 +19,9 @@ private [internal] final class Satisfies(f: Char => Boolean, _expected: Option[S override def toString: String = "Sat(?)" // $COVERAGE-ON$ } +private [internal] object Satisfies { + def apply(f: Char => Boolean, expected: LabelConfig): Satisfies = new Satisfies(f, expected.asExpectDesc) +} private [internal] object RestoreAndFail extends Instr { override def apply(ctx: Context): Unit = { @@ -88,6 +92,16 @@ private [internal] object Col extends Instr { // $COVERAGE-ON$ } +private [internal] object Offset extends Instr { + override def apply(ctx: Context): Unit = { + ensureRegularInstruction(ctx) + ctx.pushAndContinue(ctx.offset) + } + // $COVERAGE-OFF$ + override def toString: String = "Offset" + // $COVERAGE-ON$ +} + // Register-Manipulators private [internal] final class Get(reg: Int) extends Instr { override def apply(ctx: Context): Unit = { diff --git a/parsley/shared/src/main/scala/parsley/internal/machine/instructions/TokenInstrs.scala b/parsley/shared/src/main/scala/parsley/internal/machine/instructions/TokenInstrs.scala index ac6db2870..8021c82dd 100644 --- a/parsley/shared/src/main/scala/parsley/internal/machine/instructions/TokenInstrs.scala +++ b/parsley/shared/src/main/scala/parsley/internal/machine/instructions/TokenInstrs.scala @@ -6,16 +6,21 @@ package parsley.internal.machine.instructions import scala.annotation.tailrec import parsley.XAssert._ +import parsley.token.descriptions.SpaceDesc +import parsley.token.errors.{ErrorConfig, LabelConfig} -import parsley.internal.errors.{ExpectDesc, UnexpectDesc} +import parsley.internal.errors.{ExpectDesc, ExpectItem, UnexpectDesc} import parsley.internal.machine.Context import parsley.internal.machine.XAssert._ -private [instructions] abstract class CommentLexer(start: String, end: String, line: String, nested: Boolean, eofAllowed: Boolean) - extends Instr { +private [instructions] abstract class CommentLexer extends Instr { + protected [this] val start: String + protected [this] val end: String + protected [this] val line: String + protected [this] val nested: Boolean + protected [this] val eofAllowed: Boolean protected [this] final val lineAllowed = line.nonEmpty protected [this] final val multiAllowed = start.nonEmpty && end.nonEmpty - protected [this] final val endOfComment = Some(ExpectDesc("end of comment")) assert(!lineAllowed || !multiAllowed || !line.startsWith(start), "multi-line comments may not prefix single-line comments") @@ -58,15 +63,52 @@ private [instructions] abstract class CommentLexer(start: String, end: String, l } } -private [instructions] abstract class WhiteSpaceLike(start: String, end: String, line: String, nested: Boolean, eofAllowed: Boolean) - extends CommentLexer(start, end, line, nested, eofAllowed) { +private [internal] final class TokenComment private ( + protected [this] val start: String, + protected [this] val end: String, + protected [this] val line: String, + protected [this] val nested: Boolean, + protected [this] val eofAllowed: Boolean, + endOfMultiComment: Option[ExpectItem], + endOfSingleComment: Option[ExpectDesc], + ) extends CommentLexer { + def this(desc: SpaceDesc, errConfig: ErrorConfig) = { + this(desc.commentStart, desc.commentEnd, desc.commentLine, desc.nestedComments, desc.commentLineAllowsEOF, + errConfig.labelSpaceEndOfMultiComment.asExpectItem(desc.commentEnd), + errConfig.labelSpaceEndOfLineComment.asExpectDesc("end of line")) + } + private [this] final val openingSize = Math.max(start.codePointCount(0, start.length), line.codePointCount(0, line.length)) + + assert(multiAllowed || lineAllowed, "one of single- or multi-line must be enabled") + + override def apply(ctx: Context): Unit = { + ensureRegularInstruction(ctx) + val startsMulti = multiAllowed && ctx.input.startsWith(start, ctx.offset) + // If neither comment is available we fail + if (!ctx.moreInput || (!lineAllowed || !ctx.input.startsWith(line, ctx.offset)) && !startsMulti) ctx.expectedFail(expected = None, openingSize) + // One of the comments must be available + else if (startsMulti && multiLineComment(ctx)) ctx.pushAndContinue(()) + else if (startsMulti) ctx.expectedFail(expected = endOfMultiComment, unexpectedWidth = 1) + // It clearly wasn't the multi-line comment, so we are left with single line + else if (singleLineComment(ctx)) ctx.pushAndContinue(()) + else ctx.expectedFail(expected = endOfSingleComment, unexpectedWidth = 1) + } + + // $COVERAGE-OFF$ + override def toString: String = "TokenComment" + // $COVERAGE-ON$ +} + +private [instructions] abstract class WhiteSpaceLike extends CommentLexer { private [this] final val numCodePointsEnd = end.codePointCount(0, end.length) + protected [this] val endOfSingleComment: Option[ExpectDesc] + protected [this] val endOfMultiComment: Option[ExpectItem] @tailrec private final def singlesOnly(ctx: Context): Unit = { spaces(ctx) if (ctx.moreInput) { val startsSingle = ctx.input.startsWith(line, ctx.offset) if (startsSingle && singleLineComment(ctx)) singlesOnly(ctx) - else if (startsSingle) ctx.expectedFail(expected = endOfComment, unexpectedWidth = 1) + else if (startsSingle) ctx.expectedFail(expected = endOfSingleComment, unexpectedWidth = 1) else ctx.pushAndContinue(()) } else ctx.pushAndContinue(()) @@ -76,7 +118,7 @@ private [instructions] abstract class WhiteSpaceLike(start: String, end: String, spaces(ctx) val startsMulti = ctx.moreInput && ctx.input.startsWith(start, ctx.offset) if (startsMulti && multiLineComment(ctx)) multisOnly(ctx) - else if (startsMulti) ctx.expectedFail(expected = endOfComment, numCodePointsEnd) + else if (startsMulti) ctx.expectedFail(expected = endOfMultiComment, numCodePointsEnd) else ctx.pushAndContinue(()) } @@ -89,11 +131,11 @@ private [instructions] abstract class WhiteSpaceLike(start: String, end: String, if (ctx.moreInput && ctx.input.startsWith(sharedPrefix, ctx.offset)) { val startsMulti = ctx.input.startsWith(factoredStart, ctx.offset + sharedPrefix.length) if (startsMulti && multiLineComment(ctx)) singlesAndMultis(ctx) - else if (startsMulti) ctx.expectedFail(expected = endOfComment, numCodePointsEnd) + else if (startsMulti) ctx.expectedFail(expected = endOfMultiComment, numCodePointsEnd) else { val startsLine = ctx.input.startsWith(factoredLine, ctx.offset + sharedPrefix.length) if (startsLine && singleLineComment(ctx)) singlesAndMultis(ctx) - else if (startsLine) ctx.expectedFail(expected = endOfComment, unexpectedWidth = 1) + else if (startsLine) ctx.expectedFail(expected = endOfSingleComment, unexpectedWidth = 1) else ctx.pushAndContinue(()) } } @@ -119,35 +161,20 @@ private [instructions] abstract class WhiteSpaceLike(start: String, end: String, protected def spaces(ctx: Context): Unit } -private [internal] final class TokenComment(start: String, end: String, line: String, nested: Boolean, eofAllowed: Boolean) - extends CommentLexer(start, end, line, nested, eofAllowed) { - private [this] final val comment = Some(ExpectDesc("comment")) - private [this] final val openingSize = Math.max(start.codePointCount(0, start.length), line.codePointCount(0, line.length)) - - assert(multiAllowed || lineAllowed, "one of single- or multi-line must be enabled") - - override def apply(ctx: Context): Unit = { - ensureRegularInstruction(ctx) - val startsMulti = multiAllowed && ctx.input.startsWith(start, ctx.offset) - // If neither comment is available we fail - if (!ctx.moreInput || (!lineAllowed || !ctx.input.startsWith(line, ctx.offset)) && !startsMulti) ctx.expectedFail(expected = comment, openingSize) - // One of the comments must be available - else if (startsMulti && multiLineComment(ctx)) ctx.pushAndContinue(()) - else if (startsMulti) ctx.expectedFail(expected = endOfComment, unexpectedWidth = 1) - // It clearly wasn't the multi-line comment, so we are left with single line - else { - singleLineComment(ctx) - ctx.pushAndContinue(()) - } +private [internal] final class TokenWhiteSpace private ( + ws: Char => Boolean, + protected [this] val start: String, + protected [this] val end: String, + protected [this] val line: String, + protected [this] val nested: Boolean, + protected [this] val eofAllowed: Boolean, + protected [this] val endOfMultiComment: Option[ExpectItem], + protected [this] val endOfSingleComment: Option[ExpectDesc]) extends WhiteSpaceLike { + def this(ws: Char => Boolean, desc: SpaceDesc, errConfig: ErrorConfig) = { + this(ws, desc.commentStart, desc.commentEnd, desc.commentLine, desc.nestedComments, desc.commentLineAllowsEOF, + errConfig.labelSpaceEndOfMultiComment.asExpectItem(desc.commentEnd), + errConfig.labelSpaceEndOfLineComment.asExpectDesc("end of line")) } - - // $COVERAGE-OFF$ - override def toString: String = "TokenComment" - // $COVERAGE-ON$ -} - -private [internal] final class TokenWhiteSpace(ws: Char => Boolean, start: String, end: String, line: String, nested: Boolean, eofAllowed: Boolean) - extends WhiteSpaceLike(start, end, line, nested, eofAllowed) { override def spaces(ctx: Context): Unit = { while (ctx.moreInput && ws(ctx.nextChar)) { ctx.consumeChar() @@ -158,17 +185,28 @@ private [internal] final class TokenWhiteSpace(ws: Char => Boolean, start: Strin // $COVERAGE-ON$ } -private [internal] final class TokenSkipComments(start: String, end: String, line: String, nested: Boolean, eofAllowed: Boolean) - extends WhiteSpaceLike(start, end, line, nested, eofAllowed) { +private [internal] final class TokenSkipComments private ( + protected [this] val start: String, + protected [this] val end: String, + protected [this] val line: String, + protected [this] val nested: Boolean, + protected [this] val eofAllowed: Boolean, + protected [this] val endOfMultiComment: Option[ExpectItem], + protected [this] val endOfSingleComment: Option[ExpectDesc]) extends WhiteSpaceLike { + def this(desc: SpaceDesc, errConfig: ErrorConfig) = { + this(desc.commentStart, desc.commentEnd, desc.commentLine, desc.nestedComments, desc.commentLineAllowsEOF, + errConfig.labelSpaceEndOfMultiComment.asExpectItem(desc.commentEnd), + errConfig.labelSpaceEndOfLineComment.asExpectDesc("end of line")) + } override def spaces(ctx: Context): Unit = () // $COVERAGE-OFF$ override def toString: String = "TokenSkipComments" // $COVERAGE-ON$ } -private [internal] final class TokenNonSpecific(name: String, illegalName: String) +private [internal] final class TokenNonSpecific(name: String, unexpectedIllegal: String => String) (start: Char => Boolean, letter: Char => Boolean, illegal: String => Boolean) extends Instr { - private [this] final val expected = Some(ExpectDesc(name)) + private [this] final val expected = Some(new ExpectDesc(name)) override def apply(ctx: Context): Unit = { ensureRegularInstruction(ctx) @@ -183,7 +221,7 @@ private [internal] final class TokenNonSpecific(name: String, illegalName: Strin private def ensureLegal(ctx: Context, tok: String) = { if (illegal(tok)) { ctx.offset -= tok.length - ctx.unexpectedFail(expected = expected, unexpected = new UnexpectDesc(s"$illegalName $tok", tok.length)) + ctx.unexpectedFail(expected = expected, unexpected = new UnexpectDesc(unexpectedIllegal(tok), tok.length)) } else { ctx.col += tok.length @@ -204,10 +242,11 @@ private [internal] final class TokenNonSpecific(name: String, illegalName: Strin // $COVERAGE-ON$ } -private [instructions] abstract class TokenSpecificAllowTrailing(_specific: String, caseSensitive: Boolean) extends Instr { - private [this] final val expected = Some(ExpectDesc(_specific)) - protected final val expectedEnd = Some(ExpectDesc(s"end of ${_specific}")) - private [this] final val specific = (if (caseSensitive) _specific else _specific.toLowerCase) +private [instructions] abstract class TokenSpecificAllowTrailing( + specific: String, expected: Option[ExpectDesc], protected final val expectedEnd: Option[ExpectDesc], caseSensitive: Boolean) extends Instr { + def this(specific: String, expected: LabelConfig, expectedEnd: String, caseSensitive: Boolean) = { + this(if (caseSensitive) specific else specific.toLowerCase, expected.asExpectDesc, Some(new ExpectDesc(expectedEnd)), caseSensitive) + } private [this] final val strsz = specific.length private [this] final val numCodePoints = specific.codePointCount(0, strsz) protected def postprocess(ctx: Context, i: Int): Unit @@ -233,8 +272,8 @@ private [instructions] abstract class TokenSpecificAllowTrailing(_specific: Stri } } -private [internal] final class TokenSpecific(_specific: String, letter: Char => Boolean, caseSensitive: Boolean) - extends TokenSpecificAllowTrailing(_specific, caseSensitive) { +private [internal] final class TokenSpecific(specific: String, expected: LabelConfig, _expectedEnd: String, letter: Char => Boolean, caseSensitive: Boolean) + extends TokenSpecificAllowTrailing(specific, expected, _expectedEnd, caseSensitive) { override def postprocess(ctx: Context, i: Int): Unit = { if (i < ctx.inputsz && letter(ctx.input.charAt(i))) { ctx.expectedFail(expectedEnd, unexpectedWidth = 1) //This should only report a single token @@ -247,7 +286,7 @@ private [internal] final class TokenSpecific(_specific: String, letter: Char => } // $COVERAGE-OFF$ - override def toString: String = s"TokenSpecific(${_specific})" + override def toString: String = s"TokenSpecific($specific)" // $COVERAGE-ON$ } diff --git a/parsley/shared/src/main/scala/parsley/internal/machine/stacks/HandlerStack.scala b/parsley/shared/src/main/scala/parsley/internal/machine/stacks/HandlerStack.scala index 7f20de1ff..2827d905e 100644 --- a/parsley/shared/src/main/scala/parsley/internal/machine/stacks/HandlerStack.scala +++ b/parsley/shared/src/main/scala/parsley/internal/machine/stacks/HandlerStack.scala @@ -3,16 +3,23 @@ */ package parsley.internal.machine.stacks -private [machine] final class HandlerStack(val depth: Int, val pc: Int, val stacksz: Int, val tail: HandlerStack) +import parsley.internal.machine.instructions.Instr + +private [machine] final class HandlerStack( + val calls: CallStack, + val instrs: Array[Instr], + val pc: Int, + val stacksz: Int, + val tail: HandlerStack) private [machine] object HandlerStack extends Stack[HandlerStack] { implicit val inst: Stack[HandlerStack] = this - type ElemTy = (Int, Int, Int) + type ElemTy = (Int, Int) // $COVERAGE-OFF$ override protected def show(x: ElemTy): String = { - val (depth, pc, stacksz) = x - s"Handler@$depth:$pc(-${stacksz + 1})" + val (pc, stacksz) = x + s"Handler:$pc(-${stacksz + 1})" } - override protected def head(xs: HandlerStack): ElemTy = (xs.depth, xs.pc, xs.stacksz) + override protected def head(xs: HandlerStack): ElemTy = (xs.pc, xs.stacksz) override protected def tail(xs: HandlerStack): HandlerStack = xs.tail // $COVERAGE-ON$ } diff --git a/parsley/shared/src/main/scala/parsley/position.scala b/parsley/shared/src/main/scala/parsley/position.scala new file mode 100644 index 000000000..b5a6c2032 --- /dev/null +++ b/parsley/shared/src/main/scala/parsley/position.scala @@ -0,0 +1,86 @@ +/* SPDX-FileCopyrightText: © 2023 Parsley Contributors + * SPDX-License-Identifier: BSD-3-Clause + */ +package parsley + +import parsley.implicits.zipped.Zipped3 + +import parsley.internal.deepembedding.singletons + +// TODO: In future, the contents of this object will be made public, and the old versions +// will be deprecated for removal in 5.0.0 +private [parsley] object position { + /** This parser returns the current line number of the input without having any other effect. + * + * When this combinator is ran, no input is required, nor consumed, and + * the current line number will always be successfully returned. It has no other + * effect on the state of the parser. + * + * @example {{{ + * scala> import parsley.Parsley.line, parsley.character.char + * scala> line.parse("") + * val res0 = Success(1) + * scala> (char('a') *> line).parse("a") + * val res0 = Success(1) + * scala> (char('\n') *> line).parse("\n") + * val res0 = Success(2) + * }}} + * + * @return a parser that returns the line number the parser is currently at. + * @group pos + */ + val line: Parsley[Int] = new Parsley(singletons.Line) + /** This parser returns the current column number of the input without having any other effect. + * + * When this combinator is ran, no input is required, nor consumed, and + * the current column number will always be successfully returned. It has no other + * effect on the state of the parser. + * + * @example {{{ + * scala> import parsley.Parsley.col, parsley.character.char + * scala> col.parse("") + * val res0 = Success(1) + * scala> (char('a') *> col).parse("a") + * val res0 = Success(2) + * scala> (char('\n') *> col).parse("\n") + * val res0 = Success(1) + * }}} + * + * @return a parser that returns the column number the parser is currently at. + * @note in the presence of wide unicode characters, the value returned may be inaccurate. + * @group pos + */ + val col: Parsley[Int] = new Parsley(singletons.Col) + /** This parser returns the current line and column numbers of the input without having any other effect. + * + * When this combinator is ran, no input is required, nor consumed, and + * the current line and column number will always be successfully returned. It has no other + * effect on the state of the parser. + * + * @example {{{ + * scala> import parsley.Parsley.pos, parsley.character.char + * scala> pos.parse("") + * val res0 = Success((1, 1)) + * scala> (char('a') *> pos).parse("a") + * val res0 = Success((1, 2)) + * scala> (char('\n') *> pos).parse("\n") + * val res0 = Success((2, 1)) + * }}} + * + * @return a parser that returns the line and column number the parser is currently at. + * @note in the presence of wide unicode characters, the column value returned may be inaccurate. + * @group pos + */ + val pos: Parsley[(Int, Int)] = line <~> col + + // this is subject to change at the slightest notice, do NOT expose + private [parsley] val internalOffset: Parsley[Int] = new Parsley(singletons.Offset) + // IMPORTANT: this is NOT to be released until a Int/Long stance has been taken + // for offset in the deepest internals + // We could use `BigInt` as an arbiter here, and just declare it's expensive? + private [parsley] val offset: Parsley[BigInt] = internalOffset.map(BigInt(_)) + + def spanWith[A, S](end: Parsley[S])(p: Parsley[A]): Parsley[(S, A, S)] = (end, p, end).zipped + // this is subject to change at the slightest notice, do NOT expose + private [parsley] def internalOffsetSpan[A](p: Parsley[A]): Parsley[(Int, A, Int)] = spanWith(internalOffset)(p) +} diff --git a/parsley/shared/src/main/scala/parsley/token/Lexer.scala b/parsley/shared/src/main/scala/parsley/token/Lexer.scala index 927bb11e2..4706a5407 100644 --- a/parsley/shared/src/main/scala/parsley/token/Lexer.scala +++ b/parsley/shared/src/main/scala/parsley/token/Lexer.scala @@ -4,7 +4,6 @@ package parsley.token import parsley.Parsley, Parsley.{attempt, unit} -import parsley.XCompat.unused import parsley.character.satisfyUtf16 import parsley.combinator.{between, eof, sepBy, sepBy1, skipMany} import parsley.errors.combinator.{markAsToken, ErrorMethods} @@ -27,7 +26,8 @@ private [token] abstract class Lexeme { def apply[A](p: Parsley[A]): Parsley[A] } - +// TODO: flatten out `numeric` and `text` (and `enclosing` and `separators`) for 5.0.0? wouldn't do much damage, +// we can use documentation tags to group them in the high-level docs again :) /** This class provides a large selection of functionality concerned * with lexing. * @@ -224,14 +224,17 @@ private [token] abstract class Lexeme { * the lexer. * @since 4.0.0 */ -class Lexer private[parsley] (desc: descriptions.LexicalDesc, @unused errConfig: errors.ErrorConfig) { +@deprecatedInheritance("this class will be made final in 5.0.0", since = "4.1.0") +class Lexer(desc: descriptions.LexicalDesc, errConfig: errors.ErrorConfig) { /** Builds a new lexer with a given description for the lexical structure of the language. * * @param desc the configuration for the lexer, specifying the lexical * rules of the grammar/language being parsed. * @since 4.0.0 */ - def this(desc: descriptions.LexicalDesc) = this(desc, errors.ErrorConfig.default) + def this(desc: descriptions.LexicalDesc) = this(desc, new errors.ErrorConfig) + + private val generic = new numeric.Generic(errConfig) /** This object is concerned with ''lexemes'': these are tokens that are * treated as "words", such that whitespace will be consumed after each @@ -279,6 +282,13 @@ class Lexer private[parsley] (desc: descriptions.LexicalDesc, @unused errConfig: * @since 4.0.0 */ object numeric { + private [Lexer] val _natural = new LexemeInteger(nonlexeme.numeric.natural, lexeme) + private [Lexer] val _integer = new LexemeInteger(nonlexeme.numeric.integer, lexeme) + private [Lexer] val _positiveReal = new LexemeReal(nonlexeme.numeric._positiveReal, lexeme, errConfig) + private [Lexer] val _real = new LexemeReal(nonlexeme.numeric.real, lexeme, errConfig) + private [Lexer] val _unsignedCombined = new LexemeCombined(nonlexeme.numeric.unsignedCombined, lexeme, errConfig) + private [Lexer] val _signedCombined = new LexemeCombined(nonlexeme.numeric.signedCombined, lexeme, errConfig) + /** $natural * * @since 4.0.0 @@ -291,7 +301,7 @@ class Lexer private[parsley] (desc: descriptions.LexicalDesc, @unused errConfig: * * @since 4.0.0 */ - val natural: parsley.token.numeric.Integer = new LexemeInteger(nonlexeme.numeric.natural, lexeme) + def natural: parsley.token.numeric.Integer = _natural /** $integer * @@ -307,7 +317,7 @@ class Lexer private[parsley] (desc: descriptions.LexicalDesc, @unused errConfig: * @since 4.0.0 * @see [[natural `natural`]] for a full description of integer configuration */ - val integer: parsley.token.numeric.Integer = new LexemeInteger(nonlexeme.numeric.integer, lexeme) + def integer: parsley.token.numeric.Integer = _integer /** $real * @@ -318,24 +328,23 @@ class Lexer private[parsley] (desc: descriptions.LexicalDesc, @unused errConfig: // $COVERAGE-OFF$ def floating: parsley.token.numeric.Real = real // $COVERAGE-ON$ - private [Lexer] val positiveReal = new LexemeReal(nonlexeme.numeric.positiveReal, lexeme) /** $real * * @since 4.0.0 * @see [[natural `natural`]] and [[integer `integer`]] for a full description of the configuration for the start of a real number */ - val real: parsley.token.numeric.Real = new LexemeReal(nonlexeme.numeric.real, lexeme) + def real: parsley.token.numeric.Real = _real /** $unsignedCombined * * @since 4.0.0 */ - val unsignedCombined: parsley.token.numeric.Combined = new LexemeCombined(nonlexeme.numeric.unsignedCombined, lexeme) + def unsignedCombined: parsley.token.numeric.Combined = _unsignedCombined /** $signedCombined * * @since 4.0.0 */ - val signedCombined: parsley.token.numeric.Combined = new LexemeCombined(nonlexeme.numeric.signedCombined, lexeme) + def signedCombined: parsley.token.numeric.Combined = _signedCombined } /** $text @@ -343,40 +352,46 @@ class Lexer private[parsley] (desc: descriptions.LexicalDesc, @unused errConfig: * @since 4.0.0 */ object text { + private [Lexer] val _character = new LexemeCharacter(nonlexeme.text._character, lexeme) + private [Lexer] val _string = new LexemeString(nonlexeme.text._string, lexeme) + private [Lexer] val _rawString = new LexemeString(nonlexeme.text._rawString, lexeme) + private [Lexer] val _multiString = new LexemeString(nonlexeme.text._multiString, lexeme) + private [Lexer] val _rawMultiString = new LexemeString(nonlexeme.text._rawMultiString, lexeme) + /** $character * * @since 4.0.0 */ - val character: parsley.token.text.Character = new LexemeCharacter(nonlexeme.text.character, lexeme) + def character: parsley.token.text.Character = _character /** $string * * @since 4.0.0 */ - val string: parsley.token.text.String = new LexemeString(nonlexeme.text.string, lexeme) + def string: parsley.token.text.String = _string /** $string * * @note $raw * @since 4.0.0 */ - val rawString: parsley.token.text.String = new LexemeString(nonlexeme.text.rawString, lexeme) + def rawString: parsley.token.text.String = _rawString /** $multiString * * @since 4.0.0 */ - val multiString: parsley.token.text.String = new LexemeString(nonlexeme.text.multiString, lexeme) + def multiString: parsley.token.text.String = _multiString /** $multiString * * @note $raw * @since 4.0.0 */ - val rawMultiString: parsley.token.text.String = new LexemeString(nonlexeme.text.rawMultiString, lexeme) + def rawMultiString: parsley.token.text.String = _rawMultiString } /** $symbol * * @since 4.0.0 */ - val symbol: parsley.token.symbol.Symbol = new LexemeSymbol(nonlexeme.symbol, this) + val symbol: parsley.token.symbol.Symbol = new LexemeSymbol(nonlexeme.symbol, this, errConfig) /** This object contains helper combinators for parsing terms separated by * common symbols. @@ -603,26 +618,34 @@ class Lexer private[parsley] (desc: descriptions.LexicalDesc, @unused errConfig: * * @since 4.0.0 */ - val names: parsley.token.names.Names = new ConcreteNames(desc.nameDesc, desc.symbolDesc) + val names: parsley.token.names.Names = new ConcreteNames(desc.nameDesc, desc.symbolDesc, errConfig) /** $numeric * * @since 4.0.0 */ object numeric { + private [Lexer] val _natural = new UnsignedInteger(desc.numericDesc, errConfig, generic) + private [Lexer] val _integer = new SignedInteger(desc.numericDesc, _natural, errConfig) + private [Lexer] val _positiveReal = new UnsignedReal(desc.numericDesc, _natural, errConfig, generic) + private [Lexer] val _real = new SignedReal(desc.numericDesc, _positiveReal, errConfig) + private [Lexer] val _unsignedCombined = new UnsignedCombined(desc.numericDesc, _integer, _positiveReal, errConfig) + private [Lexer] val _signedCombined = new SignedCombined(desc.numericDesc, _unsignedCombined, errConfig) + /** $natural * * @since 4.0.0 * @note alias for [[natural `natural`]]. */ // $COVERAGE-OFF$ - def unsigned: parsley.token.numeric.Integer = natural + def unsigned: parsley.token.numeric.Integer = _natural // $COVERAGE-ON$ + /** $natural * * @since 4.0.0 */ - val natural: parsley.token.numeric.Integer = new UnsignedInteger(desc.numericDesc) + def natural: parsley.token.numeric.Integer = _natural /** $integer * @@ -631,14 +654,14 @@ class Lexer private[parsley] (desc: descriptions.LexicalDesc, @unused errConfig: * @see [[unsigned `unsigned`]] for a full description of signed integer configuration */ // $COVERAGE-OFF$ - def signed: parsley.token.numeric.Integer = integer + def signed: parsley.token.numeric.Integer = _integer // $COVERAGE-ON$ /** $integer * * @since 4.0.0 * @see [[natural `natural`]] for a full description of integer configuration */ - val integer: parsley.token.numeric.Integer = new SignedInteger(desc.numericDesc, natural) + def integer: parsley.token.numeric.Integer = _integer /** $real * @@ -649,24 +672,24 @@ class Lexer private[parsley] (desc: descriptions.LexicalDesc, @unused errConfig: // $COVERAGE-OFF$ def floating: parsley.token.numeric.Real = real // $COVERAGE-ON$ - private [Lexer] val positiveReal = new UnsignedReal(desc.numericDesc, natural) + /** $real * * @since 4.0.0 * @see [[natural `natural`]] and [[integer `integer`]] for a full description of the configuration for the start of a real number */ - val real: parsley.token.numeric.Real = new SignedReal(desc.numericDesc, positiveReal) + def real: parsley.token.numeric.Real = _real /** $unsignedCombined * * @since 4.0.0 */ - val unsignedCombined: parsley.token.numeric.Combined = new UnsignedCombined(desc.numericDesc, integer, positiveReal) + def unsignedCombined: parsley.token.numeric.Combined = _unsignedCombined /** $signedCombined * * @since 4.0.0 */ - val signedCombined: parsley.token.numeric.Combined = new SignedCombined(desc.numericDesc, unsignedCombined) + def signedCombined: parsley.token.numeric.Combined = _signedCombined } /** $text @@ -674,47 +697,49 @@ class Lexer private[parsley] (desc: descriptions.LexicalDesc, @unused errConfig: * @since 4.0.0 */ object text { - private val escapes = new Escape(desc.textDesc.escapeSequences) - private val escapeChar = new EscapableCharacter(desc.textDesc.escapeSequences, escapes, space.space) + private val escapes = new Escape(desc.textDesc.escapeSequences, errConfig, generic) + private val escapeChar = new EscapableCharacter(desc.textDesc.escapeSequences, escapes, space.space, errConfig) + private val rawChar = new RawCharacter(errConfig) + private [Lexer] val _character: parsley.token.text.Character = new ConcreteCharacter(desc.textDesc, escapes, errConfig) + private [Lexer] val _string = new ConcreteString(desc.textDesc.stringEnds, escapeChar, desc.textDesc.graphicCharacter, false, errConfig) + private [Lexer] val _rawString = new ConcreteString(desc.textDesc.stringEnds, rawChar, desc.textDesc.graphicCharacter, false, errConfig) + private [Lexer] val _multiString = new ConcreteString(desc.textDesc.multiStringEnds, escapeChar, desc.textDesc.graphicCharacter, true, errConfig) + private [Lexer] val _rawMultiString = new ConcreteString(desc.textDesc.multiStringEnds, rawChar, desc.textDesc.graphicCharacter, true, errConfig) /** $character * * @since 4.0.0 */ - val character: parsley.token.text.Character = new ConcreteCharacter(desc.textDesc, escapes) + def character: parsley.token.text.Character = _character /** $string * * @since 4.0.0 */ - val string: parsley.token.text.String = - new ConcreteString(desc.textDesc.stringEnds, escapeChar, desc.textDesc.graphicCharacter, false) + def string: parsley.token.text.String = _string /** $string * * @note $raw * @since 4.0.0 */ - val rawString: parsley.token.text.String = - new ConcreteString(desc.textDesc.stringEnds, RawCharacter, desc.textDesc.graphicCharacter, false) + def rawString: parsley.token.text.String = _rawString /** $multiString * * @since 4.0.0 */ - val multiString: parsley.token.text.String = - new ConcreteString(desc.textDesc.multiStringEnds, escapeChar, desc.textDesc.graphicCharacter, true) + def multiString: parsley.token.text.String = _multiString /** $multiString * * @note $raw * @since 4.0.0 */ - val rawMultiString: parsley.token.text.String = - new ConcreteString(desc.textDesc.multiStringEnds, RawCharacter, desc.textDesc.graphicCharacter, true) + def rawMultiString: parsley.token.text.String = _rawMultiString } /** $symbol * * @since 4.0.0 */ - val symbol: parsley.token.symbol.Symbol = new ConcreteSymbol(desc.nameDesc, desc.symbolDesc) + val symbol: parsley.token.symbol.Symbol = new ConcreteSymbol(desc.nameDesc, desc.symbolDesc, errConfig) } /** This combinator ensures a parser fully parses all available input, and consumes whitespace @@ -804,7 +829,7 @@ class Lexer private[parsley] (desc: descriptions.LexicalDesc, @unused errConfig: * @since 4.0.0 */ val whiteSpace: Parsley[Unit] = { - if (desc.spaceDesc.whitespaceIsContextDependent) wsImpl.get.flatten.hide + if (desc.spaceDesc.whitespaceIsContextDependent) wsImpl.get.flatten else configuredWhiteSpace } @@ -819,26 +844,17 @@ class Lexer private[parsley] (desc: descriptions.LexicalDesc, @unused errConfig: */ lazy val skipComments: Parsley[Unit] = { if (!desc.spaceDesc.supportsComments) unit - else { - new Parsley(new singletons.SkipComments(desc.spaceDesc.commentStart, desc.spaceDesc.commentEnd, - desc.spaceDesc.commentLine, desc.spaceDesc.nestedComments, - desc.spaceDesc.commentLineAllowsEOF)).hide - } + else new Parsley(new singletons.SkipComments(desc.spaceDesc, errConfig)) } private def configuredWhiteSpace: Parsley[Unit] = whiteSpace(desc.spaceDesc.space) private def whiteSpace(impl: CharPredicate): Parsley[Unit] = impl match { case NotRequired => skipComments - case Basic(ws) => new Parsley(new singletons.WhiteSpace(ws, desc.spaceDesc.commentStart, desc.spaceDesc.commentEnd, - desc.spaceDesc.commentLine, desc.spaceDesc.nestedComments, - desc.spaceDesc.commentLineAllowsEOF)).hide + case Basic(ws) => new Parsley(new singletons.WhiteSpace(ws, desc.spaceDesc, errConfig)) + // satisfyUtf16 is effectively hidden, and so is Comment case Unicode(ws) if desc.spaceDesc.supportsComments => - skipMany(attempt(new Parsley(new singletons.Comment(desc.spaceDesc.commentStart, - desc.spaceDesc.commentEnd, - desc.spaceDesc.commentLine, - desc.spaceDesc.nestedComments, - desc.spaceDesc.commentLineAllowsEOF))) <|> satisfyUtf16(ws)).hide - case Unicode(ws) => skipMany(satisfyUtf16(ws)).hide + skipMany(attempt(new Parsley(new singletons.Comment(desc.spaceDesc, errConfig))) <|> satisfyUtf16(ws))//.hide + case Unicode(ws) => skipMany(satisfyUtf16(ws))//.hide } } } diff --git a/parsley/shared/src/main/scala/parsley/token/errors/ConfigImplTyped.scala b/parsley/shared/src/main/scala/parsley/token/errors/ConfigImplTyped.scala new file mode 100644 index 000000000..82b4f7b90 --- /dev/null +++ b/parsley/shared/src/main/scala/parsley/token/errors/ConfigImplTyped.scala @@ -0,0 +1,210 @@ +/* SPDX-FileCopyrightText: © 2023 Parsley Contributors + * SPDX-License-Identifier: BSD-3-Clause + */ +package parsley.token.errors + +import parsley.Parsley, Parsley.pure +import parsley.errors.combinator, combinator.ErrorMethods +import parsley.position + +private [parsley] object FilterOps { + def amendThenDislodge[A](full: Boolean)(p: Parsley[A]): Parsley[A] = { + if (full) combinator.amendThenDislodge(p) + else p + } + def amendThenDislodgeOrPartial[A](full: Boolean)(p: Parsley[A]): Parsley[A] = { + if (full) combinator.amendThenDislodge(p) + else combinator.partialAmendThenDislodge(p) + } + def entrench[A](full: Boolean)(p: Parsley[A]): Parsley[A] = { + if (full) combinator.entrench(p) + else p + } +} + +/** This trait, and its subclasses, can be used to configure how filters should be used within the `Lexer`. + * @since 4.1.0 + * @group filters + */ +trait FilterConfig[A] { + private [parsley] def filter(p: Parsley[A])(f: A => Boolean): Parsley[A] + // $COVERAGE-OFF$ + private [parsley] def collect[B](p: Parsley[A])(f: PartialFunction[A, B]): Parsley[B] = this.filter(p)(f.isDefinedAt).map(f) + private [parsley] def injectLeft[B]: FilterConfig[Either[A, B]] + private [parsley] def injectRight[B]: FilterConfig[Either[B, A]] + // $COVERAGE-ON$ +} + +/** This subtrait of `FilterConfig` specifies that only filters generating ''specialised'' errors may be used. + * @since 4.1.0 + * @group filters + */ +trait SpecialisedFilterConfig[A] extends FilterConfig[A] +/** This subtrait of `FilterConfig` specifies that only filters generating ''vanilla'' errors may be used. + * @since 4.1.0 + * @group filters + */ +trait VanillaFilterConfig[A] extends FilterConfig[A] + +/** This class ensures that the filter will generate ''specialised'' messages for the given failing parse. + * @since 4.1.0 + * @param fullAmend filters usually have partial amend semantics: should this instead do a full amend? + * @group filters + */ +abstract class SpecialisedMessage[A](fullAmend: Boolean) extends SpecialisedFilterConfig[A] { self => + /** This method produces the messages for the given value. + * @since 4.1.0 + * @group badchar + */ + def message(x: A): Seq[String] + + private [parsley] final override def filter(p: Parsley[A])(f: A => Boolean) = FilterOps.amendThenDislodge(fullAmend) { + FilterOps.entrench(fullAmend)(p).guardAgainst { + case x if !f(x) => message(x) + } + } + private [parsley] final override def collect[B](p: Parsley[A])(f: PartialFunction[A, B]) = FilterOps.amendThenDislodge(fullAmend) { + FilterOps.entrench(fullAmend)(p).collectMsg(message(_))(f) + } + // $COVERAGE-OFF$ + private [parsley] final override def injectLeft[B] = new SpecialisedMessage[Either[A, B]](fullAmend) { + def message(xy: Either[A, B]) = { + val Left(x) = xy + self.message(x) + } + } + private [parsley] final override def injectRight[B] = new SpecialisedMessage[Either[B, A]](fullAmend) { + def message(xy: Either[B, A]) = { + val Right(y) = xy + self.message(y) + } + } + // $COVERAGE-ON$ +} + +/** This class ensures that the filter will generate a ''vanilla'' unexpected item for the given failing parse. + * @since 4.1.0 + * @param fullAmend filters usually have partial amend semantics: should this instead do a full amend? + * @group filters + */ +abstract class Unexpected[A](fullAmend: Boolean) extends VanillaFilterConfig[A] { self => + /** This method produces the unexpected label for the given value. + * @since 4.1.0 + * @group badchar + */ + def unexpected(x: A): String + + private [parsley] final override def filter(p: Parsley[A])(f: A => Boolean) = FilterOps.amendThenDislodge(fullAmend) { + FilterOps.entrench(fullAmend)(p).unexpectedWhen { + case x if !f(x) => unexpected(x) + } + } + // $COVERAGE-OFF$ + private [parsley] final override def injectLeft[B] = new Unexpected[Either[A, B]](fullAmend) { + def unexpected(xy: Either[A, B]) = { + val Left(x) = xy + self.unexpected(x) + } + } + private [parsley] final override def injectRight[B] = new Unexpected[Either[B, A]](fullAmend) { + def unexpected(xy: Either[B, A]) = { + val Right(y) = xy + self.unexpected(y) + } + } + // $COVERAGE-ON$ +} + +/** This class ensures that the filter will generate a ''vanilla'' reason for the given failing parse. + * @since 4.1.0 + * @param fullAmend filters usually have partial amend semantics: should this instead do a full amend? + * @group filters + */ +abstract class Because[A](fullAmend: Boolean) extends VanillaFilterConfig[A] { self => + /** This method produces the reason for the given value. + * @since 4.1.0 + * @group badchar + */ + def reason(x: A): String + + private [parsley] final override def filter(p: Parsley[A])(f: A => Boolean) = FilterOps.amendThenDislodge(fullAmend) { + FilterOps.entrench(fullAmend)(p).filterOut { + case x if !f(x) => reason(x) + } + } + // $COVERAGE-OFF$ + private [parsley] final override def injectLeft[B] = new Because[Either[A, B]](fullAmend) { + def reason(xy: Either[A, B]) = { + val Left(x) = xy + self.reason(x) + } + } + private [parsley] final override def injectRight[B] = new Because[Either[B, A]](fullAmend) { + def reason(xy: Either[B, A]) = { + val Right(y) = xy + self.reason(y) + } + } + // $COVERAGE-ON$ +} + +/** This class ensures that the filter will generate a ''vanilla'' unexpected item and a reason for the given failing parse. + * @since 4.1.0 + * @param fullAmend filters usually have partial amend semantics: should this instead do a full amend? + * @group filters + */ +abstract class UnexpectedBecause[A](fullAmend: Boolean) extends VanillaFilterConfig[A] { self => + /** This method produces the unexpected label for the given value. + * @since 4.1.0 + * @group badchar + */ + def unexpected(x: A): String + /** This method produces the reason for the given value. + * @since 4.1.0 + * @group badchar + */ + def reason(x: A): String + + // TODO: factor this combinator out with the "Great Move" in 4.2 + private [parsley] final override def filter(p: Parsley[A])(f: A => Boolean) = FilterOps.amendThenDislodgeOrPartial(fullAmend) { + position.internalOffsetSpan(combinator.entrench(p)).flatMap { case (os, x, oe) => + if (f(x)) combinator.unexpected(oe - os, this.unexpected(x)).explain(reason(x)) + else pure(x) + } + } + // $COVERAGE-OFF$ + private [parsley] final override def injectLeft[B] = new UnexpectedBecause[Either[A, B]](fullAmend) { + def unexpected(xy: Either[A, B]) = { + val Left(x) = xy + self.unexpected(x) + } + def reason(xy: Either[A, B]) = { + val Left(x) = xy + self.reason(x) + } + } + private [parsley] final override def injectRight[B] = new UnexpectedBecause[Either[B, A]](fullAmend) { + def unexpected(xy: Either[B, A]) = { + val Right(y) = xy + self.unexpected(y) + } + def reason(xy: Either[B, A]) = { + val Right(x) = xy + self.reason(x) + } + } + // $COVERAGE-ON$ +} + +/** This class can be used to not specify an error configuration for the filter, a regular `filter` is used instead. + * @since 4.1.0 + * @group filters + */ +final class BasicFilter[A] extends SpecialisedFilterConfig[A] with VanillaFilterConfig[A] { + private [parsley] final override def filter(p: Parsley[A])(f: A => Boolean) = p.filter(f) + private [parsley] final override def collect[B](p: Parsley[A])(f: PartialFunction[A, B]) = p.collect(f) + // $COVERAGE-OFF$ + private [parsley] final override def injectLeft[B] = new BasicFilter[Either[A, B]] + private [parsley] final override def injectRight[B] = new BasicFilter[Either[B, A]] + // $COVERAGE-ON$ +} diff --git a/parsley/shared/src/main/scala/parsley/token/errors/ConfigImplUntyped.scala b/parsley/shared/src/main/scala/parsley/token/errors/ConfigImplUntyped.scala new file mode 100644 index 000000000..0044c2f25 --- /dev/null +++ b/parsley/shared/src/main/scala/parsley/token/errors/ConfigImplUntyped.scala @@ -0,0 +1,136 @@ +/* SPDX-FileCopyrightText: © 2023 Parsley Contributors + * SPDX-License-Identifier: BSD-3-Clause + */ +package parsley.token.errors + +import parsley.Parsley +import parsley.XCompat.unused +import parsley.errors.combinator.ErrorMethods + +// This feels wrong? perhaps token is the wrong package +// Because this is now used for Char, Sat, String to encode the label config... +import parsley.internal.errors.{ExpectDesc, ExpectItem, ExpectRaw} + +private [parsley] sealed trait ConfigImplUntyped { + private [parsley] def apply[A](p: Parsley[A]): Parsley[A] +} + +// TODO: move into internal? +// Relaxing Types +private [parsley] trait LabelOps { + private [parsley] def asExpectDesc: Option[ExpectDesc] + private [parsley] def asExpectDesc(otherwise: String): Option[ExpectDesc] + private [parsley] def asExpectItem(raw: String): Option[ExpectItem] + private [parsley] final def asExpectItem(raw: Char): Option[ExpectItem] = asExpectItem(s"$raw") +} + +// TODO: reason extraction, maybe tie into errors? +private [parsley] trait ExplainOps + + +// Constraining Types +/** This type can be used to configure ''both'' errors that make labels and those that make reasons. + * @since 4.1.0 + * @group labels + */ +trait LabelWithExplainConfig extends ConfigImplUntyped with LabelOps with ExplainOps { + private [parsley] def orElse(other: LabelWithExplainConfig): LabelWithExplainConfig +} +/** This type can be used to configure errors that make labels. + * @since 4.1.0 + * @group labels + */ +trait LabelConfig extends LabelWithExplainConfig { + private [parsley] def orElse(other: LabelConfig): LabelConfig +} +/** This type can be used to configure errors that make reasons. + * @since 4.1.0 + * @group labels + */ +trait ExplainConfig extends LabelWithExplainConfig + +private final class Label private[errors] (val label: String) extends LabelConfig { + private [parsley] final override def apply[A](p: Parsley[A]) = p.label(label) + private [parsley] final override def asExpectDesc = Some(ExpectDesc(label)) + private [parsley] final override def asExpectDesc(@unused otherwise: String) = asExpectDesc + private [parsley] final override def asExpectItem(@unused raw: String) = asExpectDesc + private [parsley] final override def orElse(config: LabelWithExplainConfig) = config match { + case r: Reason => new LabelAndReason(label, r.reason) + case lr: LabelAndReason => new LabelAndReason(label, lr.reason) + case _ => this + } + private [parsley] final override def orElse(config: LabelConfig) = this +} +/** This object has a factory for configurations producing labels: if the empty string is provided, this equivalent to [[Hidden `Hidden`]]. + * @since 4.1.0 + * @group labels + */ +object Label { + def apply(label: String): LabelConfig = if (label.isEmpty) Hidden else new Label(label) +} + +/** This object configures labels by stating that it must be hidden. + * @since 4.1.0 + * @group labels + */ +object Hidden extends LabelConfig { + private [parsley] final override def apply[A](p: Parsley[A]) = p.hide + private [parsley] final override def asExpectDesc = None + private [parsley] final override def asExpectDesc(@unused otherwise: String) = asExpectDesc + private [parsley] final override def asExpectItem(@unused raw: String) = asExpectDesc + private [parsley] final override def orElse(config: LabelWithExplainConfig) = this + private [parsley] final override def orElse(config: LabelConfig) = this +} + +private final class Reason private[errors] (val reason: String) extends ExplainConfig { + require(reason.nonEmpty, "reason cannot be empty, use `Label` instead") + private [parsley] final override def apply[A](p: Parsley[A]) = p.explain(reason) + private [parsley] final override def asExpectDesc = None + private [parsley] final override def asExpectDesc(otherwise: String) = Some(ExpectDesc(otherwise)) + private [parsley] final override def asExpectItem(raw: String) = Some(ExpectRaw(raw)) + private [parsley] final override def orElse(config: LabelWithExplainConfig) = config match { + case l: Label => new LabelAndReason(l.label, reason) + case lr: LabelAndReason => new LabelAndReason(lr.label, reason) + case _ => this + } +} +/** This object has a factory for configurations producing reasons: if the empty string is provided, this equivalent to [[NotConfigured `NotConfigured`]]. + * @since 4.1.0 + * @group labels + */ +object Reason { + def apply(reason: String): ExplainConfig = if (reason.nonEmpty) new Reason(reason) else NotConfigured +} + +private final class LabelAndReason private[errors] (val label: String, val reason: String) extends LabelWithExplainConfig { + private [parsley] final override def apply[A](p: Parsley[A]) = p.label(label).explain(reason) + private [parsley] final override def asExpectDesc = Some(ExpectDesc(label)) + private [parsley] final override def asExpectDesc(@unused otherwise: String) = asExpectDesc + private [parsley] final override def asExpectItem(@unused raw: String) = asExpectDesc + private [parsley] final override def orElse(config: LabelWithExplainConfig) = this +} +/** This object has a factory for configurations producing labels and reasons: if the empty label is provided, this equivalent to [[Hidden `Hidden`]] with no + * reason; if the empty reason is provided this is equivalent to [[Label `Label`]]. + * @since 4.1.0 + * @group labels + */ +object LabelAndReason { + def apply(label: String, reason: String): LabelWithExplainConfig = { + if (label.isEmpty) Hidden + else if (reason.nonEmpty) new LabelAndReason(label, reason) + else new Label(label) + } +} + +/** This object specifies that no special labels or reasons should be generated, and default errors should be used instead. + * @since 4.1.0 + * @group labels + */ +object NotConfigured extends LabelConfig with ExplainConfig with LabelWithExplainConfig { + private [parsley] final override def apply[A](p: Parsley[A]) = p + private [parsley] final override def asExpectDesc = None + private [parsley] final override def asExpectDesc(otherwise: String) = Some(ExpectDesc(otherwise)) + private [parsley] final override def asExpectItem(raw: String) = Some(ExpectRaw(raw)) + private [parsley] final override def orElse(config: LabelWithExplainConfig) = config + private [parsley] final override def orElse(config: LabelConfig) = config +} diff --git a/parsley/shared/src/main/scala/parsley/token/errors/ErrorConfig.scala b/parsley/shared/src/main/scala/parsley/token/errors/ErrorConfig.scala index 4d60d34f1..1fee4e5cd 100644 --- a/parsley/shared/src/main/scala/parsley/token/errors/ErrorConfig.scala +++ b/parsley/shared/src/main/scala/parsley/token/errors/ErrorConfig.scala @@ -3,10 +3,828 @@ */ package parsley.token.errors -private [parsley] // TODO: remove -case class ErrorConfig() +import parsley.XCompat.unused -private [parsley] // TODO: remove -object ErrorConfig { - val default = ErrorConfig() +/** This class is used to specify how errors should be produced by the + * [[parsley.token.Lexer `Lexer`]] class. + * + * The [[parsley.token.Lexer `Lexer`]] is set up to produce a variety of different + * errors via `label`-ing, `explain`-ing, and `filter`-ing, and some applications of + * the ''Verified'' and ''Preventative'' error patterns. The exact content of those + * errors can be configured here. Errors can be suppressed or specified with different + * levels of detail, or even switching between ''vanilla'' or ''specialised'' errors. + * + * This class should be used by extending it and overriding the relevant parts: all + * methods here are non-abstract and their default is documented inside. Not configuring + * something does not mean it will not appear in the message, but will mean it uses the + * underlying base errors. + * + * @since 4.1.0 + * @group errconfig + * + * @groupprio numeric 0 + * @groupname numeric Numeric Errors + * @groupdesc numeric These control the errors generated with the `numeric` component of the `Lexer`. + * + * @groupprio text 0 + * @groupname text Text Errors + * @groupdesc text These control the errors generated with the `text` component of the `Lexer`. + * + * @groupprio names 0 + * @groupname names Name Errors + * @groupdesc names These control the errors generated with the `names` component of the `Lexer`. + * + * @groupprio symbol 0 + * @groupname symbol Symbol Errors + * @groupdesc symbol These control the errors generated with the `symbol` component of the `Lexer`. + * + * @groupprio space 0 + * @groupname space Space Errors + * @groupdesc space These control the errors generated with the `space` component of the `Lexer`. + */ +class ErrorConfig { + // numeric + /** How a numeric break character should (like `_`) be referred to or explained within an error. + * @since 4.1.0 + * @group numeric + */ + def labelNumericBreakChar: LabelWithExplainConfig = NotConfigured + + /** How unsigned decimal integers should be referred to or explained within an error. + * @since 4.1.0 + * @note defaults to [[parsley.token.errors.ErrorConfig.labelIntegerUnsignedNumber:* `labelIntegerUnsignedNumber`]] + * @group numeric + */ + def labelIntegerUnsignedDecimal: LabelWithExplainConfig = labelIntegerUnsignedNumber + /** How unsigned hexadecimal integers should be referred to or explained within an error. + * @since 4.1.0 + * @note defaults to [[parsley.token.errors.ErrorConfig.labelIntegerUnsignedNumber:* `labelIntegerUnsignedNumber`]] + * @group numeric + */ + def labelIntegerUnsignedHexadecimal: LabelWithExplainConfig = labelIntegerUnsignedNumber + /** How unsigned octal integers should be referred to or explained within an error. + * @since 4.1.0 + * @note defaults to [[parsley.token.errors.ErrorConfig.labelIntegerUnsignedNumber:* `labelIntegerUnsignedNumber`]] + * @group numeric + */ + def labelIntegerUnsignedOctal: LabelWithExplainConfig = labelIntegerUnsignedNumber + /** How unsigned binary integers should be referred to or explained within an error. + * @since 4.1.0 + * @note defaults to [[parsley.token.errors.ErrorConfig.labelIntegerUnsignedNumber:* `labelIntegerUnsignedNumber`]] + * @group numeric + */ + def labelIntegerUnsignedBinary: LabelWithExplainConfig = labelIntegerUnsignedNumber + /** How generic unsigned integers should be referred to or explained within an error. + * @since 4.1.0 + * @note defaults to [[NotConfigured `NotConfigured`]] + * @group numeric + */ + def labelIntegerUnsignedNumber: LabelWithExplainConfig = NotConfigured + /** How unsigned decimal integers should of a given bit-width be referred to or explained within an error. + * @since 4.1.0 + * @note defaults to [[parsley.token.errors.ErrorConfig.labelIntegerUnsignedNumber(bits:Int):* `labelIntegerUnsignedNumber`]] + * @group numeric + */ + def labelIntegerUnsignedDecimal(@unused bits: Int): LabelWithExplainConfig = labelIntegerUnsignedDecimal + /** How unsigned hexadecimal integers should of a given bit-width be referred to or explained within an error. + * @since 4.1.0 + * @note defaults to [[parsley.token.errors.ErrorConfig.labelIntegerUnsignedNumber(bits:Int):* `labelIntegerUnsignedNumber`]] + * @group numeric + */ + def labelIntegerUnsignedHexadecimal(@unused bits: Int): LabelWithExplainConfig = labelIntegerUnsignedHexadecimal + /** How unsigned octal integers should of a given bit-width be referred to or explained within an error. + * @since 4.1.0 + * @note defaults to [[parsley.token.errors.ErrorConfig.labelIntegerUnsignedNumber(bits:Int):* `labelIntegerUnsignedNumber`]] + * @group numeric + */ + def labelIntegerUnsignedOctal(@unused bits: Int): LabelWithExplainConfig = labelIntegerUnsignedOctal + /** How unsigned binary integers should of a given bit-width be referred to or explained within an error. + * @since 4.1.0 + * @note defaults to [[parsley.token.errors.ErrorConfig.labelIntegerUnsignedNumber(bits:Int):* `labelIntegerUnsignedNumber`]] + * @group numeric + */ + def labelIntegerUnsignedBinary(@unused bits: Int): LabelWithExplainConfig = labelIntegerUnsignedBinary + /** How generic unsigned integers should of a given bit-width be referred to or explained within an error. + * @since 4.1.0 + * @note defaults to [[NotConfigured `NotConfigured`]] + * @group numeric + */ + def labelIntegerUnsignedNumber(@unused bits: Int): LabelWithExplainConfig = labelIntegerUnsignedNumber + + /** How signed decimal integers should be referred to or explained within an error. + * @since 4.1.0 + * @note defaults to [[parsley.token.errors.ErrorConfig.labelIntegerSignedNumber:* `labelIntegerSignedNumber`]] + * @group numeric + */ + def labelIntegerSignedDecimal: LabelWithExplainConfig = labelIntegerSignedNumber + /** How signed hexadecimal integers should be referred to or explained within an error. + * @since 4.1.0 + * @note defaults to [[parsley.token.errors.ErrorConfig.labelIntegerSignedNumber:* `labelIntegerSignedNumber`]] + * @group numeric + */ + def labelIntegerSignedHexadecimal: LabelWithExplainConfig = labelIntegerSignedNumber + /** How signed octal integers should be referred to or explained within an error. + * @since 4.1.0 + * @note defaults to [[parsley.token.errors.ErrorConfig.labelIntegerSignedNumber:* `labelIntegerSignedNumber`]] + * @group numeric + */ + def labelIntegerSignedOctal: LabelWithExplainConfig = labelIntegerSignedNumber + /** How signed binary integers should be referred to or explained within an error. + * @since 4.1.0 + * @note defaults to [[parsley.token.errors.ErrorConfig.labelIntegerSignedNumber:* `labelIntegerSignedNumber`]] + * @group numeric + */ + def labelIntegerSignedBinary: LabelWithExplainConfig = labelIntegerSignedNumber + /** How generic signed integers should be referred to or explained within an error. + * @since 4.1.0 + * @note defaults to [[NotConfigured `NotConfigured`]] + * @group numeric + */ + def labelIntegerSignedNumber: LabelWithExplainConfig = NotConfigured + /** How signed decimal integers should of a given bit-width be referred to or explained within an error. + * @since 4.1.0 + * @note defaults to [[parsley.token.errors.ErrorConfig.labelIntegerSignedDecimal(bits:Int):* `labelIntegerSignedDecimal`]] + * @group numeric + */ + def labelIntegerSignedDecimal(@unused bits: Int): LabelWithExplainConfig = labelIntegerSignedDecimal + /** How signed hexadecimal integers should of a given bit-width be referred to or explained within an error. + * @since 4.1.0 + * @note defaults to [[parsley.token.errors.ErrorConfig.labelIntegerSignedHexadecimal(bits:Int):* `labelIntegerSignedHexadecimal`]] + * @group numeric + */ + def labelIntegerSignedHexadecimal(@unused bits: Int): LabelWithExplainConfig = labelIntegerSignedHexadecimal + /** How signed octal integers should of a given bit-width be referred to or explained within an error. + * @since 4.1.0 + * @note defaults to [[parsley.token.errors.ErrorConfig.labelIntegerSignedOctal(bits:Int):* `labelIntegerSignedOctal`]] + * @group numeric + */ + def labelIntegerSignedOctal(@unused bits: Int): LabelWithExplainConfig = labelIntegerSignedOctal + /** How signed binary integers should of a given bit-width be referred to or explained within an error. + * @since 4.1.0 + * @note defaults to [[parsley.token.errors.ErrorConfig.labelIntegerSignedBinary(bits:Int):* `labelIntegerSignedBinary`]] + * @group numeric + */ + def labelIntegerSignedBinary(@unused bits: Int): LabelWithExplainConfig = labelIntegerSignedBinary + /** How generic signed integers should of a given bit-width be referred to or explained within an error. + * @since 4.1.0 + * @note defaults to [[NotConfigured `NotConfigured`]] + * @group numeric + */ + def labelIntegerSignedNumber(@unused bits: Int): LabelWithExplainConfig = labelIntegerSignedNumber + + /** How the fact that the end of a decimal integer literal is expected should be referred to within an error. + * @since 4.1.0 + * @note defaults to [[labelIntegerNumberEnd `labelIntegerNumberEnd`]] + * @group numeric + */ + def labelIntegerDecimalEnd: LabelConfig = labelIntegerNumberEnd + /** How the fact that the end of a hexadecimal integer literal is expected should be referred to within an error. + * @since 4.1.0 + * @note defaults to [[labelIntegerNumberEnd `labelIntegerNumberEnd`]] + * @group numeric + */ + def labelIntegerHexadecimalEnd: LabelConfig = labelIntegerNumberEnd + /** How the fact that the end of an octal integer literal is expected should be referred to within an error. + * @since 4.1.0 + * @note defaults to [[labelIntegerNumberEnd `labelIntegerNumberEnd`]] + * @group numeric + */ + def labelIntegerOctalEnd: LabelConfig = labelIntegerNumberEnd + /** How the fact that the end of a binary integer literal is expected should be referred to within an error. + * @since 4.1.0 + * @note defaults to [[labelIntegerNumberEnd `labelIntegerNumberEnd`]] + * @group numeric + */ + def labelIntegerBinaryEnd: LabelConfig = labelIntegerNumberEnd + /** How the fact that the end of a generic integer literal is expected should be referred to within an error. + * @since 4.1.0 + * @note defaults to [[NotConfigured `NotConfigured`]] + * @group numeric + */ + def labelIntegerNumberEnd: LabelConfig = NotConfigured + + /** How decimal reals should be referred to or explained within an error. + * @since 4.1.0 + * @note defaults to [[labelRealNumber `labelRealNumber`]] + * @group numeric + */ + def labelRealDecimal: LabelWithExplainConfig = labelRealNumber + /** How hexadecimal reals should be referred to or explained within an error. + * @since 4.1.0 + * @note defaults to [[labelRealNumber `labelRealNumber`]] + * @group numeric + */ + def labelRealHexadecimal: LabelWithExplainConfig = labelRealNumber + /** How octal reals should be referred to or explained within an error. + * @since 4.1.0 + * @note defaults to [[labelRealNumber `labelRealNumber`]] + * @group numeric + */ + def labelRealOctal: LabelWithExplainConfig = labelRealNumber + /** How binary reals should be referred to or explained within an error. + * @since 4.1.0 + * @note defaults to [[labelRealNumber `labelRealNumber`]] + * @group numeric + */ + def labelRealBinary: LabelWithExplainConfig = labelRealNumber + /** How generic reals should be referred to or explained within an error. + * @since 4.1.0 + * @note defaults to [[NotConfigured `NotConfigured`]] + * @group numeric + */ + def labelRealNumber: LabelWithExplainConfig = NotConfigured + /** How decimal floats should be referred to or explained within an error. + * @since 4.1.0 + * @note defaults to [[labelRealDecimal `labelRealDecimal`]] + * @group numeric + */ + def labelRealFloatDecimal: LabelWithExplainConfig = labelRealDecimal + /** How hexadecimal floats should be referred to or explained within an error. + * @since 4.1.0 + * @note defaults to [[labelRealHexadecimal `labelRealHexadecimal`]] + * @group numeric + */ + def labelRealFloatHexadecimal: LabelWithExplainConfig = labelRealHexadecimal + /** How octal floats should be referred to or explained within an error. + * @since 4.1.0 + * @note defaults to [[labelRealOctal `labelRealOctal`]] + * @group numeric + */ + def labelRealFloatOctal: LabelWithExplainConfig = labelRealOctal + /** How binary floats should be referred to or explained within an error. + * @since 4.1.0 + * @note defaults to [[labelRealBinary `labelRealBinary`]] + * @group numeric + */ + def labelRealFloatBinary: LabelWithExplainConfig = labelRealBinary + /** How generic floats should be referred to or explained within an error. + * @since 4.1.0 + * @note defaults to [[labelRealNumber `labelRealNumber`]] + * @group numeric + */ + def labelRealFloatNumber: LabelWithExplainConfig = labelRealNumber + /** How decimal doubles should be referred to or explained within an error. + * @since 4.1.0 + * @note defaults to [[labelRealDecimal `labelRealDecimal`]] + * @group numeric + */ + def labelRealDoubleDecimal: LabelWithExplainConfig = labelRealDecimal + /** How hexadecimal doubles should be referred to or explained within an error. + * @since 4.1.0 + * @note defaults to [[labelRealHexadecimal `labelRealHexadecimal`]] + * @group numeric + */ + def labelRealDoubleHexadecimal: LabelWithExplainConfig = labelRealHexadecimal + /** How octal doubles should be referred to or explained within an error. + * @since 4.1.0 + * @note defaults to [[labelRealOctal `labelRealOctal`]] + * @group numeric + */ + def labelRealDoubleOctal: LabelWithExplainConfig = labelRealOctal + /** How binary doubles should be referred to or explained within an error. + * @since 4.1.0 + * @note defaults to [[labelRealBinary `labelRealBinary`]] + * @group numeric + */ + def labelRealDoubleBinary: LabelWithExplainConfig = labelRealBinary + /** How generic doubles should be referred to or explained within an error. + * @since 4.1.0 + * @note defaults to [[labelRealNumber `labelRealNumber`]] + * @group numeric + */ + def labelRealDoubleNumber: LabelWithExplainConfig = labelRealNumber + + /** How the fact that the end of a decimal real literal is expected should be referred to within an error. + * @since 4.1.0 + * @note defaults to [[labelRealNumberEnd `labelRealNumberEnd`]] + * @group numeric + */ + def labelRealDecimalEnd: LabelConfig = labelRealNumberEnd + /** How the fact that the end of a hexadecimal real literal is expected should be referred to within an error. + * @since 4.1.0 + * @note defaults to [[labelRealNumberEnd `labelRealNumberEnd`]] + * @group numeric + */ + def labelRealHexadecimalEnd: LabelConfig = labelRealNumberEnd + /** How the fact that the end of an octal real literal is expected should be referred to within an error. + * @since 4.1.0 + * @note defaults to [[labelRealNumberEnd `labelRealNumberEnd`]] + * @group numeric + */ + def labelRealOctalEnd: LabelConfig = labelRealNumberEnd + /** How the fact that the end of a binary real literal is expected should be referred to within an error. + * @since 4.1.0 + * @note defaults to [[labelRealNumberEnd `labelRealNumberEnd`]] + * @group numeric + */ + def labelRealBinaryEnd: LabelConfig = labelRealNumberEnd + /** How the fact that the end of a generic real literal is expected should be referred to within an error. + * @since 4.1.0 + * @note defaults to [[NotConfigured `NotConfigured`]] + * @group numeric + */ + def labelRealNumberEnd: LabelConfig = NotConfigured + + /** How the "dot" that separates the integer and fractional part of a real number should be referred to or explained within an error. + * @since 4.1.0 + * @note defaults to [[NotConfigured `NotConfigured`]] + * @group numeric + */ + def labelRealDot: LabelWithExplainConfig = NotConfigured + /** How the trailing exponents of a real number should be referred to or explained within an error. + * @since 4.1.0 + * @note defaults to [[NotConfigured `NotConfigured`]] + * @group numeric + */ + def labelRealExponent: LabelWithExplainConfig = NotConfigured + /** How the fact that the end of an exponent part of a real literal is expected should be referred to within an error. + * @since 4.1.0 + * @note defaults to [[NotConfigured `NotConfigured`]] + * @group numeric + */ + def labelRealExponentEnd: LabelConfig = NotConfigured + + /** Even if leading and trailing zeros can be dropped, `.` is not a valid real number: this + * method specifies how to report that to the user. + * @since 4.1.0 + * @note defaults to a ''vanilla'' explain: "a real number cannot drop both a leading and trailing zero" + * @group numeric + */ + def preventRealDoubleDroppedZero: PreventDotIsZeroConfig = ZeroDotReason("a real number cannot drop both a leading and trailing zero") + + /** This method describes the content of the error when an integer literal is parsed and it is not within the + * required bit-width. + * @param min the smallest value the integer could have taken + * @param max the largest value the integer could have taken + * @param nativeRadix the radix that the integer was parsed using + * @since 4.1.0 + * @note defaults to a ''specialised'' error describing what the min and max bounds are. + * @group numeric + */ + def filterIntegerOutOfBounds(min: BigInt, max: BigInt, nativeRadix: Int): FilterConfig[BigInt] = new SpecialisedMessage[BigInt](fullAmend = false) { + def message(n: BigInt) = Seq(s"literal is not within the range ${min.toString(nativeRadix)} to ${max.toString(nativeRadix)}") + } + + /** This method describes the content of the error when a real literal is parsed and it is not representable exactly as the required precision. + * @param name the name of the required precision (one of `doubleName` or `floatName`) + * @since 4.1.0 + * @note defaults to a ''specialised'' error stating that the literal is not exactly representable. + * @group numeric + */ + def filterRealNotExact(name: String): FilterConfig[BigDecimal] = new SpecialisedMessage[BigDecimal](fullAmend = false) { + def message(n: BigDecimal) = Seq(s"literal cannot be represented exactly as an $name") + } + + /** This method describes the content of the error when a real literal is parsed and it is not within the bounds perscribed by the required precision. + * @param name the name of the required precision (one of `doubleName` or `floatName`) + * @param min the smallest value the real could have taken + * @param max the largest value the real could have taken + * @since 4.1.0 + * @note defaults to a ''specialised'' error describing what the min and max bounds are. + * @group numeric + */ + def filterRealOutOfBounds(name: String, min: BigDecimal, max: BigDecimal): FilterConfig[BigDecimal] = + new SpecialisedMessage[BigDecimal](fullAmend = false) { + def message(n: BigDecimal) = Seq(s"literal is not within the range $min to $max and is not an $name") + } + + // expensive ;) + // this is not as effective as it may seem, because reasons cannot be hints + // it's possible a preventative error could be more effective? + /*def verifiedIntegerBadCharsUsedInLiteral: Option[(predicate.CharPredicate, Int => String)] = + None*/ + + /** The name given to doubles. + * @since 4.1.0 + * @note defaults to "IEEE 754 double-precision float" + * @group numeric + */ + def doubleName: String = "IEEE 754 double-precision float" + /** The name given to floats. + * @since 4.1.0 + * @note defaults to "IEEE 754 single-precision float" + * @group numeric + */ + def floatName: String = "IEEE 754 single-precision float" + + private [token] final def labelDecimal(bits: Int, signed: Boolean): LabelWithExplainConfig = { + if (signed) labelIntegerSignedDecimal(bits) else labelIntegerUnsignedDecimal(bits) + } + private [token] final def labelHexadecimal(bits: Int, signed: Boolean): LabelWithExplainConfig = { + if (signed) labelIntegerSignedHexadecimal(bits) else labelIntegerUnsignedHexadecimal(bits) + } + private [token] final def labelOctal(bits: Int, signed: Boolean): LabelWithExplainConfig = { + if (signed) labelIntegerSignedOctal(bits) else labelIntegerUnsignedOctal(bits) + } + private [token] final def labelBinary(bits: Int, signed: Boolean): LabelWithExplainConfig = { + if (signed) labelIntegerSignedBinary(bits) else labelIntegerUnsignedBinary(bits) + } + private [token] final def labelNumber(bits: Int, signed: Boolean): LabelWithExplainConfig = { + if (signed) labelIntegerSignedNumber(bits) else labelIntegerUnsignedNumber(bits) + } + + /** How an identifier should be referred to in an error message. + * @since 4.1.0 + * @note defaults to "identifier" + * @group names + */ + def labelNameIdentifier: String = "identifier" + /** How a user-defined operator should be referred to in an error message. + * @since 4.1.0 + * @note defaults to "operator" + * @group names + */ + def labelNameOperator: String = "operator" + /** How an illegally parsed hard keyword should be referred to as an unexpected component. + * @param v the illegal identifier + * @since 4.1.0 + * @note defaults to "keyword v" + * @group names + */ + def unexpectedNameIllegalIdentifier(v: String): String = s"keyword $v" + /** How an illegally parsed hard operator should be referred to as an unexpected component. + * @since 4.1.0 + * @note defaults to "reserved operator v" + * @group names + */ + def unexpectedNameIllegalOperator(v: String): String = s"reserved operator $v" + /** When parsing identifiers that are required to have specific start characters, how bad identifiers should be reported. + * @since 4.1.0 + * @note defaults to unexpected "identifier v" + * @group names + */ + def filterNameIllFormedIdentifier: FilterConfig[String] = new Unexpected[String](fullAmend = false) { + def unexpected(v: String) = s"identifer $v" + } + /** When parsing operators that are required to have specific start/end characters, how bad operators should be reported. + * @since 4.1.0 + * @note defaults to unexpected "operator v" + * @group names + */ + def filterNameIllFormedOperator: FilterConfig[String] = new Unexpected[String](fullAmend = false) { + def unexpected(v: String) = s"operator $v" + } + + /** How a ASCII character literal should be referred to or explained in error messages. + * @since 4.1.0 + * @note defaults to [[NotConfigured `NotConfigured`]] + * @group text + */ + def labelCharAscii: LabelWithExplainConfig = NotConfigured + /** How a Latin1 (extended ASCII) character literal should be referred to or explained in error messages. + * @since 4.1.0 + * @note defaults to [[NotConfigured `NotConfigured`]] + * @group text + */ + def labelCharLatin1: LabelWithExplainConfig = NotConfigured + /** How a BMP (Basic Multilingual Plane) character literal should be referred to or explained in error messages. + * @since 4.1.0 + * @note defaults to [[NotConfigured `NotConfigured`]] + * @group text + */ + def labelCharBasicMultilingualPlane: LabelWithExplainConfig = NotConfigured + /** How a UTF-16 character literal should be referred to or explained in error messages. + * @since 4.1.0 + * @note defaults to [[NotConfigured `NotConfigured`]] + * @group text + */ + def labelCharUtf16: LabelWithExplainConfig = NotConfigured + + /** How the closing quote of an ASCII character literal should be referred to in error messages. + * @since 4.1.0 + * @note defaults to [[NotConfigured `NotConfigured`]] + * @group text + */ + def labelCharAsciiEnd: LabelConfig = NotConfigured + /** How the closing quote of a Latin1 character literal should be referred to in error messages. + * @since 4.1.0 + * @note defaults to [[NotConfigured `NotConfigured`]] + * @group text + */ + def labelCharLatin1End: LabelConfig = NotConfigured + /** How the closing quote of a BMP character literal should be referred to in error messages. + * @since 4.1.0 + * @note defaults to [[NotConfigured `NotConfigured`]] + * @group text + */ + def labelCharBasicMultilingualPlaneEnd: LabelConfig = NotConfigured + /** How the closing quote of a UTF-16 character literal should be referred to in error messages. + * @since 4.1.0 + * @note defaults to [[NotConfigured `NotConfigured`]] + * @group text + */ + def labelCharUtf16End: LabelConfig = NotConfigured + + /** How a ASCII-only string literal should be referred to or explained in error messages. + * @since 4.1.0 + * @param multi whether this is for multi-line strings + * @param raw whether this is for raw strings + * @note defaults to [[NotConfigured `NotConfigured`]] + * @group text + */ + def labelStringAscii(multi: Boolean, raw: Boolean): LabelWithExplainConfig = NotConfigured + /** How a Latin1-only string literal should be referred to or explained in error messages. + * @since 4.1.0 + * @param multi whether this is for multi-line strings + * @param raw whether this is for raw strings + * @note defaults to [[NotConfigured `NotConfigured`]] + * @group text + */ + def labelStringLatin1(multi: Boolean, raw: Boolean): LabelWithExplainConfig = NotConfigured + /** How a UTF-16-only string should literal be referred to or explained in error messages. + * @since 4.1.0 + * @param multi whether this is for multi-line strings + * @param raw whether this is for raw strings + * @note defaults to [[NotConfigured `NotConfigured`]] + * @group text + */ + def labelStringUtf16(multi: Boolean, raw: Boolean): LabelWithExplainConfig = NotConfigured + + /** How the closing quote(s) of an ASCII string literal should be referred to in error messages. + * @since 4.1.0 + * @param multi whether this is for multi-line strings + * @param raw whether this is for raw strings + * @note defaults to [[NotConfigured `NotConfigured`]] + * @group text + */ + def labelStringAsciiEnd(multi: Boolean, raw: Boolean): LabelConfig = NotConfigured + /** How the closing quote(s) of a Latin1 string literal should be referred to in error messages. + * @since 4.1.0 + * @param multi whether this is for multi-line strings + * @param raw whether this is for raw strings + * @note defaults to [[NotConfigured `NotConfigured`]] + * @group text + */ + def labelStringLatin1End(multi: Boolean, raw: Boolean): LabelConfig = NotConfigured + /** How the closing quote(s) of a UTF-16 string literal should be referred to in error messages. + * @since 4.1.0 + * @param multi whether this is for multi-line strings + * @param raw whether this is for raw strings + * @note defaults to [[NotConfigured `NotConfigured`]] + * @group text + */ + def labelStringUtf16End(multi: Boolean, raw: Boolean): LabelConfig = NotConfigured + + /** How general string characters should be referred to in error messages. + * @since 4.1.0 + * @note defaults to label of "string character" + * @note this superscedes [[labelGraphicCharacter `labelGraphicCharacter`]] and [[labelEscapeSequence `labelEscapeSequence`]] within string literals. + * @group text + */ + def labelStringCharacter: LabelConfig = Label("string character") + /** How a graphic character (a regular character in the literal) should be referred to or explained in error messages. + * @since 4.1.0 + * @note defaults to a label of "graphic character" + * @note explains for graphic characters do not work in string literals. + * @group text + */ + def labelGraphicCharacter: LabelWithExplainConfig = Label("graphic character") + /** How an escape sequence should be referred to or explained in error messages. + * @since 4.1.0 + * @note defaults to label of "escape sequence" + * @note explains for escape characters do not work in string literals. + * @see [[labelEscapeEnd `labelEscapeEnd`]] for how to explain what valid escape sequences may be when the lead character has been parsed. + * @group text + */ + def labelEscapeSequence: LabelWithExplainConfig = Label("escape sequence") //different to "invalid escape sequence"! + /** How a numeric escape sequence (after the opening character) should be referred to or explained in error messages. + * @since 4.1.0 + * @param radix the radix this specific configuration applies to + * @note defaults to [[NotConfigured `NotConfigured`]] + * @group text + */ + def labelEscapeNumeric(radix: Int): LabelWithExplainConfig = NotConfigured + /** How the end of a numeric escape sequence (after a prefix) should be referred to or explained in error messages. + * @since 4.1.0 + * @param radix the radix this specific configuration applies to + * @param prefix the character that started this sequence + * @note defaults to [[NotConfigured `NotConfigured`]] + * @group text + */ + def labelEscapeNumericEnd(prefix: Char, radix: Int): LabelWithExplainConfig = NotConfigured + /** How the end of an escape sequence (anything past the opening character) should be referred to or explained within an error message. + * @since 4.1.0 + * @note defaults to label of "end of escape sequence" with a reason of "invalid escape sequence" + * @group text + */ + def labelEscapeEnd: LabelWithExplainConfig = LabelAndReason("end of escape sequence", "invalid escape sequence") + /** How zero-width escape characters should be referred to within error messages. + * @since 4.1.0 + * @note defaults to [[NotConfigured `NotConfigured`]] + * @group text + */ + def labelStringEscapeEmpty: LabelConfig = NotConfigured + /** How string gaps should be referred to within error messages. + * @since 4.1.0 + * @note defaults to label of "string gap" + * @group text + */ + def labelStringEscapeGap: LabelConfig = Label("string gap") + /** How the end of a string gap (the closing slash) should be referred to within error messages. + * @since 4.1.0 + * @note defaults to label of "end of string gap" + * @group text + */ + def labelStringEscapeGapEnd: LabelConfig = Label("end of string gap") + + /** When a non-BMP character is found in a BMP-only character literal, specifies how this should be reported. + * @since 4.1.0 + * @note defaults to a filter generating the reason "non-BMP character" + * @group text + */ + def filterCharNonBasicMultilingualPlane: VanillaFilterConfig[Int] = new Because[Int](fullAmend = false) { + def reason(@unused x: Int) = "non-BMP character" + } + /** When a non-ASCII character is found in a ASCII-only character literal, specifies how this should be reported. + * @since 4.1.0 + * @note defaults to a filter generating the reason "non-ascii character" + * @group text + */ + def filterCharNonAscii: VanillaFilterConfig[Int] = new Because[Int](fullAmend = false) { + def reason(@unused x: Int) = "non-ascii character" + } + /** When a non-Latin1 character is found in a Latin1-only character literal, specifies how this should be reported. + * @since 4.1.0 + * @note defaults to a filter generating the reason "non-latin1 character" + * @group text + */ + def filterCharNonLatin1: VanillaFilterConfig[Int] = new Because[Int](fullAmend = false) { + def reason(@unused x: Int) = "non-latin1 character" + } + + /** When a non-ASCII character is found in a ASCII-only string literal, specifies how this should be reported. + * @since 4.1.0 + * @note defaults to a filter generating a ''specialised'' message of "non-ascii characters in string literal, this is not allowed" + * @group text + */ + def filterStringNonAscii: SpecialisedFilterConfig[StringBuilder] = new SpecialisedMessage[StringBuilder](fullAmend = false) { + def message(@unused s: StringBuilder) = Seq("non-ascii characters in string literal, this is not allowed") + } + + /** When a non-Latin1 character is found in a Latin1-only string literal, specifies how this should be reported. + * @since 4.1.0 + * @note defaults to a filter generating a ''specialised'' message of "non-latin1 characters in string literal, this is not allowed" + * @group text + */ + def filterStringNonLatin1: SpecialisedFilterConfig[StringBuilder] = new SpecialisedMessage[StringBuilder](fullAmend = false) { + def message(@unused s: StringBuilder) = Seq("non-latin1 characters in string literal, this is not allowed") + } + + /** When a numeric escape sequence requires a specific number of digits but this was not successfully parsed, this describes how to + * report that error given the number of successfully parsed digits up this point. + * @since 4.1.0 + * @param radix the radix used for this numeric escape sequence + * @param needed the possible numbers of digits required + * @note defaults to a ''specialised'' message describing how many digits are required but how many were present. + * @group text + */ + def filterEscapeCharRequiresExactDigits(@unused radix: Int, needed: Seq[Int]): SpecialisedFilterConfig[Int] = + new SpecialisedMessage[Int](fullAmend = false) { + def message(got: Int) = Seq( + s"numeric escape requires ${parsley.errors.helpers.combineAsList(needed.toList.map(_.toString))} digits, but only got $got" + ) + } + + /** When a numeric escape sequence is not legal, this describes how to report that error, given the original illegal character. + * @since 4.1.0 + * @param maxEscape the largest legal escape character + * @param radix the radix used for this numeric escape sequence + * @note defaults to a ''specialised'' message stating if the character is larger than the given maximum, or just an illegal codepoint otherwise. + * @group text + */ + def filterEscapeCharNumericSequenceIllegal(maxEscape: Int, radix: Int): SpecialisedFilterConfig[BigInt] = + new SpecialisedMessage[BigInt](fullAmend = false) { + def message(escapeChar: BigInt) = Seq( + if (escapeChar > BigInt(maxEscape)) { + s"${escapeChar.toString(radix)} is greater than the maximum character value of ${BigInt(maxEscape).toString(radix)}" + } + else s"illegal unicode codepoint: ${escapeChar.toString(radix)}" + ) + } + + /** Character literals parse either graphic characters or escape characters. This configuration allows for individual errors when a character ''not'' part + * of either graphic characters or escape characters is encountered. + * @since 4.1.0 + * @note defaults to [[Unverified `Unverified`]] + * @group text + */ + def verifiedCharBadCharsUsedInLiteral: VerifiedBadChars = Unverified + /** String literals parse either graphic characters or escape characters. This configuration allows for individual errors when a character ''not'' part + * of either graphic characters or escape characters is encountered. + * @since 4.1.0 + * @note defaults to [[Unverified `Unverified`]] + * @group text + */ + def verifiedStringBadCharsUsedInLiteral: VerifiedBadChars = Unverified + + /** How to refer to a `;` symbol in an error message. + * @since 4.1.0 + * @note defaults to "semicolon" + * @group symbol + */ + def labelSymbolSemi: LabelConfig = Label("semicolon") + /** How to refer to a `,` symbol in an error message. + * @since 4.1.0 + * @note defaults to "comma" + * @group symbol + */ + def labelSymbolComma: LabelConfig = Label("comma") + /** How to refer to a `:` symbol in an error message. + * @since 4.1.0 + * @note defaults to "colon" + * @group symbol + */ + def labelSymbolColon: LabelConfig = Label("colon") + /** How to refer to a `.` symbol in an error message. + * @since 4.1.0 + * @note defaults to "dot" + * @group symbol + */ + def labelSymbolDot: LabelConfig = Label("dot") + /** How to refer to a `(` symbol in an error message. + * @since 4.1.0 + * @note defaults to "open parenthesis" + * @group symbol + */ + def labelSymbolOpenParen: LabelConfig = Label("open parenthesis") + /** How to refer to a `{` symbol in an error message. + * @since 4.1.0 + * @note defaults to "open brace" + * @group symbol + */ + def labelSymbolOpenBrace: LabelConfig = Label("open brace") + /** How to refer to a `[` symbol in an error message. + * @since 4.1.0 + * @note defaults to "open square bracket" + * @group symbol + */ + def labelSymbolOpenSquare: LabelConfig = Label("open square bracket") + /** How to refer to a `<` symbol in an error message. + * @since 4.1.0 + * @note defaults to "open angle bracket" + * @group symbol + */ + def labelSymbolOpenAngle: LabelConfig = Label("open angle bracket") + /** How to refer to a `)` symbol in an error message. + * @since 4.1.0 + * @note defaults to "closing parenthesis" + * @group symbol + */ + def labelSymbolClosingParen: LabelConfig = Label("closing parenthesis") + /** How to refer to a `}` symbol in an error message. + * @since 4.1.0 + * @note defaults to "closing brace" + * @group symbol + */ + def labelSymbolClosingBrace: LabelConfig = Label("closing brace") + /** How to refer to a `]` symbol in an error message. + * @since 4.1.0 + * @note defaults to "closing square bracket" + * @group symbol + */ + def labelSymbolClosingSquare: LabelConfig = Label("closing square bracket") + /** How to refer to a `>` symbol in an error message. + * @since 4.1.0 + * @note defaults to "closing angle bracket" + * @group symbol + */ + def labelSymbolClosingAngle: LabelConfig = Label("closing angle bracket") + //TODO: In future, we want to add LabelWithExplain config here: to do that, introduce an explainSymbolKeyword and merge with a private verson + /** How a given keyword should be described in an error message. + * @since 4.1.0 + * @note defaults to labelling with the symbol itself + * @group symbol + */ + def labelSymbolKeyword(symbol: String): LabelConfig = Label(symbol) + /** How a given operator should be described in an error message. + * @since 4.1.0 + * @note defaults to labelling with the symbol itself + * @group symbol + */ + def labelSymbolOperator(symbol: String): LabelConfig = Label(symbol) + /** How the required end of a given keyword should be specified in an error. + * @since 4.1.0 + * @note defaults to "end of symbol" + * @group symbol + */ + def labelSymbolEndOfKeyword(symbol: String): String = s"end of $symbol" + /** How the required end of a given operator should be specified in an error. + * @since 4.1.0 + * @note defaults to "end of symbol" + * @group symbol + */ + def labelSymbolEndOfOperator(symbol: String): String = s"end of $symbol" + + /** How the end of a single-line comment should be described or explained. + * @since 4.1.0 + * @note defaults to "end of comment" + * @group space + */ + def labelSpaceEndOfLineComment: LabelWithExplainConfig = Label("end of comment") + /** How the end of a multi-line comment should be described or explained. + * @since 4.1.0 + * @note defaults to "end of comment" + * @group space + */ + def labelSpaceEndOfMultiComment: LabelWithExplainConfig = Label("end of comment") } diff --git a/parsley/shared/src/main/scala/parsley/token/errors/VerifiedAndPreventativeErrors.scala b/parsley/shared/src/main/scala/parsley/token/errors/VerifiedAndPreventativeErrors.scala new file mode 100644 index 000000000..ba2bb1083 --- /dev/null +++ b/parsley/shared/src/main/scala/parsley/token/errors/VerifiedAndPreventativeErrors.scala @@ -0,0 +1,103 @@ +/* SPDX-FileCopyrightText: © 2023 Parsley Contributors + * SPDX-License-Identifier: BSD-3-Clause + */ +package parsley.token.errors + +import parsley.Parsley, Parsley.{pure, empty} +import parsley.character.satisfyUtf16 +import parsley.errors.combinator, combinator.ErrorMethods +import parsley.position + +/** This class is used to configure what error is generated when `.` is parsed as a real number. + * @since 4.1.0 + * @group doubledot + */ +sealed abstract class PreventDotIsZeroConfig { + private [token] def apply(p: Parsley[Boolean]): Parsley[Boolean] +} +private final class UnexpectedZeroDot private (unexpected: String) extends PreventDotIsZeroConfig { + private [token] override def apply(p: Parsley[Boolean]): Parsley[Boolean] = p.unexpectedWhen { case true => unexpected } +} +/** This object makes "dot is zero" generate a given unexpected message in a ''vanilla'' error. + * @since 4.1.0 + * @group doubledot + */ +object UnexpectedZeroDot { + def apply(unexpected: String): PreventDotIsZeroConfig = new UnexpectedZeroDot(unexpected) +} + +// TODO: factor this combinator out with the "Great Move" in 4.2 +private final class UnexpectedZeroDotWithReason private (unexpected: String, reason: String) extends PreventDotIsZeroConfig { + private [token] override def apply(p: Parsley[Boolean]): Parsley[Boolean] = combinator.partialAmendThenDislodge { + position.internalOffsetSpan(combinator.entrench(p)).flatMap { case (os, x, oe) => + if (x) combinator.unexpected(oe - os, unexpected).explain(reason) + else pure(x) + } + } +} +/** This object makes "dot is zero" generate a given unexpected message with a given reason in a ''vanilla'' error. + * @since 4.1.0 + * @group doubledot + */ +object UnexpectedZeroDotWithReason { + def apply(unexpected: String, reason: String): PreventDotIsZeroConfig = new UnexpectedZeroDotWithReason(unexpected, reason) +} + +private final class ZeroDotReason private (reason: String) extends PreventDotIsZeroConfig { + private [token] override def apply(p: Parsley[Boolean]): Parsley[Boolean] = p.filterOut { case true => reason } +} +/** This object makes "dot is zero" generate a given reason in a ''vanilla'' error. + * @since 4.1.0 + * @group doubledot + */ +object ZeroDotReason { + def apply(reason: String): PreventDotIsZeroConfig = new ZeroDotReason(reason) +} + +private final class ZeroDotFail private (msg0: String, msgs: String*) extends PreventDotIsZeroConfig { + private [token] override def apply(p: Parsley[Boolean]): Parsley[Boolean] = p.guardAgainst { case true => msg0 +: msgs } +} +/** This object makes "dot is zero" generate a bunch of given messages in a ''specialised'' error. + * @since 4.1.0 + * @group doubledot + */ +object ZeroDotFail { + def apply(msg0: String, msgs: String*): PreventDotIsZeroConfig = new ZeroDotFail(msg0, msgs: _*) +} + +/** This class is used to configure what error should be generated when illegal characters in a string or character literal are parsable. + * @since 4.1.0 + * @group badchar + */ +sealed abstract class VerifiedBadChars { + private [token] def checkBadChar: Parsley[Nothing] +} +private final class BadCharsFail private (cs: Map[Int, Seq[String]]) extends VerifiedBadChars { + private [token] def checkBadChar: Parsley[Nothing] = satisfyUtf16(cs.contains).fail(cs.apply(_)) +} +/** This object makes "bad literal chars" generate a bunch of given messages in a ''specialised'' error. Requires a map from bad characters to their messages. + * @since 4.1.0 + * @group badchar + */ +object BadCharsFail { + private [token] def apply(cs: Map[Int, Seq[String]]): VerifiedBadChars = if (cs.isEmpty) Unverified else new BadCharsFail(cs) +} + +private final class BadCharsReason private (cs: Map[Int, String]) extends VerifiedBadChars { + private [token] def checkBadChar: Parsley[Nothing] = satisfyUtf16(cs.contains)._unexpected(cs.apply) +} +/** This object makes "bad literal chars" generate a reason in a ''vanilla'' error. Requires a map from bad characters to their reasons. + * @since 4.1.0 + * @group badchar + */ +object BadCharsReason { + def apply(cs: Map[Int, String]): VerifiedBadChars = if (cs.isEmpty) Unverified else new BadCharsReason(cs) +} + +/** This object disables the verified error for bad characters: this may improve parsing performance slightly on the failure case. + * @since 4.1.0 + * @group badchar + */ +object Unverified extends VerifiedBadChars { + private [token] def checkBadChar: Parsley[Nothing] = empty +} diff --git a/parsley/shared/src/main/scala/parsley/token/errors/package.scala b/parsley/shared/src/main/scala/parsley/token/errors/package.scala new file mode 100644 index 000000000..3f5987c5f --- /dev/null +++ b/parsley/shared/src/main/scala/parsley/token/errors/package.scala @@ -0,0 +1,32 @@ +/* SPDX-FileCopyrightText: © 2023 Parsley Contributors + * SPDX-License-Identifier: BSD-3-Clause + */ +package parsley.token + +/** This package contains the relevant functionality for configuring the error messages generated by the parsers provided by the `Lexer` class. + * + * @groupprio errconfig 5 + * @groupname errconfig Error Configuration + * @groupdesc errconfig This is the main class that defines the configuration for errors from the `Lexer`. + * + * @groupprio labels 5 + * @groupname labels Labelling and Explain Configuration + * @groupdesc labels These classes can be used to configure both labels and/or explains for simple description configurations. + * + * @groupprio filters 10 + * @groupname filters Filtering Configuration + * @groupdesc filters These classes can be used to describe how to generate the filters to rule out specific parses. They can used to either generate + * types of ''vanilla'' error or ''specialised'' errors. + * + * @groupprio doubledot 20 + * @groupname doubledot Preventing Double Dot + * @groupdesc doubledot These classes and objects help to configure the ''Preventative Error'' pattern for bad `.`s, + * used by [[parsley.token.errors.ErrorConfig.preventRealDoubleDroppedZero `peventRealDoubleDroppedZero`]]. + * + * @groupprio badchar 20 + * @groupname badchar Verifying Bad Characters + * @groupdesc badchar These classes can be used to help configure the ''Verified Error'' pattern for illegal string and character literal characters, + * used by [[parsley.token.errors.ErrorConfig.verifiedCharBadCharsUsedInLiteral `verifiedCharBadCharsUsedInLiteral`]] and + * [[parsley.token.errors.ErrorConfig.verifiedStringBadCharsUsedInLiteral `verifiedStringBadCharsUsedInLiteral`]]. + */ +package object errors diff --git a/parsley/shared/src/main/scala/parsley/token/names/ConcreteNames.scala b/parsley/shared/src/main/scala/parsley/token/names/ConcreteNames.scala index 895437dfd..3b4f012b6 100644 --- a/parsley/shared/src/main/scala/parsley/token/names/ConcreteNames.scala +++ b/parsley/shared/src/main/scala/parsley/token/names/ConcreteNames.scala @@ -8,21 +8,21 @@ import parsley.character.{satisfy, satisfyUtf16, stringOfMany, stringOfManyUtf16 import parsley.errors.combinator.ErrorMethods import parsley.implicits.zipped.Zipped2 import parsley.token.descriptions.{NameDesc, SymbolDesc} +import parsley.token.errors.ErrorConfig import parsley.token.predicate.{Basic, CharPredicate, NotRequired, Unicode} import parsley.internal.deepembedding.singletons -private [token] class ConcreteNames(nameDesc: NameDesc, symbolDesc: SymbolDesc) extends Names { - private def keyOrOp(startImpl: CharPredicate, letterImpl: CharPredicate, parser: =>Parsley[String], illegal: String => Boolean, - combinatorName: String, name: String, illegalName: String) = { +private [token] class ConcreteNames(nameDesc: NameDesc, symbolDesc: SymbolDesc, err: ErrorConfig) extends Names { + private def keyOrOp(startImpl: CharPredicate, letterImpl: CharPredicate, illegal: String => Boolean, + name: String, unexpectedIllegal: String => String) = { (startImpl, letterImpl) match { - case (Basic(start), Basic(letter)) => new Parsley(new singletons.NonSpecific(combinatorName, name, illegalName, start, letter, illegal)) - case _ => - attempt { - parser.unexpectedWhen { - case x if illegal(x) => s"$illegalName $x" - } - }.label(name) + case (Basic(start), Basic(letter)) => new Parsley(new singletons.NonSpecific(name, unexpectedIllegal, start, letter, illegal)) + case _ => attempt { + complete(startImpl, letterImpl).unexpectedWhen { + case x if illegal(x) => unexpectedIllegal(x) + } + }.label(name) } } private def trailer(impl: CharPredicate) = impl match { @@ -38,22 +38,17 @@ private [token] class ConcreteNames(nameDesc: NameDesc, symbolDesc: SymbolDesc) } case NotRequired => empty } - private lazy val ident = complete(nameDesc.identifierStart, nameDesc.identifierLetter) - private lazy val oper = complete(nameDesc.operatorStart, nameDesc.operatorLetter) override lazy val identifier: Parsley[String] = - keyOrOp(nameDesc.identifierStart, nameDesc.identifierLetter, ident, symbolDesc.isReservedName(_), "identifier", "identifier", "keyword") + keyOrOp(nameDesc.identifierStart, nameDesc.identifierLetter, symbolDesc.isReservedName, + err.labelNameIdentifier, err.unexpectedNameIllegalIdentifier) override def identifier(startChar: CharPredicate): Parsley[String] = attempt { - identifier.unexpectedWhen { - case x if !startChar.startsWith(x) => s"identifier $x" - } + err.filterNameIllFormedIdentifier.filter(identifier)(startChar.startsWith) } override lazy val userDefinedOperator: Parsley[String] = - keyOrOp(nameDesc.operatorStart, nameDesc.operatorLetter, oper, symbolDesc.isReservedOp(_), "userOp", "operator", "reserved operator") + keyOrOp(nameDesc.operatorStart, nameDesc.operatorLetter, symbolDesc.isReservedOp, err.labelNameOperator, err.unexpectedNameIllegalOperator) def userDefinedOperator(startChar: CharPredicate, endChar: CharPredicate): Parsley[String] = attempt { - userDefinedOperator.unexpectedWhen { - case x if !startChar.startsWith(x) || !endChar.endsWith(x) => s"operator $x" - } + err.filterNameIllFormedOperator.filter(userDefinedOperator)(x => startChar.startsWith(x) && endChar.endsWith(x)) } } diff --git a/parsley/shared/src/main/scala/parsley/token/names/Names.scala b/parsley/shared/src/main/scala/parsley/token/names/Names.scala index 3c72dd57c..18c1fb9f8 100644 --- a/parsley/shared/src/main/scala/parsley/token/names/Names.scala +++ b/parsley/shared/src/main/scala/parsley/token/names/Names.scala @@ -4,7 +4,7 @@ package parsley.token.names import parsley.Parsley -import parsley.token.predicate.CharPredicate +import parsley.token.predicate.{CharPredicate, NotRequired} /** This class defines a uniform interface for defining parsers for user-defined * names (identifiers and operators), independent of how whitespace should be @@ -19,7 +19,7 @@ import parsley.token.predicate.CharPredicate * `Lexer`, which will depend on user-defined configuration. Please see the * relevant documentation of these specific objects. */ -abstract class Names private[token] { +abstract class Names private[names] { /** This parser will parse an identifier based on the * defined identifier start and identifier letter. It * is capable of handling unicode characters if the @@ -95,17 +95,17 @@ abstract class Names private[token] { * // operatorStart = Basic(Set('+', '-')) * // operatorLetter = Basic(Set('+', '-', ':')) * // hardKeywords = Set("+", "+:", ...) - * scala> userDefinedOperator(NotRequired, Basic(Set(':'))).parse("-:") + * scala> userDefinedOperator.parse("-:") * val res0 = Success("-:") - * scala> userDefinedOperator(NotRequired, Basic(Set(':'))).parse("*:") + * scala> userDefinedOperator.parse("*:") * val res1 = Failure(...) - * scala> userDefinedOperator(NotRequired, Basic(Set(':'))).parse("") + * scala> userDefinedOperator.parse("") * val res2 = Failure(...) - * scala> userDefinedOperator(NotRequired, Basic(Set(':'))).parse("++") - * val res3 = Failure(...) - * scala> userDefinedOperator(NotRequired, Basic(Set(':'))).parse("+:") + * scala> userDefinedOperator.parse("++") + * val res3 = Success("++") + * scala> userDefinedOperator.parse("+:") * val res4 = Failure(...) - * scala> userDefinedOperator(NotRequired, Basic(Set(':'))).parse("++:") + * scala> userDefinedOperator.parse("++:") * val res5 = Success("++:") * }}} * @@ -133,16 +133,18 @@ abstract class Names private[token] { * // operatorStart = Basic(Set('+', '-')) * // operatorLetter = Basic(Set('+', '-', ':')) * // hardKeywords = Set("+", "+:", ...) - * scala> identifier.parse("-") - * val res0 = Success("-") - * scala> identifier.parse("*") + * scala> userDefinedOperator(NotRequired, Basic(Set(':'))).parse("-:") + * val res0 = Success("-:") + * scala> userDefinedOperator(NotRequired, Basic(Set(':'))).parse("*:") * val res1 = Failure(...) - * scala> identifier.parse("") + * scala> userDefinedOperator(NotRequired, Basic(Set(':'))).parse("") * val res2 = Failure(...) - * scala> identifier.parse("++") - * val res3 = Success("++") - * scala> identifier.parse("+:") + * scala> userDefinedOperator(NotRequired, Basic(Set(':'))).parse("++") + * val res3 = Failure(...) + * scala> userDefinedOperator(NotRequired, Basic(Set(':'))).parse("+:") * val res4 = Failure(...) + * scala> userDefinedOperator(NotRequired, Basic(Set(':'))).parse("++:") + * val res5 = Success("++:") * }}} * * @param startChar describes what the starting character must be @@ -151,4 +153,44 @@ abstract class Names private[token] { * @since 4.0.0 */ def userDefinedOperator(startChar: CharPredicate, endChar: CharPredicate): Parsley[String] + /** This combinator will parse a user-defined operator based on the + * defined operator start and operator letter, refined by the + * provided `startChar`. It is capable of handling unicode characters if the + * configuration permits. + * + * After parsing a valid operator as in `userDefinedOperator`, + * this combinator will verify that the first character + * matches the given parameter. If `NotRequired` is passed it + * will be equivalent to `userDefinedOperator`. + * + * If hard operators are specified + * by the configuration, this parser is not permitted + * to parse them. + * + * @example {{{ + * // operatorStart = Basic(Set('+', '-')) + * // operatorLetter = Basic(Set('+', '-', ':')) + * // hardKeywords = Set("+", "+:", ...) + * scala> userDefinedOperator(Basic(Set('+'))).parse("-:") + * val res0 = Failure(...) + * scala> userDefinedOperator(Basic(Set('+'))).parse("*:") + * val res1 = Failure(...) + * scala> userDefinedOperator(Basic(Set('+'))).parse("") + * val res2 = Failure(...) + * scala> userDefinedOperator(Basic(Set('+'))).parse("++") + * val res3 = Success("++") + * scala> userDefinedOperator(Basic(Set('+'))).parse("+:") + * val res4 = Failure(...) + * scala> userDefinedOperator(Basic(Set('+'))).parse("++:") + * val res5 = Success("++:") + * }}} + * + * @param startChar describes what the starting character must be + * @note $disclaimer + * @since 4.1.0 + */ + final def userDefinedOperator(startChar: CharPredicate): Parsley[String] = userDefinedOperator(startChar, NotRequired) + + // TODO: Two variants of the above that also have reasons that describe + // the requirements of the identifier/operator } diff --git a/parsley/shared/src/main/scala/parsley/token/numeric/BitBounds.scala b/parsley/shared/src/main/scala/parsley/token/numeric/BitBounds.scala index 3e23f2a48..51a2b847d 100644 --- a/parsley/shared/src/main/scala/parsley/token/numeric/BitBounds.scala +++ b/parsley/shared/src/main/scala/parsley/token/numeric/BitBounds.scala @@ -10,30 +10,35 @@ private [numeric] sealed abstract class Bits { private [numeric] def upperSigned: BigInt private [numeric] def lowerSigned: BigInt private [numeric] def upperUnsigned: BigInt + private [numeric] def bits: Int } private [numeric] object _8 extends Bits { private [numeric] type self = _8.type - private [numeric] val upperSigned = Byte.MaxValue - private [numeric] val lowerSigned = Byte.MinValue - private [numeric] val upperUnsigned = 0xff + private [numeric] final val upperSigned = Byte.MaxValue + private [numeric] final val lowerSigned = Byte.MinValue + private [numeric] final val upperUnsigned = 0xff + private [numeric] final val bits = 8 } private [numeric] object _16 extends Bits { private [numeric] type self = _16.type - private [numeric] val upperSigned = Short.MaxValue - private [numeric] val lowerSigned = Short.MinValue - private [numeric] val upperUnsigned = 0xffff + private [numeric] final val upperSigned = Short.MaxValue + private [numeric] final val lowerSigned = Short.MinValue + private [numeric] final val upperUnsigned = 0xffff + private [numeric] final val bits = 16 } private [numeric] object _32 extends Bits { private [numeric] type self = _32.type - private [numeric] val upperSigned = Int.MaxValue - private [numeric] val lowerSigned = Int.MinValue - private [numeric] val upperUnsigned = 0xffffffffL + private [numeric] final val upperSigned = Int.MaxValue + private [numeric] final val lowerSigned = Int.MinValue + private [numeric] final val upperUnsigned = 0xffffffffL + private [numeric] final val bits = 32 } private [numeric] object _64 extends Bits { private [numeric] type self = _64.type - private [numeric] val upperSigned = Long.MaxValue - private [numeric] val lowerSigned = Long.MinValue - private [numeric] val upperUnsigned = BigInt("ffffffffffffffff", 16) + private [numeric] final val upperSigned = Long.MaxValue + private [numeric] final val lowerSigned = Long.MinValue + private [numeric] final val upperUnsigned = BigInt("ffffffffffffffff", 16) + private [numeric] final val bits = 64 } private [numeric] sealed abstract class CanHold[N <: Bits, T] { @@ -49,7 +54,7 @@ private [numeric] sealed abstract class CanHold[N <: Bits, T] { * * @since 4.0.0 */ -abstract class LowPriorityImplicits private[numeric] { +sealed class LowPriorityImplicits private[numeric] { import CanHold.can_hold_64_bits // scalastyle:ignore import.grouping // this being here means that Scala will look for it last, which allows default to Long for 64-bit /** Evidence that `BigInt` can store (at least) 64 bits of data. diff --git a/parsley/shared/src/main/scala/parsley/token/numeric/Combined.scala b/parsley/shared/src/main/scala/parsley/token/numeric/Combined.scala index afa0fdfe1..a86d1dd58 100644 --- a/parsley/shared/src/main/scala/parsley/token/numeric/Combined.scala +++ b/parsley/shared/src/main/scala/parsley/token/numeric/Combined.scala @@ -4,7 +4,7 @@ package parsley.token.numeric import parsley.Parsley -import parsley.errors.combinator.ErrorMethods +import parsley.token.errors.ErrorConfig /** This class defines a uniform interface for defining parsers for mixed kind * numeric literals, independent of how whitespace should be handled after the literal @@ -31,7 +31,7 @@ import parsley.errors.combinator.ErrorMethods * @define multibase * Depending on the configuration this may be able to handle different bases for each type of number. */ -abstract class Combined private[token] { // scalastyle:ignore number.of.methods +abstract class Combined private[numeric] (err: ErrorConfig) { // scalastyle:ignore number.of.methods /** $base1 decimal number, $base2. * * @since 4.0.0 @@ -518,24 +518,15 @@ abstract class Combined private[token] { // scalastyle:ignore number.of.methods private def octalBounded[T](bits: Bits)(implicit ev: CanHold[bits.self, T]): Parsley[Either[T, BigDecimal]] = bounded(_octal, bits, 8) private def binaryBounded[T](bits: Bits)(implicit ev: CanHold[bits.self, T]): Parsley[Either[T, BigDecimal]] = bounded(_binary, bits, 2) - // TODO: basically shared with Real... - private def bad(min: Double, max: Double, precision: String)(rn: Either[_, BigDecimal]): Seq[String] = { - val Right(n) = rn - if (n > BigDecimal(max) || n < BigDecimal(min)) { - Seq(s"literal $n is too large to be an IEEE 754 $precision-precision float") - } - else Seq(s"literal $n is too small to be an IEEE 754 $precision-precision float") - } - protected [numeric] def ensureFloat[T](number: Parsley[Either[T, BigDecimal]]): Parsley[Either[T, Float]] = { - number.collectMsg(bad(Float.MinValue.toDouble, Float.MaxValue.toDouble, "single")(_)) { + err.filterRealOutOfBounds(err.floatName, BigDecimal(Float.MinValue.toDouble), BigDecimal(Float.MaxValue.toDouble)).injectRight.collect(number) { case Left(n) => Left(n) case Right(n) if Real.isFloat(n) => Right(n.toFloat) } } protected [numeric] def ensureDouble[T](number: Parsley[Either[T, BigDecimal]]): Parsley[Either[T, Double]] = { - number.collectMsg(bad(Double.MinValue, Double.MaxValue, "double")(_)) { + err.filterRealOutOfBounds(err.doubleName, BigDecimal(Double.MinValue), BigDecimal(Double.MaxValue)).injectRight.collect(number) { case Left(n) => Left(n) case Right(n) if Real.isDouble(n) => Right(n.toDouble) } diff --git a/parsley/shared/src/main/scala/parsley/token/numeric/Generic.scala b/parsley/shared/src/main/scala/parsley/token/numeric/Generic.scala index dbadf24e3..0abf37d11 100644 --- a/parsley/shared/src/main/scala/parsley/token/numeric/Generic.scala +++ b/parsley/shared/src/main/scala/parsley/token/numeric/Generic.scala @@ -4,65 +4,88 @@ package parsley.token.numeric import parsley.Parsley, Parsley.pure -import parsley.character.{bit, digit, hexDigit, octDigit} +import parsley.character, character.{isHexDigit, isOctDigit, satisfy} import parsley.combinator.optional -import parsley.extension.OperatorSugar +import parsley.errors.combinator.ErrorMethods import parsley.implicits.character.charLift import parsley.token.descriptions.numeric.{BreakCharDesc, NumericDesc} +import parsley.token.errors.{ErrorConfig, LabelConfig} -private [token] object Generic { - private def ofRadix(radix: Int, digit: Parsley[Char]): Parsley[BigInt] = ofRadix(radix, digit, digit) - private def ofRadix(radix: Int, startDigit: Parsley[Char], digit: Parsley[Char]): Parsley[BigInt] = { +private [token] class Generic(err: ErrorConfig) { + private def ofRadix(radix: Int, digit: Parsley[Char], label: LabelConfig): Parsley[BigInt] = ofRadix(radix, digit, digit, label) + private def ofRadix(radix: Int, startDigit: Parsley[Char], digit: Parsley[Char], label: LabelConfig): Parsley[BigInt] = { val pf = pure((x: BigInt, d: Char) => x*radix + d.asDigit) - parsley.expr.infix.secretLeft1(startDigit.map(d => BigInt(d.asDigit)), digit, pf) + val trailingDigit = label(digit) + parsley.expr.infix.secretLeft1(startDigit.map(d => BigInt(d.asDigit)), trailingDigit, pf) } - private def ofRadix(radix: Int, digit: Parsley[Char], breakChar: Char): Parsley[BigInt] = ofRadix(radix, digit, digit, breakChar) - private def ofRadix(radix: Int, startDigit: Parsley[Char], digit: Parsley[Char], breakChar: Char): Parsley[BigInt] = { + private def ofRadix(radix: Int, digit: Parsley[Char], breakChar: Char, label: LabelConfig): Parsley[BigInt] = { + ofRadix(radix, digit, digit, breakChar, label) + } + private def ofRadix(radix: Int, startDigit: Parsley[Char], digit: Parsley[Char], breakChar: Char, label: LabelConfig): Parsley[BigInt] = { val pf = pure((x: BigInt, d: Char) => x*radix + d.asDigit) - parsley.expr.infix.secretLeft1(startDigit.map(d => BigInt(d.asDigit)), optional(breakChar) *> digit, pf) + val trailingDigit = + optional(err.labelNumericBreakChar.orElse(label)(breakChar)) *> + label(digit) + parsley.expr.infix.secretLeft1(startDigit.map(d => BigInt(d.asDigit)), trailingDigit, pf) } - // TODO: these could improve by not using `-` - lazy val zeroAllowedDecimal = ofRadix(10, digit) - lazy val zeroAllowedHexadecimal = ofRadix(16, hexDigit) - lazy val zeroAllowedOctal = ofRadix(8, octDigit) - lazy val zeroAllowedBinary = ofRadix(2, bit) + // Digits + private def nonZeroDigit = satisfy(c => c.isDigit && c != '0').label("digit") + private def nonZeroHexDigit = satisfy(c => isHexDigit(c) && c != '0').label("hexadecimal digit") + private def nonZeroOctDigit = satisfy(c => isOctDigit(c) && c != '0').label("octal digit") + private def nonZeroBit = '1'.label("bit") + // why secret? so that the above digits can be marked as digits without "non-zero or zero digit" nonsense + private def secretZero = '0'.hide #> BigInt(0) + + private def digit = character.digit + private def hexDigit = character.hexDigit + private def octDigit = character.octDigit + private def bit = character.bit - lazy val zeroNotAllowedDecimal = ofRadix(10, digit - '0', digit) <|> ('0' #> BigInt(0)) - lazy val zeroNotAllowedHexadecimal = ofRadix(16, hexDigit - '0', hexDigit) <|> ('0' #> BigInt(0)) - lazy val zeroNotAllowedOctal = ofRadix(8, octDigit - '0', octDigit) <|> ('0' #> BigInt(0)) - lazy val zeroNotAllowedBinary = ofRadix(2, '1', bit) <|> ('0' #> BigInt(0)) + def zeroAllowedDecimal(endLabel: LabelConfig): Parsley[BigInt] = ofRadix(10, digit, endLabel) + def zeroAllowedHexadecimal(endLabel: LabelConfig): Parsley[BigInt] = ofRadix(16, hexDigit, endLabel) + def zeroAllowedOctal(endLabel: LabelConfig): Parsley[BigInt] = ofRadix(8, octDigit, endLabel) + def zeroAllowedBinary(endLabel: LabelConfig): Parsley[BigInt] = ofRadix(2, bit, endLabel) - def plainDecimal(desc: NumericDesc): Parsley[BigInt] = plainDecimal(desc.leadingZerosAllowed, desc.literalBreakChar) - def plainDecimal(leadingZerosAllowed: Boolean, literalBreakChar: BreakCharDesc): Parsley[BigInt] = literalBreakChar match { - case BreakCharDesc.NoBreakChar if leadingZerosAllowed => zeroAllowedDecimal - case BreakCharDesc.NoBreakChar => zeroNotAllowedDecimal - case BreakCharDesc.Supported(c, _) if leadingZerosAllowed => ofRadix(10, digit, c) - case BreakCharDesc.Supported(c, _) => ofRadix(10, digit - '0', digit, c) <|> ('0' #> BigInt(0)) + def zeroNotAllowedDecimal(endLabel: LabelConfig): Parsley[BigInt] = ofRadix(10, nonZeroDigit, digit, endLabel) <|> secretZero + def zeroNotAllowedHexadecimal(endLabel: LabelConfig): Parsley[BigInt] = ofRadix(16, nonZeroHexDigit, hexDigit, endLabel) <|> secretZero + def zeroNotAllowedOctal(endLabel: LabelConfig): Parsley[BigInt] = ofRadix(8, nonZeroOctDigit, octDigit, endLabel) <|> secretZero + def zeroNotAllowedBinary(endLabel: LabelConfig): Parsley[BigInt] = ofRadix(2, nonZeroBit, bit, endLabel) <|> secretZero + + def plainDecimal(desc: NumericDesc, endLabel: LabelConfig): Parsley[BigInt] = plainDecimal(desc.leadingZerosAllowed, desc.literalBreakChar, endLabel) + private def plainDecimal(leadingZerosAllowed: Boolean, literalBreakChar: BreakCharDesc, endLabel: LabelConfig): Parsley[BigInt] = literalBreakChar match { + case BreakCharDesc.NoBreakChar if leadingZerosAllowed => zeroAllowedDecimal(endLabel) + case BreakCharDesc.NoBreakChar => zeroNotAllowedDecimal(endLabel) + case BreakCharDesc.Supported(c, _) if leadingZerosAllowed => ofRadix(10, digit, c, endLabel) + case BreakCharDesc.Supported(c, _) => ofRadix(10, nonZeroDigit, digit, c, endLabel) <|> secretZero } - def plainHexadecimal(desc: NumericDesc): Parsley[BigInt] = plainHexadecimal(desc.leadingZerosAllowed, desc.literalBreakChar) - def plainHexadecimal(leadingZerosAllowed: Boolean, literalBreakChar: BreakCharDesc): Parsley[BigInt] = literalBreakChar match { - case BreakCharDesc.NoBreakChar if leadingZerosAllowed => zeroAllowedHexadecimal - case BreakCharDesc.NoBreakChar => zeroNotAllowedHexadecimal - case BreakCharDesc.Supported(c, _) if leadingZerosAllowed => ofRadix(16, hexDigit, c) - case BreakCharDesc.Supported(c, _) => ofRadix(16, hexDigit - '0', hexDigit, c) <|> ('0' #> BigInt(0)) + def plainHexadecimal(desc: NumericDesc, endLabel: LabelConfig): Parsley[BigInt] = { + plainHexadecimal(desc.leadingZerosAllowed, desc.literalBreakChar, endLabel) + } + private def plainHexadecimal(leadingZerosAllowed: Boolean, literalBreakChar: BreakCharDesc, endLabel: LabelConfig): Parsley[BigInt] = { + literalBreakChar match { + case BreakCharDesc.NoBreakChar if leadingZerosAllowed => zeroAllowedHexadecimal(endLabel) + case BreakCharDesc.NoBreakChar => zeroNotAllowedHexadecimal(endLabel) + case BreakCharDesc.Supported(c, _) if leadingZerosAllowed => ofRadix(16, hexDigit, c, endLabel) + case BreakCharDesc.Supported(c, _) => ofRadix(16, nonZeroHexDigit, hexDigit, c, endLabel) <|> secretZero + } } - def plainOctal(desc: NumericDesc): Parsley[BigInt] = plainOctal(desc.leadingZerosAllowed, desc.literalBreakChar) - def plainOctal(leadingZerosAllowed: Boolean, literalBreakChar: BreakCharDesc): Parsley[BigInt] = literalBreakChar match { - case BreakCharDesc.NoBreakChar if leadingZerosAllowed => zeroAllowedOctal - case BreakCharDesc.NoBreakChar => zeroNotAllowedOctal - case BreakCharDesc.Supported(c, _) if leadingZerosAllowed => ofRadix(8, octDigit, c) - case BreakCharDesc.Supported(c, _) => ofRadix(8, octDigit - '0', octDigit, c) <|> ('0' #> BigInt(0)) + def plainOctal(desc: NumericDesc, endLabel: LabelConfig): Parsley[BigInt] = plainOctal(desc.leadingZerosAllowed, desc.literalBreakChar, endLabel) + private def plainOctal(leadingZerosAllowed: Boolean, literalBreakChar: BreakCharDesc, endLabel: LabelConfig): Parsley[BigInt] = literalBreakChar match { + case BreakCharDesc.NoBreakChar if leadingZerosAllowed => zeroAllowedOctal(endLabel) + case BreakCharDesc.NoBreakChar => zeroNotAllowedOctal(endLabel) + case BreakCharDesc.Supported(c, _) if leadingZerosAllowed => ofRadix(8, octDigit, c, endLabel) + case BreakCharDesc.Supported(c, _) => ofRadix(8, nonZeroOctDigit, octDigit, c, endLabel) <|> secretZero } - def plainBinary(desc: NumericDesc): Parsley[BigInt] = plainBinary(desc.leadingZerosAllowed, desc.literalBreakChar) - def plainBinary(leadingZerosAllowed: Boolean, literalBreakChar: BreakCharDesc): Parsley[BigInt] = literalBreakChar match { - case BreakCharDesc.NoBreakChar if leadingZerosAllowed => zeroAllowedBinary - case BreakCharDesc.NoBreakChar => zeroNotAllowedBinary - case BreakCharDesc.Supported(c, _) if leadingZerosAllowed => ofRadix(2, bit, c) - case BreakCharDesc.Supported(c, _) => ofRadix(2, '1', bit, c) <|> ('0' #> BigInt(0)) + def plainBinary(desc: NumericDesc, endLabel: LabelConfig): Parsley[BigInt] = plainBinary(desc.leadingZerosAllowed, desc.literalBreakChar, endLabel) + private def plainBinary(leadingZerosAllowed: Boolean, literalBreakChar: BreakCharDesc, endLabel: LabelConfig): Parsley[BigInt] = literalBreakChar match { + case BreakCharDesc.NoBreakChar if leadingZerosAllowed => zeroAllowedBinary(endLabel) + case BreakCharDesc.NoBreakChar => zeroNotAllowedBinary(endLabel) + case BreakCharDesc.Supported(c, _) if leadingZerosAllowed => ofRadix(2, bit, c, endLabel) + case BreakCharDesc.Supported(c, _) => ofRadix(2, nonZeroBit, bit, c, endLabel) <|> secretZero } } diff --git a/parsley/shared/src/main/scala/parsley/token/numeric/Integer.scala b/parsley/shared/src/main/scala/parsley/token/numeric/Integer.scala index 0f32231bb..386cde159 100644 --- a/parsley/shared/src/main/scala/parsley/token/numeric/Integer.scala +++ b/parsley/shared/src/main/scala/parsley/token/numeric/Integer.scala @@ -5,6 +5,7 @@ package parsley.token.numeric import parsley.Parsley import parsley.token.descriptions.numeric.NumericDesc +import parsley.token.errors.{ErrorConfig, LabelWithExplainConfig} /** This class defines a uniform interface for defining parsers for integer * literals, independent of how whitespace should be handled after the literal @@ -28,7 +29,7 @@ import parsley.token.descriptions.numeric.NumericDesc * accounts for unsignedness when necessary. * @define bounded4 the desired type of the result, defaulting to */ -abstract class Integer private[token] (private [numeric] val desc: NumericDesc) { +abstract class Integer private[numeric] (private [numeric] val desc: NumericDesc) { /** This parser will parse a single integer literal, which is in decimal form (base 10). * * @example {{{ @@ -264,7 +265,8 @@ abstract class Integer private[token] (private [numeric] val desc: NumericDesc) @inline final def binary64[T: CanHold.can_hold_64_bits]: Parsley[T] = binaryBounded(_64) - protected [numeric] def bounded[T](number: Parsley[BigInt], bits: Bits, radix: Int)(implicit ev: CanHold[bits.self, T]): Parsley[T] + protected [numeric] def bounded[T](number: Parsley[BigInt], bits: Bits, radix: Int, label: (ErrorConfig, Boolean) => LabelWithExplainConfig) + (implicit ev: CanHold[bits.self, T]): Parsley[T] protected [numeric] def _decimal: Parsley[BigInt] = decimal protected [numeric] def _hexadecimal: Parsley[BigInt] = hexadecimal protected [numeric] def _octal: Parsley[BigInt] = octal @@ -272,9 +274,10 @@ abstract class Integer private[token] (private [numeric] val desc: NumericDesc) protected [numeric] def _number: Parsley[BigInt] = number // $COVERAGE-ON$ - private def numberBounded[T](bits: Bits)(implicit ev: CanHold[bits.self, T]): Parsley[T] = bounded(_number, bits, 10) - private def decimalBounded[T](bits: Bits)(implicit ev: CanHold[bits.self, T]): Parsley[T] = bounded(_decimal, bits, 10) - private def hexadecimalBounded[T](bits: Bits)(implicit ev: CanHold[bits.self, T]): Parsley[T] = bounded(_hexadecimal, bits, 16) - private def octalBounded[T](bits: Bits)(implicit ev: CanHold[bits.self, T]): Parsley[T] = bounded(_octal, bits, 8) - private def binaryBounded[T](bits: Bits)(implicit ev: CanHold[bits.self, T]): Parsley[T] = bounded(_binary, bits, 2) + private def numberBounded[T](bits: Bits)(implicit ev: CanHold[bits.self, T]): Parsley[T] = bounded(_number, bits, 10, _.labelNumber(bits.bits, _)) + private def decimalBounded[T](bits: Bits)(implicit ev: CanHold[bits.self, T]): Parsley[T] = bounded(_decimal, bits, 10, _.labelDecimal(bits.bits, _)) + private def hexadecimalBounded[T](bits: Bits)(implicit ev: CanHold[bits.self, T]): Parsley[T] = + bounded(_hexadecimal, bits, 16, _.labelHexadecimal(bits.bits, _)) + private def octalBounded[T](bits: Bits)(implicit ev: CanHold[bits.self, T]): Parsley[T] = bounded(_octal, bits, 8, _.labelOctal(bits.bits, _)) + private def binaryBounded[T](bits: Bits)(implicit ev: CanHold[bits.self, T]): Parsley[T] = bounded(_binary, bits, 2, _.labelBinary(bits.bits, _)) } diff --git a/parsley/shared/src/main/scala/parsley/token/numeric/LexemeCombined.scala b/parsley/shared/src/main/scala/parsley/token/numeric/LexemeCombined.scala index ce0fbb5c0..b46cc387c 100644 --- a/parsley/shared/src/main/scala/parsley/token/numeric/LexemeCombined.scala +++ b/parsley/shared/src/main/scala/parsley/token/numeric/LexemeCombined.scala @@ -5,8 +5,9 @@ package parsley.token.numeric import parsley.Parsley import parsley.token.Lexeme +import parsley.token.errors.ErrorConfig -private [token] final class LexemeCombined(combined: Combined, lexeme: Lexeme) extends Combined { +private [token] final class LexemeCombined(combined: Combined, lexeme: Lexeme, err: ErrorConfig) extends Combined(err) { override lazy val decimal: Parsley[Either[BigInt,BigDecimal]] = lexeme(combined.decimal) override lazy val hexadecimal: Parsley[Either[BigInt,BigDecimal]] = lexeme(combined.hexadecimal) override lazy val octal: Parsley[Either[BigInt,BigDecimal]] = lexeme(combined.octal) diff --git a/parsley/shared/src/main/scala/parsley/token/numeric/LexemeInteger.scala b/parsley/shared/src/main/scala/parsley/token/numeric/LexemeInteger.scala index d4e4a3da2..a9d51dd7d 100644 --- a/parsley/shared/src/main/scala/parsley/token/numeric/LexemeInteger.scala +++ b/parsley/shared/src/main/scala/parsley/token/numeric/LexemeInteger.scala @@ -5,6 +5,7 @@ package parsley.token.numeric import parsley.Parsley import parsley.token.Lexeme +import parsley.token.errors.{ErrorConfig, LabelWithExplainConfig} private [token] final class LexemeInteger(integer: Integer, lexeme: Lexeme) extends Integer(integer.desc) { override lazy val decimal: Parsley[BigInt] = lexeme(integer.decimal) @@ -13,8 +14,9 @@ private [token] final class LexemeInteger(integer: Integer, lexeme: Lexeme) exte override lazy val binary: Parsley[BigInt] = lexeme(integer.binary) override lazy val number: Parsley[BigInt] = lexeme(integer.number) - override protected[numeric] def bounded[T](number: Parsley[BigInt], bits: Bits, radix: Int)(implicit ev: CanHold[bits.self,T]): Parsley[T] = - lexeme(integer.bounded(number, bits, radix)) + override protected[numeric] def bounded[T](number: Parsley[BigInt], bits: Bits, radix: Int, label: (ErrorConfig, Boolean) => LabelWithExplainConfig) + (implicit ev: CanHold[bits.self,T]): Parsley[T] = + lexeme(integer.bounded(number, bits, radix, label)) override protected [numeric] def _decimal: Parsley[BigInt] = integer.decimal override protected [numeric] def _hexadecimal: Parsley[BigInt] = integer.hexadecimal diff --git a/parsley/shared/src/main/scala/parsley/token/numeric/LexemeReal.scala b/parsley/shared/src/main/scala/parsley/token/numeric/LexemeReal.scala index 42e00a489..5776b9af6 100644 --- a/parsley/shared/src/main/scala/parsley/token/numeric/LexemeReal.scala +++ b/parsley/shared/src/main/scala/parsley/token/numeric/LexemeReal.scala @@ -5,22 +5,27 @@ package parsley.token.numeric import parsley.Parsley import parsley.token.Lexeme +import parsley.token.errors.{ErrorConfig, LabelWithExplainConfig} -private [token] final class LexemeReal(rational: Real, lexeme: Lexeme) extends Real { - override lazy val decimal: Parsley[BigDecimal] = lexeme(rational.decimal) - override lazy val hexadecimal: Parsley[BigDecimal] = lexeme(rational.hexadecimal) - override lazy val octal: Parsley[BigDecimal] = lexeme(rational.octal) - override lazy val binary: Parsley[BigDecimal] = lexeme(rational.binary) - override lazy val number: Parsley[BigDecimal] = lexeme(rational.number) +private [token] final class LexemeReal(real: Real, lexeme: Lexeme, err: ErrorConfig) extends Real(err) { + override lazy val decimal: Parsley[BigDecimal] = lexeme(real.decimal) + override lazy val hexadecimal: Parsley[BigDecimal] = lexeme(real.hexadecimal) + override lazy val octal: Parsley[BigDecimal] = lexeme(real.octal) + override lazy val binary: Parsley[BigDecimal] = lexeme(real.binary) + override lazy val number: Parsley[BigDecimal] = lexeme(real.number) - override protected [numeric] def _decimal: Parsley[BigDecimal] = rational.decimal - override protected [numeric] def _hexadecimal: Parsley[BigDecimal] = rational.hexadecimal - override protected [numeric] def _octal: Parsley[BigDecimal] = rational.octal - override protected [numeric] def _binary: Parsley[BigDecimal] = rational.binary - override protected [numeric] def _number: Parsley[BigDecimal] = rational.number + override protected [numeric] def _decimal: Parsley[BigDecimal] = real.decimal + override protected [numeric] def _hexadecimal: Parsley[BigDecimal] = real.hexadecimal + override protected [numeric] def _octal: Parsley[BigDecimal] = real.octal + override protected [numeric] def _binary: Parsley[BigDecimal] = real.binary + override protected [numeric] def _number: Parsley[BigDecimal] = real.number - override protected [numeric] def ensureFloat(number: Parsley[BigDecimal]): Parsley[Float] = lexeme(super.ensureFloat(number)) - override protected [numeric] def ensureDouble(number: Parsley[BigDecimal]): Parsley[Double] = lexeme(super.ensureDouble(number)) - override protected [numeric] def ensureExactFloat(number: Parsley[BigDecimal]): Parsley[Float] = lexeme(super.ensureExactFloat(number)) - override protected [numeric] def ensureExactDouble(number: Parsley[BigDecimal]): Parsley[Double] = lexeme(super.ensureExactDouble(number)) + override protected [numeric] def ensureFloat(number: Parsley[BigDecimal], label: LabelWithExplainConfig): Parsley[Float] = + lexeme(super.ensureFloat(number, label)) + override protected [numeric] def ensureDouble(number: Parsley[BigDecimal], label: LabelWithExplainConfig): Parsley[Double] = + lexeme(super.ensureDouble(number, label)) + override protected [numeric] def ensureExactFloat(number: Parsley[BigDecimal], label: LabelWithExplainConfig): Parsley[Float] = + lexeme(super.ensureExactFloat(number, label)) + override protected [numeric] def ensureExactDouble(number: Parsley[BigDecimal], label: LabelWithExplainConfig): Parsley[Double] = + lexeme(super.ensureExactDouble(number, label)) } diff --git a/parsley/shared/src/main/scala/parsley/token/numeric/Real.scala b/parsley/shared/src/main/scala/parsley/token/numeric/Real.scala index 7dfb0c5d5..6dd682fb5 100644 --- a/parsley/shared/src/main/scala/parsley/token/numeric/Real.scala +++ b/parsley/shared/src/main/scala/parsley/token/numeric/Real.scala @@ -4,7 +4,7 @@ package parsley.token.numeric import parsley.Parsley -import parsley.errors.combinator.ErrorMethods +import parsley.token.errors.{ErrorConfig, LabelWithExplainConfig} /** This class defines a uniform interface for defining parsers for floating * literals, independent of how whitespace should be handled after the literal. @@ -32,7 +32,7 @@ import parsley.errors.combinator.ErrorMethods * if the values are too big or too negatively big, they will * be rounded to the corresponding infinity. */ -abstract class Real private[token] { +abstract class Real private[numeric](err: ErrorConfig) { /** This parser will parse a single real number literal, which is in decimal form (base 10). * * @since 4.0.0 @@ -150,35 +150,35 @@ abstract class Real private[token] { * @note $disclaimer * @note $plausible */ - lazy val decimalFloat: Parsley[Float] = ensureFloat(_decimal) + lazy val decimalFloat: Parsley[Float] = ensureFloat(_decimal, err.labelRealFloatDecimal) /** $bounded1 [[hexadecimal `hexadecimal`]] $bounded2Plausible single-precision $bounded3 `Float`. * * @since 4.0.0 * @note $disclaimer * @note $plausible */ - lazy val hexadecimalFloat: Parsley[Float] = ensureFloat(_hexadecimal) + lazy val hexadecimalFloat: Parsley[Float] = ensureFloat(_hexadecimal, err.labelRealFloatHexadecimal) /** $bounded1 [[octal `octal`]] $bounded2Plausible single-precision $bounded3 `Float`. * * @since 4.0.0 * @note $disclaimer * @note $plausible */ - lazy val octalFloat: Parsley[Float] = ensureFloat(_octal) + lazy val octalFloat: Parsley[Float] = ensureFloat(_octal, err.labelRealFloatOctal) /** $bounded1 [[binary `binary`]] $bounded2Plausible single-precision $bounded3 `Float`. * * @since 4.0.0 * @note $disclaimer * @note $plausible */ - lazy val binaryFloat: Parsley[Float] = ensureFloat(_binary) + lazy val binaryFloat: Parsley[Float] = ensureFloat(_binary, err.labelRealFloatBinary) /** $bounded1 [[number `number`]] $bounded2Plausible single-precision $bounded3 `Float`. * * @since 4.0.0 * @note $disclaimer * @note $plausible */ - lazy val float: Parsley[Float] = ensureFloat(_number) + lazy val float: Parsley[Float] = ensureFloat(_number, err.labelRealFloatNumber) /** $bounded1 [[decimal `decimal`]] $bounded2Plausible double-precision $bounded3 `Double`. * @@ -186,70 +186,70 @@ abstract class Real private[token] { * @note $disclaimer * @note $plausible */ - lazy val decimalDouble: Parsley[Double] = ensureDouble(_decimal) + lazy val decimalDouble: Parsley[Double] = ensureDouble(_decimal, err.labelRealDoubleDecimal) /** $bounded1 [[hexadecimal `hexadecimal`]] $bounded2Plausible double-precision $bounded3 `Double`. * * @since 4.0.0 * @note $disclaimer * @note $plausible */ - lazy val hexadecimalDouble: Parsley[Double] = ensureDouble(_hexadecimal) + lazy val hexadecimalDouble: Parsley[Double] = ensureDouble(_hexadecimal, err.labelRealDoubleHexadecimal) /** $bounded1 [[octal `octal`]] $bounded2Plausible double-precision $bounded3 `Double`. * * @since 4.0.0 * @note $disclaimer * @note $plausible */ - lazy val octalDouble: Parsley[Double] = ensureDouble(_octal) + lazy val octalDouble: Parsley[Double] = ensureDouble(_octal, err.labelRealDoubleOctal) /** $bounded1 [[binary `binary`]] $bounded2Plausible double-precision $bounded3 `Double`. * * @since 4.0.0 * @note $disclaimer * @note $plausible */ - lazy val binaryDouble: Parsley[Double] = ensureDouble(_binary) + lazy val binaryDouble: Parsley[Double] = ensureDouble(_binary, err.labelRealDoubleBinary) /** $bounded1 [[number `number`]] $bounded2Plausible double-precision $bounded3 `Double`. * * @since 4.0.0 * @note $disclaimer * @note $plausible */ - lazy val double: Parsley[Double] = ensureDouble(_number) + lazy val double: Parsley[Double] = ensureDouble(_number, err.labelRealDoubleNumber) /** $bounded1 [[decimal `decimal`]] $bounded2Plausible single-precision $bounded3 `Float`. * * @since 4.0.0 * @note $disclaimer * @note $exact */ - lazy val decimalExactFloat: Parsley[Float] = ensureExactFloat(_decimal) + lazy val decimalExactFloat: Parsley[Float] = ensureExactFloat(_decimal, err.labelRealFloatDecimal) /** $bounded1 [[hexadecimal `hexadecimal`]] $bounded2Exact single-precision $bounded3 `Float`. * * @since 4.0.0 * @note $disclaimer * @note $exact */ - lazy val hexadecimalExactFloat: Parsley[Float] = ensureExactFloat(_hexadecimal) + lazy val hexadecimalExactFloat: Parsley[Float] = ensureExactFloat(_hexadecimal, err.labelRealFloatHexadecimal) /** $bounded1 [[octal `octal`]] $bounded2Exact single-precision $bounded3 `Float`. * * @since 4.0.0 * @note $disclaimer * @note $exact */ - lazy val octalExactFloat: Parsley[Float] = ensureExactFloat(_octal) + lazy val octalExactFloat: Parsley[Float] = ensureExactFloat(_octal, err.labelRealFloatOctal) /** $bounded1 [[binary `binary`]] $bounded2Exact single-precision $bounded3 `Float`. * * @since 4.0.0 * @note $disclaimer * @note $exact */ - lazy val binaryExactFloat: Parsley[Float] = ensureExactFloat(_binary) + lazy val binaryExactFloat: Parsley[Float] = ensureExactFloat(_binary, err.labelRealFloatBinary) /** $bounded1 [[number `number`]] $bounded2Exact single-precision $bounded3 `Float`. * * @since 4.0.0 * @note $disclaimer * @note $exact */ - lazy val exactFloat: Parsley[Float] = ensureExactFloat(_number) + lazy val exactFloat: Parsley[Float] = ensureExactFloat(_number, err.labelRealFloatNumber) /** $bounded1 [[decimal `decimal`]] $bounded2Exact double-precision $bounded3 `Double`. * @@ -257,64 +257,57 @@ abstract class Real private[token] { * @note $disclaimer * @note $exact */ - lazy val decimalExactDouble: Parsley[Double] = ensureExactDouble(_decimal) + lazy val decimalExactDouble: Parsley[Double] = ensureExactDouble(_decimal, err.labelRealDoubleDecimal) /** $bounded1 [[hexadecimal `hexadecimal`]] $bounded2Exact double-precision $bounded3 `Double`. * * @since 4.0.0 * @note $disclaimer * @note $exact */ - lazy val hexadecimalExactDouble: Parsley[Double] = ensureExactDouble(_hexadecimal) + lazy val hexadecimalExactDouble: Parsley[Double] = ensureExactDouble(_hexadecimal, err.labelRealDoubleHexadecimal) /** $bounded1 [[octal `octal`]] $bounded2Exact double-precision $bounded3 `Double`. * * @since 4.0.0 * @note $disclaimer * @note $exact */ - lazy val octalExactDouble: Parsley[Double] = ensureExactDouble(_octal) + lazy val octalExactDouble: Parsley[Double] = ensureExactDouble(_octal, err.labelRealDoubleOctal) /** $bounded1 [[binary `binary`]] $bounded2Exact double-precision $bounded3 `Double`. * * @since 4.0.0 * @note $disclaimer * @note $exact */ - lazy val binaryExactDouble: Parsley[Double] = ensureExactDouble(_binary) + lazy val binaryExactDouble: Parsley[Double] = ensureExactDouble(_binary, err.labelRealDoubleBinary) /** $bounded1 [[number `number`]] $bounded2Exact double-precision $bounded3 `Double`. * * @since 4.0.0 * @note $disclaimer * @note $exact */ - lazy val exactDouble: Parsley[Double] = ensureExactDouble(_number) + lazy val exactDouble: Parsley[Double] = ensureExactDouble(_number, err.labelRealDoubleNumber) // $COVERAGE-ON$ - private def bad(min: Double, max: Double, precision: String)(n: BigDecimal): Seq[String] = { - if (n > BigDecimal(max) || n < BigDecimal(min)) { - Seq(s"literal $n is too large to be an IEEE 754 $precision-precision float") - } - else Seq(s"literal $n is too small to be an IEEE 754 $precision-precision float") - } - - protected [numeric] def ensureFloat(number: Parsley[BigDecimal]): Parsley[Float] = { - number.collectMsg(bad(Float.MinValue.toDouble, Float.MaxValue.toDouble, "single")(_)) { + protected [numeric] def ensureFloat(number: Parsley[BigDecimal], label: LabelWithExplainConfig): Parsley[Float] = { + err.filterRealOutOfBounds(err.floatName, BigDecimal(Float.MinValue.toDouble), BigDecimal(Float.MaxValue.toDouble)).collect(label(number)) { case n if Real.isFloat(n) => n.toFloat } } - protected [numeric] def ensureDouble(number: Parsley[BigDecimal]): Parsley[Double] = { - number.collectMsg(bad(Double.MinValue, Double.MaxValue, "double")(_)) { + protected [numeric] def ensureDouble(number: Parsley[BigDecimal], label: LabelWithExplainConfig): Parsley[Double] = { + err.filterRealOutOfBounds(err.doubleName, BigDecimal(Double.MinValue), BigDecimal(Double.MaxValue)).collect(label(number)) { case n if Real.isDouble(n) => n.toDouble } } - protected [numeric] def ensureExactFloat(number: Parsley[BigDecimal]): Parsley[Float] = { - number.collectMsg(n => Seq(s"$n cannot be represented exactly as a IEEE 754 single-precision float")) { + protected [numeric] def ensureExactFloat(number: Parsley[BigDecimal], label: LabelWithExplainConfig): Parsley[Float] = { + err.filterRealNotExact(err.floatName).collect(label(number)) { case n if n.isExactFloat => n.toFloat } } - protected [numeric] def ensureExactDouble(number: Parsley[BigDecimal]): Parsley[Double] = { - number.collectMsg(n => Seq(s"$n cannot be represented exactly as a IEEE 754 double-precision float")) { + protected [numeric] def ensureExactDouble(number: Parsley[BigDecimal], label: LabelWithExplainConfig): Parsley[Double] = { + err.filterRealNotExact(err.doubleName).collect(label(number)) { case n if n.isExactDouble => n.toDouble } } diff --git a/parsley/shared/src/main/scala/parsley/token/numeric/SignedCombined.scala b/parsley/shared/src/main/scala/parsley/token/numeric/SignedCombined.scala index 7a6bc3e8f..cff38d8b5 100644 --- a/parsley/shared/src/main/scala/parsley/token/numeric/SignedCombined.scala +++ b/parsley/shared/src/main/scala/parsley/token/numeric/SignedCombined.scala @@ -4,13 +4,13 @@ package parsley.token.numeric import parsley.Parsley, Parsley.attempt -import parsley.errors.combinator.ErrorMethods import parsley.token.descriptions.numeric.NumericDesc +import parsley.token.errors.ErrorConfig import parsley.internal.deepembedding.Sign.CombinedType import parsley.internal.deepembedding.singletons -private [token] final class SignedCombined(desc: NumericDesc, unsigned: Combined) extends Combined { +private [token] final class SignedCombined(desc: NumericDesc, unsigned: Combined, err: ErrorConfig) extends Combined(err) { private val sign = new Parsley(new singletons.Sign[CombinedType.resultType](CombinedType, desc.positiveSign)) override def decimal: Parsley[Either[BigInt,BigDecimal]] = attempt(sign <*> unsigned.decimal) @@ -21,11 +21,7 @@ private [token] final class SignedCombined(desc: NumericDesc, unsigned: Combined override protected[numeric] def bounded[T](number: Parsley[Either[BigInt,BigDecimal]], bits: Bits, radix: Int) (implicit ev: CanHold[bits.self,T]): Parsley[Either[T,BigDecimal]] = { - number.collectMsg(ex => { - val Left(x) = ex - Seq(if (x > bits.upperSigned) s"literal $x is larger than the max value of ${bits.upperSigned}" - else s"literal $x is less than the min value of ${bits.lowerSigned}") - }) { + err.filterIntegerOutOfBounds(bits.lowerSigned, bits.upperSigned, radix).injectLeft.collect(number) { case Left(x) if bits.lowerSigned <= x && x <= bits.upperSigned => Left(ev.fromBigInt(x)) case Right(y) => Right(y) } diff --git a/parsley/shared/src/main/scala/parsley/token/numeric/SignedInteger.scala b/parsley/shared/src/main/scala/parsley/token/numeric/SignedInteger.scala index c647de206..80ee55aa3 100644 --- a/parsley/shared/src/main/scala/parsley/token/numeric/SignedInteger.scala +++ b/parsley/shared/src/main/scala/parsley/token/numeric/SignedInteger.scala @@ -4,24 +4,30 @@ package parsley.token.numeric import parsley.Parsley, Parsley.attempt -import parsley.errors.combinator.ErrorMethods import parsley.token.descriptions.numeric.NumericDesc +import parsley.token.errors.{ErrorConfig, LabelWithExplainConfig} import parsley.internal.deepembedding.Sign.IntType import parsley.internal.deepembedding.singletons -private [token] final class SignedInteger(desc: NumericDesc, unsigned: Integer) extends Integer(desc) { +private [token] final class SignedInteger(desc: NumericDesc, unsigned: UnsignedInteger, err: ErrorConfig) extends Integer(desc) { private val sign = new Parsley(new singletons.Sign[IntType.resultType](IntType, desc.positiveSign)) - override lazy val decimal: Parsley[BigInt] = attempt(sign <*> unsigned.decimal) - override lazy val hexadecimal: Parsley[BigInt] = attempt(sign <*> unsigned.hexadecimal) - override lazy val octal: Parsley[BigInt] = attempt(sign <*> unsigned.octal) - override lazy val binary: Parsley[BigInt] = attempt(sign <*> unsigned.binary) - override lazy val number: Parsley[BigInt] = attempt(sign <*> unsigned.number) - // TODO: render in the "native" radix - override protected [numeric] def bounded[T](number: Parsley[BigInt], bits: Bits, radix: Int)(implicit ev: CanHold[bits.self,T]): Parsley[T] = { - number.collectMsg(x => Seq(if (x > bits.upperSigned) s"literal $x is larger than the max value of ${bits.upperSigned}" - else s"literal $x is less than the min value of ${bits.lowerSigned}")) { + override lazy val _decimal: Parsley[BigInt] = attempt(sign <*> err.labelIntegerDecimalEnd(unsigned._decimal)) + override lazy val _hexadecimal: Parsley[BigInt] = attempt(sign <*> err.labelIntegerHexadecimalEnd(unsigned._hexadecimal)) + override lazy val _octal: Parsley[BigInt] = attempt(sign <*> err.labelIntegerOctalEnd(unsigned._octal)) + override lazy val _binary: Parsley[BigInt] = attempt(sign <*> err.labelIntegerBinaryEnd(unsigned._binary)) + override lazy val _number: Parsley[BigInt] = attempt(sign <*> err.labelIntegerNumberEnd(unsigned._number)) + + override def decimal: Parsley[BigInt] = err.labelIntegerSignedDecimal.apply(_decimal) + override def hexadecimal: Parsley[BigInt] = err.labelIntegerSignedHexadecimal.apply(_hexadecimal) + override def octal: Parsley[BigInt] = err.labelIntegerSignedOctal.apply(_octal) + override def binary: Parsley[BigInt] = err.labelIntegerSignedBinary.apply(_binary) + override def number: Parsley[BigInt] = err.labelIntegerSignedNumber.apply(_number) + + override protected [numeric] def bounded[T](number: Parsley[BigInt], bits: Bits, radix: Int, label: (ErrorConfig, Boolean) => LabelWithExplainConfig) + (implicit ev: CanHold[bits.self,T]): Parsley[T] = label(err, false) { + err.filterIntegerOutOfBounds(bits.lowerSigned, bits.upperSigned, radix).collect(number) { case x if bits.lowerSigned <= x && x <= bits.upperSigned => ev.fromBigInt(x) } } diff --git a/parsley/shared/src/main/scala/parsley/token/numeric/SignedReal.scala b/parsley/shared/src/main/scala/parsley/token/numeric/SignedReal.scala index c5e0afa1f..a327b8eb5 100644 --- a/parsley/shared/src/main/scala/parsley/token/numeric/SignedReal.scala +++ b/parsley/shared/src/main/scala/parsley/token/numeric/SignedReal.scala @@ -5,16 +5,23 @@ package parsley.token.numeric import parsley.Parsley, Parsley.attempt import parsley.token.descriptions.numeric.NumericDesc +import parsley.token.errors.ErrorConfig import parsley.internal.deepembedding.Sign.DoubleType import parsley.internal.deepembedding.singletons -private [token] final class SignedReal(desc: NumericDesc, unsigned: Real) extends Real { +private [token] final class SignedReal(desc: NumericDesc, unsigned: Real, err: ErrorConfig) extends Real(err) { private val sign = new Parsley(new singletons.Sign[DoubleType.resultType](DoubleType, desc.positiveSign)) - override lazy val decimal: Parsley[BigDecimal] = attempt(sign <*> unsigned.decimal) - override lazy val hexadecimal: Parsley[BigDecimal] = attempt(sign <*> unsigned.hexadecimal) - override lazy val octal: Parsley[BigDecimal] = attempt(sign <*> unsigned.octal) - override lazy val binary: Parsley[BigDecimal] = attempt(sign <*> unsigned.binary) - override lazy val number: Parsley[BigDecimal] = attempt(sign <*> unsigned.number) + override lazy val _decimal: Parsley[BigDecimal] = attempt(sign <*> err.labelRealDecimalEnd(unsigned._decimal)) + override lazy val _hexadecimal: Parsley[BigDecimal] = attempt(sign <*> err.labelRealHexadecimalEnd(unsigned._hexadecimal)) + override lazy val _octal: Parsley[BigDecimal] = attempt(sign <*> err.labelRealOctalEnd(unsigned._octal)) + override lazy val _binary: Parsley[BigDecimal] = attempt(sign <*> err.labelRealBinaryEnd(unsigned._binary)) + override lazy val _number: Parsley[BigDecimal] = attempt(sign <*> err.labelRealNumberEnd(unsigned._number)) + + override def decimal: Parsley[BigDecimal] = err.labelRealDecimal(_decimal) + override def hexadecimal: Parsley[BigDecimal] = err.labelRealHexadecimal(_hexadecimal) + override def octal: Parsley[BigDecimal] = err.labelRealOctal(_octal) + override def binary: Parsley[BigDecimal] = err.labelRealBinary(_binary) + override def number: Parsley[BigDecimal] = err.labelRealNumber(_number) } diff --git a/parsley/shared/src/main/scala/parsley/token/numeric/UnsignedCombined.scala b/parsley/shared/src/main/scala/parsley/token/numeric/UnsignedCombined.scala index ca684eebe..760f83be2 100644 --- a/parsley/shared/src/main/scala/parsley/token/numeric/UnsignedCombined.scala +++ b/parsley/shared/src/main/scala/parsley/token/numeric/UnsignedCombined.scala @@ -5,10 +5,10 @@ package parsley.token.numeric import parsley.Parsley, Parsley.attempt import parsley.XCompat.unused -import parsley.errors.combinator.ErrorMethods import parsley.token.descriptions.numeric.NumericDesc +import parsley.token.errors.ErrorConfig -private [token] final class UnsignedCombined(@unused desc: NumericDesc, integer: Integer, rational: Real) extends Combined { +private [token] final class UnsignedCombined(@unused desc: NumericDesc, integer: Integer, rational: Real, err: ErrorConfig) extends Combined(err) { override lazy val decimal: Parsley[Either[BigInt, BigDecimal]] = (attempt(rational.decimal) <+> integer.decimal).map(_.swap) override lazy val hexadecimal: Parsley[Either[BigInt, BigDecimal]] = (attempt(rational.hexadecimal) <+> integer.hexadecimal).map(_.swap) override lazy val octal: Parsley[Either[BigInt, BigDecimal]] = (attempt(rational.octal) <+> integer.octal).map(_.swap) @@ -36,10 +36,9 @@ private [token] final class UnsignedCombined(@unused desc: NumericDesc, integer: //private val noZeroOctal = oneOf(desc.octalLeads) *> ofRadix(8, 8, octDigit, oneOf('p', 'P')) //private val noZeroBinary = oneOf(desc.binaryLeads) *> ofRadix(2, 2, oneOf('0', '1'), oneOf('p', 'P')) - // TODO: render in the "native" radix override protected [numeric] def bounded[T](number: Parsley[Either[BigInt, BigDecimal]], bits: Bits, radix: Int) (implicit ev: CanHold[bits.self,T]): Parsley[Either[T, BigDecimal]] = { - number.collectMsg(x => Seq(s"literal ${x.asInstanceOf[Left[BigInt, Nothing]].value} is larger than the max value of ${bits.upperUnsigned}")) { + err.filterIntegerOutOfBounds(min = 0, bits.upperUnsigned, radix).injectLeft.collect(number) { case Left(x) if x <= bits.upperUnsigned => Left(ev.fromBigInt(x)) case Right(x) => Right(x) } diff --git a/parsley/shared/src/main/scala/parsley/token/numeric/UnsignedInteger.scala b/parsley/shared/src/main/scala/parsley/token/numeric/UnsignedInteger.scala index 2d2c6f921..02801143d 100644 --- a/parsley/shared/src/main/scala/parsley/token/numeric/UnsignedInteger.scala +++ b/parsley/shared/src/main/scala/parsley/token/numeric/UnsignedInteger.scala @@ -9,13 +9,16 @@ import parsley.combinator.optional import parsley.errors.combinator.ErrorMethods import parsley.implicits.character.charLift import parsley.token.descriptions.numeric.{BreakCharDesc, NumericDesc} +import parsley.token.errors.{ErrorConfig, LabelWithExplainConfig} -private [token] final class UnsignedInteger(desc: NumericDesc) extends Integer(desc) { - override lazy val decimal: Parsley[BigInt] = Generic.plainDecimal(desc) - override lazy val hexadecimal: Parsley[BigInt] = attempt('0' *> noZeroHexadecimal) - override lazy val octal: Parsley[BigInt] = attempt('0' *> noZeroOctal) - override lazy val binary: Parsley[BigInt] = attempt('0' *> noZeroBinary) - override lazy val number: Parsley[BigInt] = { +private [token] final class UnsignedInteger(desc: NumericDesc, err: ErrorConfig, generic: Generic) extends Integer(desc) { + + // labelless versions + protected [numeric] override lazy val _decimal: Parsley[BigInt] = generic.plainDecimal(desc, err.labelIntegerDecimalEnd) + protected [numeric] override lazy val _hexadecimal: Parsley[BigInt] = attempt('0' *> noZeroHexadecimal) + protected [numeric] override lazy val _octal: Parsley[BigInt] = attempt('0' *> noZeroOctal) + protected [numeric] override lazy val _binary: Parsley[BigInt] = attempt('0' *> noZeroBinary) + protected [numeric] override lazy val _number: Parsley[BigInt] = { if (desc.decimalIntegersOnly) decimal else { def addHex(p: Parsley[BigInt]) = { @@ -30,28 +33,38 @@ private [token] final class UnsignedInteger(desc: NumericDesc) extends Integer(d if (desc.integerNumbersCanBeBinary) noZeroBinary <|> p else p } - val zeroLead = '0' *> (addHex(addOct(addBin(decimal <|> pure(BigInt(0)))))) + val zeroLead = '0'.label("digit") *> (addHex(addOct(addBin(decimal <|> pure(BigInt(0)))))) attempt(zeroLead <|> decimal) } } - private def when(b: Boolean, p: Parsley[_]): Parsley[Unit] = if (b) p.void else unit + override def decimal: Parsley[BigInt] = err.labelIntegerUnsignedDecimal.apply(_decimal) + override def hexadecimal: Parsley[BigInt] = err.labelIntegerUnsignedHexadecimal.apply(_hexadecimal) + override def octal: Parsley[BigInt] = err.labelIntegerUnsignedOctal.apply(_octal) + override def binary: Parsley[BigInt] = err.labelIntegerUnsignedBinary.apply(_binary) + override def number: Parsley[BigInt] = err.labelIntegerUnsignedNumber.apply(_number) + + private def when(b: Boolean, p: Parsley[_]): Parsley[_] = if (b) p else unit val leadingBreakChar = desc.literalBreakChar match { case BreakCharDesc.NoBreakChar => unit case BreakCharDesc.Supported(breakChar, allowedAfterNonDecimalPrefix) => when(allowedAfterNonDecimalPrefix, optional(breakChar)) } - // TODO: Using choice here will generate a jump table, which will be nicer for `number` (this requires enhancements to the jumptable optimisation) - // TODO: Leave these as defs so they get inlined into number for the jumptable optimisation - private val noZeroHexadecimal = when(desc.hexadecimalLeads.nonEmpty, oneOf(desc.hexadecimalLeads)) *> leadingBreakChar *> Generic.plainHexadecimal(desc) - private val noZeroOctal = when(desc.octalLeads.nonEmpty, oneOf(desc.octalLeads)) *> leadingBreakChar *> Generic.plainOctal(desc) - private val noZeroBinary = when(desc.binaryLeads.nonEmpty, oneOf(desc.binaryLeads)) *> leadingBreakChar *> Generic.plainBinary(desc) + private val noZeroHexadecimal = + when(desc.hexadecimalLeads.nonEmpty, oneOf(desc.hexadecimalLeads)) *> leadingBreakChar *> + err.labelIntegerHexadecimalEnd(generic.plainHexadecimal(desc, err.labelIntegerHexadecimalEnd)) + private val noZeroOctal = + when(desc.octalLeads.nonEmpty, oneOf(desc.octalLeads)) *> leadingBreakChar *> + err.labelIntegerOctalEnd(generic.plainOctal(desc, err.labelIntegerOctalEnd)) + private val noZeroBinary = + when(desc.binaryLeads.nonEmpty, oneOf(desc.binaryLeads)) *> leadingBreakChar *> + err.labelIntegerBinaryEnd(generic.plainBinary(desc, err.labelIntegerBinaryEnd)) - // TODO: render in the "native" radix - override protected [numeric] def bounded[T](number: Parsley[BigInt], bits: Bits, radix: Int)(implicit ev: CanHold[bits.self,T]): Parsley[T] = { - number.collectMsg(x => Seq(s"literal $x is larger than the max value of ${bits.upperUnsigned}")) { - case x if x <= bits.upperUnsigned => ev.fromBigInt(x) + override protected [numeric] def bounded[T](number: Parsley[BigInt], bits: Bits, radix: Int, label: (ErrorConfig, Boolean) => LabelWithExplainConfig) + (implicit ev: CanHold[bits.self,T]): Parsley[T] = label(err, false) { + err.filterIntegerOutOfBounds(min = 0, bits.upperUnsigned, radix).collect(number) { + case x if 0 <= x && x <= bits.upperUnsigned => ev.fromBigInt(x) } } } diff --git a/parsley/shared/src/main/scala/parsley/token/numeric/UnsignedReal.scala b/parsley/shared/src/main/scala/parsley/token/numeric/UnsignedReal.scala index 204bb491c..8d0803f24 100644 --- a/parsley/shared/src/main/scala/parsley/token/numeric/UnsignedReal.scala +++ b/parsley/shared/src/main/scala/parsley/token/numeric/UnsignedReal.scala @@ -4,20 +4,21 @@ package parsley.token.numeric import parsley.Parsley, Parsley.{attempt, empty, pure, unit} -import parsley.character.{digit, hexDigit, octDigit, oneOf} +import parsley.character.{bit, digit, hexDigit, octDigit, oneOf} import parsley.combinator, combinator.optional -import parsley.errors.combinator.{amend, entrench, ErrorMethods} +import parsley.errors.combinator.{amendThenDislodge, entrench} import parsley.implicits.character.charLift import parsley.lift.lift2 import parsley.registers.Reg import parsley.token.descriptions.numeric.{BreakCharDesc, ExponentDesc, NumericDesc} +import parsley.token.errors.{ErrorConfig, LabelConfig} -private [token] final class UnsignedReal(desc: NumericDesc, natural: Integer) extends Real { - override lazy val decimal: Parsley[BigDecimal] = attempt(ofRadix(10, digit)) - override lazy val hexadecimal: Parsley[BigDecimal] = attempt('0' *> noZeroHexadecimal) - override lazy val octal: Parsley[BigDecimal] = attempt('0' *> noZeroOctal) - override lazy val binary: Parsley[BigDecimal] = attempt('0' *> noZeroBinary) - override lazy val number: Parsley[BigDecimal] = { +private [token] final class UnsignedReal(desc: NumericDesc, natural: UnsignedInteger, err: ErrorConfig, generic: Generic) extends Real(err) { + override lazy val _decimal: Parsley[BigDecimal] = attempt(ofRadix(10, digit, err.labelRealDecimalEnd)) + override lazy val _hexadecimal: Parsley[BigDecimal] = attempt('0' *> noZeroHexadecimal) + override lazy val _octal: Parsley[BigDecimal] = attempt('0' *> noZeroOctal) + override lazy val _binary: Parsley[BigDecimal] = attempt('0' *> noZeroBinary) + override lazy val _number: Parsley[BigDecimal] = { if (desc.decimalRealsOnly) decimal else { def addHex(p: Parsley[BigDecimal]) = { @@ -33,46 +34,62 @@ private [token] final class UnsignedReal(desc: NumericDesc, natural: Integer) ex else p } // this promotes sharing when the definitions would be otherwise equal - val leadingDotAllowedDecimal = if (desc.leadingDotAllowed) decimal else ofRadix(10, digit, leadingDotAllowed = true) + val leadingDotAllowedDecimal = if (desc.leadingDotAllowed) decimal else ofRadix(10, digit, leadingDotAllowed = true, err.labelRealNumberEnd) // not even accounting for the leading and trailing dot being allowed! val zeroLead = '0' *> (addHex(addOct(addBin(leadingDotAllowedDecimal <|> pure(BigDecimal(0)))))) attempt(zeroLead <|> decimal) } } - private def when(b: Boolean, p: Parsley[_]): Parsley[Unit] = if (b) p.void else unit + override def decimal: Parsley[BigDecimal] = err.labelRealDecimal(_decimal) + override def hexadecimal: Parsley[BigDecimal] = err.labelRealHexadecimal(_hexadecimal) + override def octal: Parsley[BigDecimal] = err.labelRealOctal(_octal) + override def binary: Parsley[BigDecimal] = err.labelRealBinary(_binary) + override def number: Parsley[BigDecimal] = err.labelRealNumber(_number) - val leadingBreakChar = desc.literalBreakChar match { + private def when(b: Boolean, p: Parsley[_]): Parsley[_] = if (b) p else unit + + def leadingBreakChar(label: LabelConfig): Parsley[_] = desc.literalBreakChar match { case BreakCharDesc.NoBreakChar => unit - case BreakCharDesc.Supported(breakChar, allowedAfterNonDecimalPrefix) => when(allowedAfterNonDecimalPrefix, optional(breakChar)) + case BreakCharDesc.Supported(breakChar, allowedAfterNonDecimalPrefix) => + when(allowedAfterNonDecimalPrefix, err.labelNumericBreakChar.orElse(label)(optional(breakChar))) } - // TODO: Using choice here will generate a jump table, which will be nicer for `number` (this requires enhancements to the jumptable optimisation) - // TODO: Leave these as defs so they get inlined into number for the jumptable optimisation - private val noZeroHexadecimal = when(desc.hexadecimalLeads.nonEmpty, oneOf(desc.hexadecimalLeads)) *> leadingBreakChar *> ofRadix(16, hexDigit) - private val noZeroOctal = when(desc.octalLeads.nonEmpty, oneOf(desc.octalLeads)) *> leadingBreakChar *> ofRadix(8, octDigit) - private val noZeroBinary = when(desc.binaryLeads.nonEmpty, oneOf(desc.binaryLeads)) *> leadingBreakChar *> ofRadix(2, oneOf('0', '1')) + private val noZeroHexadecimal = + when(desc.hexadecimalLeads.nonEmpty, oneOf(desc.hexadecimalLeads)) *> + leadingBreakChar(err.labelRealHexadecimalEnd) *> + ofRadix(16, hexDigit, err.labelRealHexadecimalEnd) + private val noZeroOctal = + when(desc.octalLeads.nonEmpty, oneOf(desc.octalLeads)) *> + leadingBreakChar(err.labelRealOctalEnd) *> + ofRadix(8, octDigit, err.labelRealOctalEnd) + private val noZeroBinary = + when(desc.binaryLeads.nonEmpty, oneOf(desc.binaryLeads)) *> + leadingBreakChar(err.labelRealBinaryEnd) *> + ofRadix(2, bit, err.labelRealBinaryEnd) // could allow integers to be parsed here according to configuration, the intOrFloat captures that case anyway - private def ofRadix(radix: Int, digit: Parsley[Char]): Parsley[BigDecimal] = ofRadix(radix, digit, desc.leadingDotAllowed) - private def ofRadix(radix: Int, digit: Parsley[Char], leadingDotAllowed: Boolean): Parsley[BigDecimal] = { + private def ofRadix(radix: Int, digit: Parsley[Char], endLabel: LabelConfig): Parsley[BigDecimal] = { + ofRadix(radix, digit, desc.leadingDotAllowed, endLabel) + } + private def ofRadix(radix: Int, digit: Parsley[Char], leadingDotAllowed: Boolean, endLabel: LabelConfig): Parsley[BigDecimal] = { lazy val leadingHappened = Reg.make[Boolean] - lazy val _noDoubleDroppedZero = leadingHappened.get.filterOut { - case true => "a real number cannot drop both a leading and trailing zero" - } + lazy val _noDoubleDroppedZero = err.preventRealDoubleDroppedZero(leadingHappened.get) val expDesc = desc.exponentDescForRadix(radix) - // TODO: this should reuse components of unsigned generic numbers, which will prevent duplication in a larger parser - // At the moment, a break character will prevent reuse val whole = radix match { - case 10 => Generic.plainDecimal(desc) - case 16 => Generic.plainHexadecimal(desc) - case 8 => Generic.plainOctal(desc) - case 2 => Generic.plainBinary(desc) + case 10 => generic.plainDecimal(desc, endLabel) + case 16 => generic.plainHexadecimal(desc, endLabel) + case 8 => generic.plainOctal(desc, endLabel) + case 2 => generic.plainBinary(desc, endLabel) } val f = (d: Char, x: BigDecimal) => x/radix + d.asDigit - def broken(c: Char) = lift2(f, digit, (optional(c) *> digit).foldRight[BigDecimal](0)(f)) - val fractional = amend { - '.' *> { + def broken(c: Char) = + lift2(f, + endLabel(digit), + (err.labelNumericBreakChar.orElse(endLabel)(optional(c)) *> + endLabel(digit)).foldRight[BigDecimal](0)(f)) + val fractional = amendThenDislodge { + err.labelRealDot.orElse(endLabel)('.') *> { desc.literalBreakChar match { case BreakCharDesc.NoBreakChar if desc.trailingDotAllowed => if (!leadingDotAllowed) entrench(digit.foldRight[BigDecimal](0)(f)) @@ -86,8 +103,12 @@ private [token] final class UnsignedReal(desc: NumericDesc, natural: Integer) ex } val (requiredExponent, exponent, base) = expDesc match { case ExponentDesc.Supported(compulsory, exp, base, sign) => - val integer = new SignedInteger(desc.copy(positiveSign = sign), natural) - val exponent = oneOf(exp) *> integer.decimal32 + val expErr = new ErrorConfig { + override def labelIntegerSignedDecimal(bits: Int) = err.labelRealExponentEnd.orElse(endLabel) + override def labelIntegerDecimalEnd = err.labelRealExponentEnd.orElse(endLabel) + } + val integer = new SignedInteger(desc.copy(positiveSign = sign), natural, expErr) + val exponent = err.labelRealExponent.orElse(endLabel)(oneOf(exp)) *> integer.decimal32 if (compulsory) (exponent, exponent, base) else (exponent, exponent <|> pure(0), base) // this can't fail for non-required, it has to be the identity exponent diff --git a/parsley/shared/src/main/scala/parsley/token/predicate.scala b/parsley/shared/src/main/scala/parsley/token/predicate.scala index 1e6e8dd6a..b9c299eeb 100644 --- a/parsley/shared/src/main/scala/parsley/token/predicate.scala +++ b/parsley/shared/src/main/scala/parsley/token/predicate.scala @@ -3,10 +3,13 @@ */ package parsley.token +import scala.collection.immutable.NumericRange + import parsley.Parsley, Parsley.empty import parsley.character.{satisfy, satisfyUtf16} import parsley.exceptions.ParsleyException +// TODO: for parsley 5.0.0, make this a package? /** This module contains functionality to describe character predicates, which can * be used to determine what characters are valid for different tokens. * @@ -37,7 +40,7 @@ object predicate { * @since 4.0.0 */ final case class Unicode(predicate: Int => Boolean) extends CharPredicate { - private [token] override def toBmp = satisfy(c => predicate(c.toInt) && !c.isHighSurrogate) + private [token] override def toBmp = satisfy(c => predicate(c.toInt)) private [token] override def toUnicode = satisfyUtf16(predicate) private [token] override def toNative = toUnicode.void private [token] def startsWith(s: String) = s.nonEmpty && predicate(s.codePointAt(0)) @@ -61,6 +64,11 @@ object predicate { private [token] def startsWith(s: String) = s.headOption.exists(predicate) private [token] def endsWith(s: String) = s.lastOption.exists(predicate) } + // this runs the ability to pass functions in as it creates an overloading ambiguity + /*object Basic { + // TODO: expose + private [parsley] def apply(cs: Char*) = new Basic(Set(cs: _*)) + }*/ /** Character predicate that never succeeds. * @@ -74,10 +82,64 @@ object predicate { private [token] def endsWith(s: String) = true } - // This has been deprecated, but is still used in the tests, we'll replace it with something else down the line - // but this is free to remove without affecting bin-compat - private [parsley] object _CharSet { - def apply(cs: Set[Char]): CharPredicate = Basic(cs) - def apply(cs: Char*): CharPredicate = apply(Set(cs: _*)) + /** This object provides implicit functionality for constructing `CharPredicate` values. + * @since 4.1.0 + */ + object implicits { + /** Implicit conversions to make `Basic` values. + * @since 4.1.0 + */ + object Basic { + // $COVERAGE-OFF$ + /** Lifts a regular character predicate. + * @since 4.1.0 + */ + implicit def funToBasic(pred: Char => Boolean): CharPredicate = predicate.Basic(pred) + /** Constructs a predicate for the specific given character. + * @since 4.1.0 + */ + implicit def charToBasic(c: Char): CharPredicate = predicate.Basic(_ == c) + /** Constructs a predicate for anything in a range of specific characters. + * @since 4.1.0 + */ + implicit def rangeToBasic(cs: NumericRange[Char]): CharPredicate = predicate.Basic(cs.contains) + // $COVERAGE-ON$ + } + + /** Implicit conversions to make `Unicode` values. + * @since 4.1.0 + */ + object Unicode { + // $COVERAGE-OFF$ + /** Lifts a regular full-width character predicate. + * @since 4.1.0 + */ + implicit def funToUnicode(pred: Int => Boolean): CharPredicate = predicate.Unicode(pred) + /** Lifts a regular character predicate. + * @since 4.1.0 + */ + implicit def charFunToUnicode(pred: Char => Boolean): CharPredicate = predicate.Unicode(c => c.isValidChar && pred(c.toChar)) + /** Constructs a predicate for the specific given character. + * @since 4.1.0 + */ + implicit def charToUnicode(c: Char): CharPredicate = predicate.Unicode(_ == c.toInt) + /** Constructs a predicate for the specific given unicode codepoint. + * @since 4.1.0 + */ + implicit def intToUnicode(c: Int): CharPredicate = predicate.Unicode(_ == c) + /** Constructs a predicate for anything in a range of specific characters. + * @since 4.1.0 + */ + implicit def charRangeToUnicode(cs: NumericRange[Char]): CharPredicate = predicate.Unicode(cs.contains) + /** Constructs a predicate for anything in a range of specific unicode codepoints. + * @since 4.1.0 + */ + implicit def intRangeToUnicode(cs: NumericRange[Int]): CharPredicate = predicate.Unicode(cs.contains) + /** Constructs a predicate for anything in a range of specific unicode codepoints. + * @since 4.1.0 + */ + implicit def rangeToUnicode(cs: Range): CharPredicate = predicate.Unicode(cs.contains) + // $COVERAGE-ON$ + } } } diff --git a/parsley/shared/src/main/scala/parsley/token/symbol/ConcreteSymbol.scala b/parsley/shared/src/main/scala/parsley/token/symbol/ConcreteSymbol.scala index 6943dbde0..dd0225495 100644 --- a/parsley/shared/src/main/scala/parsley/token/symbol/ConcreteSymbol.scala +++ b/parsley/shared/src/main/scala/parsley/token/symbol/ConcreteSymbol.scala @@ -3,18 +3,16 @@ */ package parsley.token.symbol -// TODO: This can be enabled later, when finalised: js-native will need to not use this -//import scala.collection.concurrent - import parsley.Parsley, Parsley.{attempt, notFollowedBy, unit} import parsley.character.{char, string, strings} import parsley.errors.combinator.ErrorMethods import parsley.token.descriptions.{NameDesc, SymbolDesc} +import parsley.token.errors.ErrorConfig import parsley.token.predicate.Basic import parsley.internal.deepembedding.singletons -private [token] class ConcreteSymbol(nameDesc: NameDesc, symbolDesc: SymbolDesc) extends Symbol { +private [token] class ConcreteSymbol(nameDesc: NameDesc, symbolDesc: SymbolDesc, err: ErrorConfig) extends Symbol(err) { private lazy val identLetter = nameDesc.identifierLetter.toNative private lazy val opLetter = nameDesc.operatorLetter.toNative @@ -32,23 +30,32 @@ private [token] class ConcreteSymbol(nameDesc: NameDesc, symbolDesc: SymbolDesc) else name.foldLeft(unit)((p, c) => p <* caseChar(c)).label(name) } - //private val keywordMemo = concurrent.TrieMap.empty[String, Parsley[Unit]] - override def softKeyword(name: String): Parsley[Unit] = /*keywordMemo.getOrElseUpdate(name, */nameDesc.identifierLetter match { + // TODO: We might want to memoise this, but it must be done thread-safely: synchronising on the maps should be enough + override def softKeyword(name: String): Parsley[Unit] = nameDesc.identifierLetter match { // TODO: this needs optimising for Unicode - case Basic(letter) => new Parsley(new singletons.Specific("keyword", name, letter, symbolDesc.caseSensitive)) - case _ => attempt(caseString(name).label(name) *> notFollowedBy(identLetter).label(s"end of $name")) - }//) + case Basic(letter) => + new Parsley(new singletons.Specific(name, err.labelSymbolKeyword(name), err.labelSymbolEndOfKeyword(name), letter, symbolDesc.caseSensitive)) + case _ => attempt { + err.labelSymbolKeyword(name)(caseString(name)) *> + notFollowedBy(identLetter).label(err.labelSymbolEndOfKeyword(name)) + } + } - //private val operatorMemo = concurrent.TrieMap.empty[String, Parsley[Unit]] - override def softOperator(name: String): Parsley[Unit] = /*operatorMemo.getOrElseUpdate(name, *//*nameDesc.operatorLetter match*/ { + override def softOperator(name: String): Parsley[Unit] = /*nameDesc.operatorLetter match*/ { //case _ => val ends = symbolDesc.hardOperators.collect { case op if op.startsWith(name) && op != name => op.substring(name.length) }.toList ends match { - case Nil => attempt(string(name).label(name) *> notFollowedBy(opLetter).label(s"end of $name")) - case end::ends => attempt(string(name).label(name) *> notFollowedBy(opLetter <|> strings(end, ends: _*)).label(s"end of $name")) + case Nil => attempt { + err.labelSymbolOperator(name)(string(name)) *> + notFollowedBy(opLetter).label(err.labelSymbolEndOfOperator(name)) + } + case end::ends => attempt { + err.labelSymbolOperator(name)(string(name)) *> + notFollowedBy(opLetter <|> strings(end, ends: _*)).label(err.labelSymbolEndOfOperator(name)) + } } - }//) + } } diff --git a/parsley/shared/src/main/scala/parsley/token/symbol/LexemeSymbol.scala b/parsley/shared/src/main/scala/parsley/token/symbol/LexemeSymbol.scala index 07ef1c6f8..dc6766ff2 100644 --- a/parsley/shared/src/main/scala/parsley/token/symbol/LexemeSymbol.scala +++ b/parsley/shared/src/main/scala/parsley/token/symbol/LexemeSymbol.scala @@ -5,8 +5,9 @@ package parsley.token.symbol import parsley.Parsley import parsley.token.Lexeme +import parsley.token.errors.ErrorConfig -private [token] class LexemeSymbol(symbol: Symbol, lexeme: Lexeme) extends Symbol { +private [token] class LexemeSymbol(symbol: Symbol, lexeme: Lexeme, err: ErrorConfig) extends Symbol(err) { override def apply(name: String): Parsley[Unit] = lexeme(symbol.apply(name)) override def apply(name: Char): Parsley[Unit] = lexeme(symbol.apply(name)) override def softKeyword(name: String): Parsley[Unit] = lexeme(symbol.softKeyword(name)) diff --git a/parsley/shared/src/main/scala/parsley/token/symbol/Symbol.scala b/parsley/shared/src/main/scala/parsley/token/symbol/Symbol.scala index 7459bea7b..dddc975b7 100644 --- a/parsley/shared/src/main/scala/parsley/token/symbol/Symbol.scala +++ b/parsley/shared/src/main/scala/parsley/token/symbol/Symbol.scala @@ -5,6 +5,7 @@ package parsley.token.symbol import parsley.Parsley import parsley.errors.combinator.ErrorMethods +import parsley.token.errors.{ErrorConfig, LabelConfig} /** This class provides implicit functionality to promote string * literals into tokens. @@ -54,7 +55,7 @@ abstract class ImplicitSymbol private [symbol] { * * @since 4.0.0 */ -abstract class Symbol private[token] { +abstract class Symbol private[symbol] (err: ErrorConfig) { /** $stringApply * * Additionally applies the given label as the name of the symbol. @@ -133,66 +134,67 @@ abstract class Symbol private[token] { // $COVERAGE-OFF$ // These really don't need testing + private final def apply(name: Char, label: LabelConfig): Parsley[Unit] = label(apply(name)) /** This parser parses a semicolon `;` as a symbol. * * @since 4.0.0 */ - final lazy val semi: Parsley[Unit] = apply(';', "semicolon") + final lazy val semi: Parsley[Unit] = apply(';', err.labelSymbolSemi) /** This parser parses a comma `,` as a symbol. * * @since 4.0.0 */ - final lazy val comma: Parsley[Unit] = apply(',', "comma") + final lazy val comma: Parsley[Unit] = apply(',', err.labelSymbolComma) /** This parser parses a colon `:` as a symbol. * * @since 4.0.0 */ - final lazy val colon: Parsley[Unit] = apply(':', "colon") + final lazy val colon: Parsley[Unit] = apply(':', err.labelSymbolColon) /** This parser parses a dot `.` as a symbol. * * @since 4.0.0 */ - final lazy val dot: Parsley[Unit] = apply('.', "dot") + final lazy val dot: Parsley[Unit] = apply('.', err.labelSymbolDot) /** This parser parses an open parenthesis `(` as a symbol. * * @since 4.0.0 */ - final lazy val openParen: Parsley[Unit] = apply('(', "open parenthesis") + final lazy val openParen: Parsley[Unit] = apply('(', err.labelSymbolOpenParen) /** This parser parses an open brace `{` as a symbol. * * @since 4.0.0 */ - final lazy val openBrace: Parsley[Unit] = apply('{', "open brace") + final lazy val openBrace: Parsley[Unit] = apply('{', err.labelSymbolOpenBrace) /** This parser parses an open square bracket `[` as a symbol. * * @since 4.0.0 */ - final lazy val openSquare: Parsley[Unit] = apply('[', "open square bracket") + final lazy val openSquare: Parsley[Unit] = apply('[', err.labelSymbolOpenSquare) /** This parser parses an open angle bracket `<` as a symbol. * * @since 4.0.0 */ - final lazy val openAngle: Parsley[Unit] = apply('<', "open angle bracket") + final lazy val openAngle: Parsley[Unit] = apply('<', err.labelSymbolOpenAngle) /** This parser parses a closing parenthesis `)` as a symbol. * * @since 4.0.0 */ - final lazy val closingParen: Parsley[Unit] = apply(')', "closing parenthesis") + final lazy val closingParen: Parsley[Unit] = apply(')', err.labelSymbolClosingParen) /** This parser parses a closing brace `}` as a symbol. * * @since 4.0.0 */ - final lazy val closingBrace: Parsley[Unit] = apply('}', "closing brace") + final lazy val closingBrace: Parsley[Unit] = apply('}', err.labelSymbolClosingBrace) /** This parser parses a closing square bracket `]` as a symbol. * * @since 4.0.0 */ - final lazy val closingSquare: Parsley[Unit] = apply(']', "closing square bracket") + final lazy val closingSquare: Parsley[Unit] = apply(']', err.labelSymbolClosingSquare) /** This parser parses a closing square bracket `>` as a symbol. * * @since 4.0.0 */ - final lazy val closingAngle: Parsley[Unit] = apply('>', "closing angle bracket") + final lazy val closingAngle: Parsley[Unit] = apply('>', err.labelSymbolClosingAngle) // $COVERAGE-ON$ } diff --git a/parsley/shared/src/main/scala/parsley/token/text/Character.scala b/parsley/shared/src/main/scala/parsley/token/text/Character.scala index 64217bcea..3e1b5e018 100644 --- a/parsley/shared/src/main/scala/parsley/token/text/Character.scala +++ b/parsley/shared/src/main/scala/parsley/token/text/Character.scala @@ -18,7 +18,7 @@ import parsley.token.predicate.{Basic, CharPredicate, NotRequired, Unicode} * `Lexer`, which will depend on user-defined configuration. Please see the * relevant documentation of these specific objects. */ -abstract class Character private[token] { +abstract class Character private[text] { /** This parser will parse a single character literal, which may contain * any unicode graphic character as defined by up to two UTF-16 codepoints. * It may also contain escape sequences. diff --git a/parsley/shared/src/main/scala/parsley/token/text/ConcreteCharacter.scala b/parsley/shared/src/main/scala/parsley/token/text/ConcreteCharacter.scala index 475718771..0b5aea2ca 100644 --- a/parsley/shared/src/main/scala/parsley/token/text/ConcreteCharacter.scala +++ b/parsley/shared/src/main/scala/parsley/token/text/ConcreteCharacter.scala @@ -4,27 +4,32 @@ package parsley.token.text import parsley.Parsley -import parsley.errors.combinator.ErrorMethods -import parsley.implicits.character.charLift +import parsley.character.char import parsley.token.descriptions.text.TextDesc +import parsley.token.errors.{ErrorConfig, FilterConfig, LabelConfig, LabelWithExplainConfig} -private [token] final class ConcreteCharacter(desc: TextDesc, escapes: Escape) extends Character { - private val quote = desc.characterLiteralEnd - private lazy val charLetter = Character.letter(quote, desc.escapeSequences.escBegin, allowsAllSpace = false, desc.graphicCharacter) +private [token] final class ConcreteCharacter(desc: TextDesc, escapes: Escape, err: ErrorConfig) extends Character { + private val quote = char(desc.characterLiteralEnd) + private lazy val graphic = Character.letter(desc.characterLiteralEnd, desc.escapeSequences.escBegin, allowsAllSpace = false, desc.graphicCharacter) - override lazy val fullUtf16: Parsley[Int] = { - quote *> (escapes.escapeChar <|> charLetter.toUnicode) <* quote + private def charLetter(graphicLetter: Parsley[Int]) = { + escapes.escapeChar <|> err.labelGraphicCharacter(graphicLetter) <|> err.verifiedCharBadCharsUsedInLiteral.checkBadChar } + private def charLiteral[A](letter: Parsley[A], end: LabelConfig) = quote *> letter <* end(quote) - override lazy val basicMultilingualPlane: Parsley[Char] = quote *> (escapes.escapeChar.collectMsg("non-BMP character") { - case n if Character.isBmpCodePoint(n) => n.toChar - } <|> charLetter.toBmp) <* quote + override lazy val fullUtf16: Parsley[Int] = err.labelCharUtf16(charLiteral(charLetter(graphic.toUnicode), err.labelCharUtf16End)) + // this is a bit inefficient, converting to int and then back to char, but it makes it consistent, and can be optimised anyway + private lazy val uncheckedBmpLetter = charLetter(graphic.toBmp.map(_.toInt)) - // FIXME: These are going to be a dodgy because of the double check here, may reference BMP - override lazy val ascii: Parsley[Char] = basicMultilingualPlane.filterOut { - case n if n > Character.MaxAscii => "non-ascii character" - } - override lazy val latin1: Parsley[Char] = basicMultilingualPlane.filterOut { - case n if n > Character.MaxLatin1 => "non-ascii character (extended)" + private def constrainedBmp(illegal: Int => Boolean, label: LabelWithExplainConfig, endLabel: LabelConfig, bad: FilterConfig[Int]) = { + label(charLiteral(bad.collect(uncheckedBmpLetter) { case x if !illegal(x) => x.toChar }, endLabel)) } + + override lazy val basicMultilingualPlane: Parsley[Char] = + constrainedBmp(!Character.isBmpCodePoint(_), err.labelCharBasicMultilingualPlane, err.labelCharBasicMultilingualPlaneEnd, + err.filterCharNonBasicMultilingualPlane) + override lazy val ascii: Parsley[Char] = + constrainedBmp(_ > Character.MaxAscii, err.labelCharAscii, err.labelCharAsciiEnd, err.filterCharNonAscii) + override lazy val latin1: Parsley[Char] = + constrainedBmp(_ > Character.MaxLatin1, err.labelCharLatin1, err.labelCharLatin1End, err.filterCharNonLatin1) } diff --git a/parsley/shared/src/main/scala/parsley/token/text/ConcreteString.scala b/parsley/shared/src/main/scala/parsley/token/text/ConcreteString.scala index f16af7b59..b883927a4 100644 --- a/parsley/shared/src/main/scala/parsley/token/text/ConcreteString.scala +++ b/parsley/shared/src/main/scala/parsley/token/text/ConcreteString.scala @@ -5,32 +5,42 @@ package parsley.token.text import scala.Predef.{String => ScalaString, _} -import parsley.Parsley, Parsley.{fresh, pure} +import parsley.Parsley, Parsley.{attempt, fresh, pure} +import parsley.character.{char, string} import parsley.combinator.{choice, skipManyUntil} -import parsley.implicits.character.{charLift, stringLift} import parsley.implicits.zipped.Zipped2 +import parsley.token.errors.{ErrorConfig, LabelConfig, LabelWithExplainConfig} import parsley.token.predicate.CharPredicate -private [token] final class ConcreteString(ends: Set[ScalaString], stringChar: StringCharacter, isGraphic: CharPredicate, allowsAllSpace: Boolean) - extends String { - override lazy val fullUtf16: Parsley[ScalaString] = choice(ends.view.map(makeStringParser).toSeq: _*) *> sbReg.gets(_.toString) - override lazy val ascii: Parsley[ScalaString] = String.ensureAscii(fullUtf16) - override lazy val latin1: Parsley[ScalaString] = String.ensureExtendedAscii(fullUtf16) +private [token] final class ConcreteString(ends: Set[ScalaString], stringChar: StringCharacter, isGraphic: CharPredicate, + allowsAllSpace: Boolean, err: ErrorConfig) extends String { + + private def stringLiteral(valid: Parsley[StringBuilder] => Parsley[StringBuilder], + openLabel: (Boolean, Boolean) => LabelWithExplainConfig, closeLabel: (Boolean, Boolean) => LabelConfig) = { + choice(ends.view.map(makeStringParser(valid, openLabel, closeLabel)).toSeq: _*) *> sbReg.gets(_.toString) + } + override lazy val fullUtf16: Parsley[ScalaString] = stringLiteral(identity, err.labelStringUtf16, err.labelStringUtf16End) + override lazy val ascii: Parsley[ScalaString] = stringLiteral(String.ensureAscii(err), err.labelStringAscii, err.labelStringAsciiEnd) + override lazy val latin1: Parsley[ScalaString] = stringLiteral(String.ensureExtendedAscii(err), err.labelStringLatin1, err.labelStringLatin1End) private val sbReg = parsley.registers.Reg.make[StringBuilder] - private def makeStringParser(terminal: ScalaString): Parsley[_] = { - val terminalInit = terminal.charAt(0) + private def makeStringParser(valid: Parsley[StringBuilder] => Parsley[StringBuilder], + openLabel: (Boolean, Boolean) => LabelWithExplainConfig, closeLabel: (Boolean, Boolean) => LabelConfig) + (terminalStr: ScalaString) = { + val terminalInit = terminalStr.charAt(0) val strChar = stringChar(Character.letter(terminalInit, allowsAllSpace, isGraphic)) val pf = (sb: StringBuilder, cpo: Option[Int]) => { for (cp <- cpo) parsley.character.addCodepoint(sb, cp) sb } - val content = parsley.expr.infix.secretLeft1((sbReg.get, strChar).zipped(pf), strChar, pure(pf)) + val content = valid(parsley.expr.infix.secretLeft1((sbReg.get, strChar).zipped(pf), strChar, pure(pf))) + val terminal = string(terminalStr) // terminal should be first, to allow for a jump table on the main choice - terminal *> + openLabel(allowsAllSpace, stringChar.isRaw)(terminal) *> // then only one string builder needs allocation sbReg.put(fresh(new StringBuilder)) *> - skipManyUntil(sbReg.modify(terminalInit #> ((sb: StringBuilder) => sb += terminalInit)) <|> content, terminal) + skipManyUntil(sbReg.modify(char(terminalInit) #> ((sb: StringBuilder) => sb += terminalInit)) <|> content, + closeLabel(allowsAllSpace, stringChar.isRaw)(attempt(terminal))) //is the attempt needed here? not sure } } diff --git a/parsley/shared/src/main/scala/parsley/token/text/Escape.scala b/parsley/shared/src/main/scala/parsley/token/text/Escape.scala index 48f976304..6e3c84bfe 100644 --- a/parsley/shared/src/main/scala/parsley/token/text/Escape.scala +++ b/parsley/shared/src/main/scala/parsley/token/text/Escape.scala @@ -3,37 +3,37 @@ */ package parsley.token.text -import parsley.Parsley, Parsley.{attempt, empty, pure, unit} +import parsley.Parsley, Parsley.{attempt, empty, pure} import parsley.character.{bit, char, digit, hexDigit, octDigit, strings} import parsley.combinator.ensure -import parsley.errors.combinator.{amend, entrench, ErrorMethods} import parsley.implicits.zipped.Zipped3 import parsley.token.descriptions.text.{EscapeDesc, NumberOfDigits, NumericEscape} +import parsley.token.errors.{ErrorConfig, NotConfigured} import parsley.token.numeric -private [token] class Escape(desc: EscapeDesc) { +private [token] class Escape(desc: EscapeDesc, err: ErrorConfig, generic: numeric.Generic) { // NOTE: `strings`, while nice, is not perfect as it doesn't leverage a trie-based folding // on the possibilities. We'll want trie-based folding here, or at least a specialised // instruction that has the trie lookup logic baked in. // We do need to backtrack out of this if things go wrong, it's possible another escape sequence might share a lead private val escMapped = { - val (x::xs) = desc.escMap.view.map { + desc.escMap.view.map { case (e, c) => e -> pure(c) - }.toList - attempt(strings(x, xs: _*)) + }.toList match { + case Nil => empty + case x::xs => attempt(strings(x, xs: _*)) + } } - private def boundedChar(p: Parsley[BigInt], maxValue: Int, prefix: Option[Char], radix: Int): Parsley[Int] = - prefix.fold(unit)(c => char(c).void) *> amend { - val prefixString = prefix.fold("")(c => s"$c") - entrench(p).collectMsg(n => Seq( - if (n > maxValue) { - s"\\$prefixString${n.toString(radix)} is greater than the maximum character of \\$prefixString${BigInt(maxValue).toString(radix)}" - } - else s"illegal unicode codepoint: \\$prefixString${n.toString(radix)}")) { - case n if n <= maxValue && Character.isValidCodePoint(n.toInt) => n.toInt - } + private def boundedChar(p: Parsley[BigInt], maxValue: Int, prefix: Option[Char], radix: Int) = err.labelEscapeNumeric(radix) { + val numericTail = err.filterEscapeCharNumericSequenceIllegal(maxValue, radix).collect(p) { + case n if n <= maxValue && Character.isValidCodePoint(n.toInt) => n.toInt + } + prefix match { + case None => numericTail + case Some(c) => char(c) *> err.labelEscapeNumericEnd(c, radix)(numericTail) } + } // this is a really neat trick :) private lazy val atMostReg = parsley.registers.Reg.make[Int] @@ -42,18 +42,17 @@ private [token] class Escape(desc: EscapeDesc) { digit <* atMostReg.modify(_ - 1)).foldLeft1[BigInt](0)((n, d) => n * radix + d.asDigit) } - private def exactly(n: Int, full: Int, radix: Int, digit: Parsley[Char]): Parsley[BigInt] = { - atMost(n, radix, digit) <* atMostReg.get.guardAgainst { - case x if x > 0 => Seq(s"literal required $full digits, but only got ${full-x}") - } + private def exactly(n: Int, full: Int, radix: Int, digit: Parsley[Char], reqDigits: Seq[Int]): Parsley[BigInt] = { + atMost(n, radix, digit) <* err.filterEscapeCharRequiresExactDigits(radix, reqDigits).filter(atMostReg.gets(full - _))(_ == full) } private lazy val digitsParsed = parsley.registers.Reg.make[Int] private def oneOfExactly(n: Int, ns: List[Int], radix: Int, digit: Parsley[Char]): Parsley[BigInt] = { + val reqDigits@(m :: ms) = (n :: ns).sorted // make this a precondition of the description? def go(digits: Int, m: Int, ns: List[Int]): Parsley[BigInt] = ns match { - case Nil => exactly(digits, m, radix, digit) <* digitsParsed.put(digits) + case Nil => exactly(digits, m, radix, digit, reqDigits) <* digitsParsed.put(digits) case n :: ns => - val theseDigits = exactly(digits, m, radix, digit) + val theseDigits = exactly(digits, m, radix, digit, reqDigits) val restDigits = ( (attempt(go(n-m, n, ns).map(Some(_)) <* digitsParsed.modify(_ + digits))) <|> (digitsParsed.put(digits) #> None) @@ -63,7 +62,6 @@ private [token] class Escape(desc: EscapeDesc) { case (x, Some(y), exp) => (x * BigInt(radix).pow(exp - digits) + y) // digits is removed here, because it's been added before the get } } - val (m :: ms) = (n :: ns).sorted // make this a precondition of the description? go(m, m, ms) } @@ -76,11 +74,12 @@ private [token] class Escape(desc: EscapeDesc) { } } - private val decimalEscape = fromDesc(radix = 10, desc.decimalEscape, numeric.Generic.zeroAllowedDecimal, digit) - private val hexadecimalEscape = fromDesc(radix = 16, desc.hexadecimalEscape, numeric.Generic.zeroAllowedHexadecimal, hexDigit) - private val octalEscape = fromDesc(radix = 8, desc.octalEscape, numeric.Generic.zeroAllowedOctal, octDigit) - private val binaryEscape = fromDesc(radix = 2, desc.binaryEscape, numeric.Generic.zeroAllowedBinary, bit) + private val decimalEscape = fromDesc(radix = 10, desc.decimalEscape, generic.zeroAllowedDecimal(NotConfigured), digit) + private val hexadecimalEscape = fromDesc(radix = 16, desc.hexadecimalEscape, generic.zeroAllowedHexadecimal(NotConfigured), hexDigit) + private val octalEscape = fromDesc(radix = 8, desc.octalEscape, generic.zeroAllowedOctal(NotConfigured), octDigit) + private val binaryEscape = fromDesc(radix = 2, desc.binaryEscape, generic.zeroAllowedBinary(NotConfigured), bit) private val numericEscape = decimalEscape <|> hexadecimalEscape <|> octalEscape <|> binaryEscape - val escapeCode = (escMapped <|> numericEscape).label("end of escape sequence") - val escapeChar = char(desc.escBegin) *> escapeCode + val escapeCode = err.labelEscapeEnd(escMapped <|> numericEscape) + val escapeBegin = err.labelEscapeSequence(char(desc.escBegin)) + val escapeChar = escapeBegin *> escapeCode } diff --git a/parsley/shared/src/main/scala/parsley/token/text/String.scala b/parsley/shared/src/main/scala/parsley/token/text/String.scala index 8ffe59242..cd4dbf67d 100644 --- a/parsley/shared/src/main/scala/parsley/token/text/String.scala +++ b/parsley/shared/src/main/scala/parsley/token/text/String.scala @@ -3,11 +3,10 @@ */ package parsley.token.text -import scala.Predef.{String => ScalaString, _} +import scala.Predef.{String => ScalaString} import parsley.Parsley -import parsley.XCompat -import parsley.errors.combinator.ErrorMethods +import parsley.token.errors.ErrorConfig /** This class defines a uniform interface for defining parsers for string * literals, independent of whether the string is raw, multi-line, or should @@ -22,7 +21,7 @@ import parsley.errors.combinator.ErrorMethods * `Lexer`, which will depend on user-defined configuration. Please see the * relevant documentation of these specific objects. */ -abstract class String private[token] { +abstract class String private[text] { /** This parser will parse a single string literal, which may contain any * number of graphical UTF-16 unicode characters; including those that span multiple * 32-bit codepoints. It may contain escape sequences, and potentially @@ -87,15 +86,11 @@ abstract class String private[token] { } private [text] object String { - private def allCharsWithin(str: ScalaString, bound: Int) = XCompat.codePoints(str).forall(_ <= bound) - def isAscii(str: ScalaString): Boolean = allCharsWithin(str, Character.MaxAscii) - def isExtendedAscii(str: ScalaString): Boolean = allCharsWithin(str, Character.MaxLatin1) + // don't need to use code points, high-surrogates are already out of range + private def allCharsWithin(str: StringBuilder, bound: Int) = str.forall(_ <= bound) + private def isAscii(str: StringBuilder): Boolean = allCharsWithin(str, Character.MaxAscii) + private def isExtendedAscii(str: StringBuilder): Boolean = allCharsWithin(str, Character.MaxLatin1) - def ensureAscii(p: Parsley[ScalaString]): Parsley[ScalaString] = p.guardAgainst { - case str if !isAscii(str) => Seq("non-ascii characters in string literal, this is not allowed") - } - - def ensureExtendedAscii(p: Parsley[ScalaString]): Parsley[ScalaString] = p.guardAgainst { - case str if !isExtendedAscii(str) => Seq("non-extended-ascii characters in string literal, this is not allowed") - } + def ensureAscii(err: ErrorConfig)(p: Parsley[StringBuilder]): Parsley[StringBuilder] = err.filterStringNonAscii.filter(p)(isAscii(_)) + def ensureExtendedAscii(err: ErrorConfig)(p: Parsley[StringBuilder]): Parsley[StringBuilder] = err.filterStringNonLatin1.filter(p)(isExtendedAscii(_)) } diff --git a/parsley/shared/src/main/scala/parsley/token/text/StringCharacter.scala b/parsley/shared/src/main/scala/parsley/token/text/StringCharacter.scala index e7c106877..c371835d0 100644 --- a/parsley/shared/src/main/scala/parsley/token/text/StringCharacter.scala +++ b/parsley/shared/src/main/scala/parsley/token/text/StringCharacter.scala @@ -6,40 +6,50 @@ package parsley.token.text import parsley.Parsley, Parsley.empty import parsley.character.{char, satisfy, satisfyUtf16} import parsley.combinator.skipSome -import parsley.errors.combinator.ErrorMethods import parsley.implicits.character.charLift import parsley.token.descriptions.text.EscapeDesc +import parsley.token.errors.ErrorConfig import parsley.token.predicate.{Basic, CharPredicate, NotRequired, Unicode} private [token] abstract class StringCharacter { def apply(isLetter: CharPredicate): Parsley[Option[Int]] + def isRaw: Boolean + + protected def _checkBadChar(err: ErrorConfig) = err.verifiedStringBadCharsUsedInLiteral.checkBadChar } -private [token] object RawCharacter extends StringCharacter { +private [token] class RawCharacter(err: ErrorConfig) extends StringCharacter { + override def isRaw: Boolean = true override def apply(isLetter: CharPredicate): Parsley[Option[Int]] = isLetter match { - case Basic(isLetter) => satisfy(isLetter).map(c => Some(c.toInt)).label("string character") - case Unicode(isLetter) => satisfyUtf16(isLetter).map(Some(_)).label("string character") + case Basic(isLetter) => err.labelStringCharacter(satisfy(isLetter).map(c => Some(c.toInt))) <|> _checkBadChar(err) + case Unicode(isLetter) => err.labelStringCharacter(satisfyUtf16(isLetter).map(Some(_))) <|> _checkBadChar(err) case NotRequired => empty } } -private [token] class EscapableCharacter(desc: EscapeDesc, escapes: Escape, space: Parsley[_]) extends StringCharacter { - private lazy val escapeEmpty = desc.emptyEscape.fold[Parsley[Char]](empty)(char) +private [token] class EscapableCharacter(desc: EscapeDesc, escapes: Escape, space: Parsley[_], err: ErrorConfig) extends StringCharacter { + override def isRaw: Boolean = false + private lazy val escapeEmpty = err.labelStringEscapeEmpty(desc.emptyEscape.fold[Parsley[Char]](empty)(char)) private lazy val escapeGap = { - if (desc.gapsSupported) skipSome(space.label("string gap")) *> desc.escBegin.label("end of string gap") + if (desc.gapsSupported) skipSome(err.labelStringEscapeGap(space)) *> err.labelStringEscapeGapEnd(desc.escBegin) else empty } - private lazy val stringEscape: Parsley[Option[Int]] = { - desc.escBegin *> (escapeGap #> None - <|> escapeEmpty #> None - <|> escapes.escapeCode.map(Some(_)).explain("invalid escape sequence")) - }.label("escape sequence") + private lazy val stringEscape: Parsley[Option[Int]] = + escapes.escapeBegin *> (escapeGap #> None + <|> escapeEmpty #> None + <|> escapes.escapeCode.map(Some(_))) - override def apply(isLetter: CharPredicate): Parsley[Option[Int]] = isLetter match { - case Basic(isLetter) => - (satisfy(c => isLetter(c) && c != desc.escBegin).map(c => Some(c.toInt)).label("graphic character") <|> stringEscape).label("string character") - case Unicode(isLetter) => - (satisfyUtf16(c => isLetter(c) && c != desc.escBegin.toInt).map(Some(_)).label("graphic character") <|> stringEscape).label("string character") - case NotRequired => stringEscape + override def apply(isLetter: CharPredicate): Parsley[Option[Int]] = { + isLetter match { + case Basic(isLetter) => err.labelStringCharacter( + stringEscape <|> err.labelGraphicCharacter(satisfy(c => isLetter(c) && c != desc.escBegin).map(c => Some(c.toInt))) + <|> _checkBadChar(err) + ) + case Unicode(isLetter) => err.labelStringCharacter( + stringEscape <|> err.labelGraphicCharacter(satisfyUtf16(c => isLetter(c) && c != desc.escBegin.toInt).map(Some(_))) + <|> _checkBadChar(err) + ) + case NotRequired => stringEscape + } } } diff --git a/parsley/shared/src/test/scala/parsley/internal/InternalTests.scala b/parsley/shared/src/test/scala/parsley/internal/InternalTests.scala index ef960c2a6..741c8dcf3 100644 --- a/parsley/shared/src/test/scala/parsley/internal/InternalTests.scala +++ b/parsley/shared/src/test/scala/parsley/internal/InternalTests.scala @@ -37,15 +37,6 @@ class InternalTests extends ParsleyTest { q.parse("a123b123c") should be (Success('3')) } - // TODO: While this test works, exposing calls inner instructions is super dodgy. - // This test was broken when calls were correctly factored out! - /*they should "function properly when a recursion boundary is inside" in { - lazy val q: Parsley[Unit] = (p *> p) <|> unit - lazy val p: Parsley[Unit] = '(' *> q <* ')' - q.internal.instrs(0).asInstanceOf[instructions.Call].instrs.count(_ == instructions.Return) shouldBe 1 - q.parse("(()())()") shouldBe a [Success[_]] - }*/ - they should "work in the precedence parser with one op" in { val atom = some(digit).map(_.mkString.toInt) val expr = precedence[Int](atom)( diff --git a/parsley/shared/src/test/scala/parsley/token/TokeniserTests.scala b/parsley/shared/src/test/scala/parsley/token/TokeniserTests.scala index 54e3d430b..286f4251e 100644 --- a/parsley/shared/src/test/scala/parsley/token/TokeniserTests.scala +++ b/parsley/shared/src/test/scala/parsley/token/TokeniserTests.scala @@ -10,14 +10,15 @@ import parsley.character.string import parsley.combinator.eof import token.{descriptions => desc} +import token.predicate.implicits.Basic._ class TokeniserTests extends ParsleyTest { val scala = desc.LexicalDesc( - desc.NameDesc(identifierStart = token.predicate._CharSet(('a' to 'z').toSet ++ ('A' to 'Z').toSet + '_'), - identifierLetter = token.predicate._CharSet(('a' to 'z').toSet ++ ('A' to 'Z').toSet ++ ('0' to '9').toSet + '_'), - operatorStart = token.predicate._CharSet('+', '-', ':', '/', '*', '='), - operatorLetter = token.predicate._CharSet('+', '-', '/', '*')), + desc.NameDesc(identifierStart = ('a' to 'z').toSet ++ ('A' to 'Z').toSet + '_', + identifierLetter = ('a' to 'z').toSet ++ ('A' to 'Z').toSet ++ ('0' to '9').toSet + '_', + operatorStart = Set('+', '-', ':', '/', '*', '='), + operatorLetter = Set('+', '-', '/', '*')), desc.SymbolDesc(hardKeywords = Set("if", "else", "for", "yield", "while", "def", "class", "trait", "abstract", "override", "val", "var", "lazy"), hardOperators = Set(":", "=", "::", ":="), diff --git a/parsley/shared/src/test/scala/parsley/token/names/NamesTests.scala b/parsley/shared/src/test/scala/parsley/token/names/NamesTests.scala index b1b43c576..80f0c4a06 100644 --- a/parsley/shared/src/test/scala/parsley/token/names/NamesTests.scala +++ b/parsley/shared/src/test/scala/parsley/token/names/NamesTests.scala @@ -7,6 +7,7 @@ import Predef.{ArrowAssoc => _, _} import parsley.{ParsleyTest, Failure} import parsley.token.LexemeImpl._ +import parsley.token.errors.ErrorConfig import parsley.token.descriptions._ import parsley.token.predicate._ @@ -16,7 +17,8 @@ import parsley.{TestError, VanillaError, Named} import org.scalactic.source.Position class NamesTests extends ParsleyTest { - def makeSymbol(nameDesc: NameDesc, symDesc: SymbolDesc): Names = new LexemeNames(new ConcreteNames(nameDesc, symDesc), spaces) + val errConfig = new ErrorConfig + def makeSymbol(nameDesc: NameDesc, symDesc: SymbolDesc): Names = new LexemeNames(new ConcreteNames(nameDesc, symDesc, errConfig), spaces) val plainName = NameDesc.plain.copy(identifierLetter = Basic(_.isLetterOrDigit), identifierStart = Basic(_.isLetter)) val plainSym = SymbolDesc.plain.copy(hardKeywords = Set("keyword", "HARD"), hardOperators = Set("+", "<", "<=")) diff --git a/parsley/shared/src/test/scala/parsley/token/numeric/RealTests.scala b/parsley/shared/src/test/scala/parsley/token/numeric/RealTests.scala index b0ec2c473..11767196c 100644 --- a/parsley/shared/src/test/scala/parsley/token/numeric/RealTests.scala +++ b/parsley/shared/src/test/scala/parsley/token/numeric/RealTests.scala @@ -8,10 +8,13 @@ import Predef.{ArrowAssoc => _, _} import parsley.ParsleyTest import parsley.token.LexemeImpl import parsley.token.descriptions.numeric._, ExponentDesc.NoExponents +import parsley.token.errors.ErrorConfig import org.scalactic.source.Position class RealTests extends ParsleyTest { - private def makeReal(desc: NumericDesc) = new LexemeReal(new SignedReal(desc, new UnsignedReal(desc, new UnsignedInteger(desc))), LexemeImpl.empty) + val errConfig = new ErrorConfig + val generic = new Generic(errConfig) + private def makeReal(desc: NumericDesc) = new LexemeReal(new SignedReal(desc, new UnsignedReal(desc, new UnsignedInteger(desc, errConfig, generic), errConfig, generic), errConfig), LexemeImpl.empty, errConfig) val plain = NumericDesc.plain.copy(decimalExponentDesc = NoExponents, hexadecimalExponentDesc = NoExponents, octalExponentDesc = NoExponents, binaryExponentDesc = NoExponents) diff --git a/parsley/shared/src/test/scala/parsley/token/numeric/SignedIntegerTests.scala b/parsley/shared/src/test/scala/parsley/token/numeric/SignedIntegerTests.scala index 452e6abeb..8db33fc03 100644 --- a/parsley/shared/src/test/scala/parsley/token/numeric/SignedIntegerTests.scala +++ b/parsley/shared/src/test/scala/parsley/token/numeric/SignedIntegerTests.scala @@ -8,10 +8,13 @@ import Predef.{ArrowAssoc => _, _} import parsley.ParsleyTest import parsley.token.LexemeImpl import parsley.token.descriptions.numeric._ +import parsley.token.errors.ErrorConfig import org.scalactic.source.Position class SignedIntegerTests extends ParsleyTest { - private def makeInteger(desc: NumericDesc) = new LexemeInteger(new SignedInteger(desc, new UnsignedInteger(desc)), LexemeImpl.empty) + val errConfig = new ErrorConfig + val generic = new Generic(errConfig) + private def makeInteger(desc: NumericDesc) = new LexemeInteger(new SignedInteger(desc, new UnsignedInteger(desc, errConfig, generic), errConfig), LexemeImpl.empty) val plain = NumericDesc.plain.copy(integerNumbersCanBeBinary = true, literalBreakChar = BreakCharDesc.Supported('_', false)) val optionalPlus = makeInteger(plain.copy(positiveSign = PlusSignPresence.Optional)) diff --git a/parsley/shared/src/test/scala/parsley/token/numeric/UnsignedIntegerTests.scala b/parsley/shared/src/test/scala/parsley/token/numeric/UnsignedIntegerTests.scala index f47ca67b9..bd2a4454b 100644 --- a/parsley/shared/src/test/scala/parsley/token/numeric/UnsignedIntegerTests.scala +++ b/parsley/shared/src/test/scala/parsley/token/numeric/UnsignedIntegerTests.scala @@ -8,10 +8,13 @@ import Predef.{ArrowAssoc => _, _} import parsley.ParsleyTest import parsley.token.LexemeImpl import parsley.token.descriptions.numeric._ +import parsley.token.errors.ErrorConfig import org.scalactic.source.Position class UnsignedIntegerTests extends ParsleyTest { - private def makeInteger(desc: NumericDesc) = new LexemeInteger(new UnsignedInteger(desc), LexemeImpl.empty) + val errConfig = new ErrorConfig + val generic = new Generic(errConfig) + private def makeInteger(desc: NumericDesc) = new LexemeInteger(new UnsignedInteger(desc, errConfig, generic), LexemeImpl.empty) val plain = NumericDesc.plain val withLeadingZero = makeInteger(plain) diff --git a/parsley/shared/src/test/scala/parsley/token/symbol/SymbolTests.scala b/parsley/shared/src/test/scala/parsley/token/symbol/SymbolTests.scala index 9805f17cc..d33411a4f 100644 --- a/parsley/shared/src/test/scala/parsley/token/symbol/SymbolTests.scala +++ b/parsley/shared/src/test/scala/parsley/token/symbol/SymbolTests.scala @@ -9,21 +9,23 @@ import parsley.{Parsley, ParsleyTest} import parsley.token.LexemeImpl._ import parsley.token.descriptions._ -import parsley.token.predicate._ +import parsley.token.errors.ErrorConfig +import parsley.token.predicate._, implicits.Basic.charToBasic, implicits.Unicode.funToUnicode import parsley.token.symbol._ import parsley.character.{spaces, string} import org.scalactic.source.Position class SymbolTests extends ParsleyTest { - def makeSymbol(nameDesc: NameDesc, symDesc: SymbolDesc): Symbol = new LexemeSymbol(new ConcreteSymbol(nameDesc, symDesc), spaces) + val errConfig = new ErrorConfig + def makeSymbol(nameDesc: NameDesc, symDesc: SymbolDesc): Symbol = new LexemeSymbol(new ConcreteSymbol(nameDesc, symDesc, errConfig), spaces, errConfig) - val plainName = NameDesc.plain.copy(identifierLetter = Basic(_.isLetter), operatorLetter = Basic(Set(':'))) + val plainName = NameDesc.plain.copy(identifierLetter = Basic(_.isLetter), operatorLetter = ':') val plainSym = SymbolDesc.plain.copy(hardKeywords = Set("keyword", "hard"), hardOperators = Set("+", "<", "<=")) val plainSymbol = makeSymbol(plainName, plainSym) - val unicodeSymbol = makeSymbol(plainName.copy(identifierLetter = Unicode(Character.isAlphabetic)), plainSym) + val unicodeSymbol = makeSymbol(plainName.copy(identifierLetter = Character.isAlphabetic(_)), plainSym) val caseInsensitive = makeSymbol(plainName, plainSym.copy(caseSensitive = false)) - val caseInsensitiveUni = makeSymbol(plainName.copy(identifierLetter = Unicode(Character.isAlphabetic)), plainSym.copy(caseSensitive = false)) + val caseInsensitiveUni = makeSymbol(plainName.copy(identifierLetter = Character.isAlphabetic(_)), plainSym.copy(caseSensitive = false)) def boolCases(p: Parsley[Unit])(tests: (String, Boolean, Position)*): Unit = cases(p, noEof = true)(tests.map { case (i, r, pos) => (i, if (r) Some(()) else None, pos) }: _*) def namedCases(sym: String => Parsley[Unit])(ktests: (String, Seq[(String, Boolean, Position)])*): Unit = { diff --git a/parsley/shared/src/test/scala/parsley/token/text/CharacterTests.scala b/parsley/shared/src/test/scala/parsley/token/text/CharacterTests.scala index 1490ace15..dda5ea91d 100644 --- a/parsley/shared/src/test/scala/parsley/token/text/CharacterTests.scala +++ b/parsley/shared/src/test/scala/parsley/token/text/CharacterTests.scala @@ -8,11 +8,14 @@ import parsley.ParsleyTest import parsley.token.LexemeImpl import parsley.token.descriptions.text._ +import parsley.token.errors.ErrorConfig import parsley.token.predicate._ import org.scalactic.source.Position class CharacterTests extends ParsleyTest { - def makeChar(desc: TextDesc): Character = new LexemeCharacter(new ConcreteCharacter(desc, new Escape(desc.escapeSequences)), LexemeImpl.empty) + val errConfig = new ErrorConfig + val generic = new parsley.token.numeric.Generic(errConfig) + def makeChar(desc: TextDesc): Character = new LexemeCharacter(new ConcreteCharacter(desc, new Escape(desc.escapeSequences, errConfig, generic), errConfig), LexemeImpl.empty) def unicodeCases(char: Character)(tests: (SString, Option[Int], Position)*): Unit = cases(char.fullUtf16)(tests: _*) def bmpCases(char: Character)(tests: (SString, Option[Char], Position)*): Unit = cases(char.basicMultilingualPlane)(tests: _*) @@ -78,6 +81,8 @@ class CharacterTests extends ParsleyTest { "'a'" -> Some('a'), "'\\lf'" -> Some('\n'), "'\\lam'" -> Some('λ'), + "'\ud800'" -> Some('\ud800'), // high surrogates are legal on their own + "'\udbff'" -> Some('\udbff'), // high surrogates are legal on their own ) they should "not parse wider unicode, including from escape characters" in bmpCases(plainChar)( @@ -86,6 +91,19 @@ class CharacterTests extends ParsleyTest { "'🇬🇧'" -> None, ) + they should "also behave similarly when given a non-unicode predicate" in bmpCases(plain.copy(graphicCharacter = Basic(_ >= ' ')))( + "'λ'" -> Some('λ'), + "' '" -> Some(' '), + "'a'" -> Some('a'), + "'\\lf'" -> Some('\n'), + "'\\lam'" -> Some('λ'), + "'\ud800'" -> Some('\ud800'), // high surrogates are legal on their own + "'\udbff'" -> Some('\udbff'), // high surrogates are legal on their own + "'\\oops'" -> None, + "'🙂'" -> None, + "'🇬🇧'" -> None, + ) + "extended-ascii literals" should "parse any valid extended ascii code-point" in extAsciiCases(plainChar)( "'a'" -> Some('a'), "'£'" -> Some('£'), diff --git a/parsley/shared/src/test/scala/parsley/token/text/EscapeTests.scala b/parsley/shared/src/test/scala/parsley/token/text/EscapeTests.scala index f7bcfbb9f..b632c1506 100644 --- a/parsley/shared/src/test/scala/parsley/token/text/EscapeTests.scala +++ b/parsley/shared/src/test/scala/parsley/token/text/EscapeTests.scala @@ -7,10 +7,13 @@ import scala.Predef.{String => SString, ArrowAssoc => _, _} import parsley.ParsleyTest import parsley.token.descriptions.text._ +import parsley.token.errors.ErrorConfig import org.scalactic.source.Position class EscapeTests extends ParsleyTest { - def cases(desc: EscapeDesc)(tests: (SString, Option[Int], Position)*): Unit = cases(new Escape(desc).escapeChar)(tests: _*) + val errConfig = new ErrorConfig + val generic = new parsley.token.numeric.Generic(errConfig) + def cases(desc: EscapeDesc)(tests: (SString, Option[Int], Position)*): Unit = cases(new Escape(desc, errConfig, generic).escapeChar)(tests: _*) val plain = EscapeDesc.plain diff --git a/parsley/shared/src/test/scala/parsley/token/text/StringTests.scala b/parsley/shared/src/test/scala/parsley/token/text/StringTests.scala index e2f102a80..6ff060da8 100644 --- a/parsley/shared/src/test/scala/parsley/token/text/StringTests.scala +++ b/parsley/shared/src/test/scala/parsley/token/text/StringTests.scala @@ -8,21 +8,24 @@ import parsley.ParsleyTest import parsley.token.LexemeImpl import parsley.token.descriptions.text._ +import parsley.token.errors.ErrorConfig import parsley.token.predicate._ import parsley.character.space import org.scalactic.source.Position class StringTests extends ParsleyTest { + val errConfig = new ErrorConfig + val generic = new parsley.token.numeric.Generic(errConfig) private def makeString(desc: TextDesc, char: StringCharacter, spaceAllowed: Boolean) = - new LexemeString(new ConcreteString(desc.stringEnds, char, desc.graphicCharacter, spaceAllowed), LexemeImpl.empty) + new LexemeString(new ConcreteString(desc.stringEnds, char, desc.graphicCharacter, spaceAllowed, errConfig), LexemeImpl.empty) private def makeString(desc: TextDesc): String = - makeString(desc, new EscapableCharacter(desc.escapeSequences, new Escape(desc.escapeSequences), space), false) + makeString(desc, new EscapableCharacter(desc.escapeSequences, new Escape(desc.escapeSequences, errConfig, generic), space, errConfig), false) private def makeMultiString(desc: TextDesc): String = - makeString(desc, new EscapableCharacter(desc.escapeSequences, new Escape(desc.escapeSequences), space), true) + makeString(desc, new EscapableCharacter(desc.escapeSequences, new Escape(desc.escapeSequences, errConfig, generic), space, errConfig), true) private def makeRawString(desc: TextDesc): String = - makeString(desc, RawCharacter, false) + makeString(desc, new RawCharacter(errConfig), false) private def makeRawMultiString(desc: TextDesc): String = - makeString(desc, RawCharacter, true) + makeString(desc, new RawCharacter(errConfig), true) def unicodeCases(str: String)(tests: (SString, Option[SString], Position)*): Unit = cases(str.fullUtf16)(tests: _*) def asciiCases(str: String)(tests: (SString, Option[SString], Position)*): Unit = cases(str.ascii)(tests: _*) diff --git a/project/build.properties b/project/build.properties index 563a014da..8b9a0b0ab 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.7.2 +sbt.version=1.8.0 diff --git a/project/plugins.sbt b/project/plugins.sbt index 0415e2fdc..36cb980ba 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -12,11 +12,11 @@ addSbtPlugin("org.typelevel" % "sbt-typelevel-ci-release" % sbtTypelevelVersion) // CI Stuff addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "1.2.0") addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.2.0") -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.11.0") -addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.8") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.12.0") +addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.9") // This is here purely to enable the niceness settings -addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.4.8") +addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1.5.6") addSbtPlugin("com.beautiful-scala" % "sbt-scalastyle" % "1.5.1") addSbtPlugin("org.jmotor.sbt" % "sbt-dependency-updates" % "1.2.7") addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.0.5") diff --git a/scalastyle-config.xml b/scalastyle-config.xml index 4cc024e1b..f2d5ec81d 100644 --- a/scalastyle-config.xml +++ b/scalastyle-config.xml @@ -75,7 +75,7 @@ ^_?[a-z][A-Za-z0-9]*_?$ - ^[a-zA-Z][A-Za-z0-9]*$ + ^_?[a-zA-Z][A-Za-z0-9]*$