diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile deleted file mode 100644 index fc606ba..0000000 --- a/.devcontainer/Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -FROM ubuntu:latest - -# https://code.visualstudio.com/remote/advancedcontainers/add-nonroot-user#_change-the-uidgid-of-an-existing-container-user -ARG USERNAME=ubuntu -ARG USER_UID=1000 -ARG USER_GID=$USER_UID - -RUN groupmod --gid $USER_GID $USERNAME \ - && usermod --uid $USER_UID --gid $USER_GID $USERNAME \ - && chown -R $USER_UID:$USER_GID /home/$USERNAME - -RUN apt-get update \ - && apt-get install -y openjdk-21-jdk openjdk-21-source git build-essential curl - -RUN mkdir -p /home/$USERNAME \ - && chown $USERNAME /home/$USERNAME \ - && su --login $USERNAME -c "curl -sSLf https://scala-cli.virtuslab.org/get | sh" \ - && su --login $USERNAME -c "scala-cli version" - -ENV EDITOR code --wait - -USER $USERNAME diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json deleted file mode 100644 index d6ece60..0000000 --- a/.devcontainer/devcontainer.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "DCal", - "build": { - "dockerfile": "Dockerfile" - }, - "customizations": { - "vscode": { - "extensions": [ - "scalameta.metals", - "github.vscode-github-actions", - "scala-lang.scala", - "ms-azuretools.vscode-docker" - ] - } - }, - "postCreateCommand": "scala-cli clean ." -} \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 627a529..3d6c7da 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,4 +12,5 @@ jobs: steps: - uses: actions/checkout@v4 - uses: coursier/cache-action@v6 + - run: ./format_src_check.sh - run: ./mill __.test diff --git a/.github/workflows/validation.yml b/.github/workflows/validation.yml deleted file mode 100644 index ebe39dc..0000000 --- a/.github/workflows/validation.yml +++ /dev/null @@ -1,21 +0,0 @@ -on: - pull_request: - push: - branches: ['main'] - -jobs: - scalafmt: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: coursier/cache-action@v6 - - run: ./format_src_check.sh - # license-check: - # runs-on: ubuntu-latest - # steps: - # - uses: actions/checkout@v4 - # - uses: coursier/cache-action@v6 - # - uses: VirtusLab/scala-cli-setup@main - # with: - # jvm: temurin:21 - # - run: scala-cli run . --main-class scripts.update_license_sc -- dry-run diff --git a/.scalafix.conf b/.scalafix.conf index 3810e05..c6cd4d2 100644 --- a/.scalafix.conf +++ b/.scalafix.conf @@ -7,7 +7,7 @@ OrganizeImports { groupedImports = Merge groups = [ "java." - "cats." + "scala." "forja." "*" ] diff --git a/.scalafmt.conf b/.scalafmt.conf index 8ccd57e..5e50b3e 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,4 @@ -version = "3.10.1" +version = "3.11.2" runner.dialect = scala3 assumeStandardLibraryStripMargin = true diff --git a/build.mill b/build.mill index 9e7b38c..9a2b9b6 100644 --- a/build.mill +++ b/build.mill @@ -11,7 +11,7 @@ import mill.api.Task.Simple import mill.api.TaskCtx trait ForjaModule extends ScalaModule, ScalafixModule: - def scalaVersion = "3.8.3" + def scalaVersion = "3.8.4" def scalacOptions = Seq( // "-Werror", "-Yexplicit-nulls", @@ -20,6 +20,7 @@ trait ForjaModule extends ScalaModule, ScalafixModule: "-source:future", "-Xcheck-macros", "-explain-cyclic", + "-Wunused:imports", "-preview", ) override def forkArgs = super.forkArgs() ++ Seq( @@ -41,75 +42,7 @@ end ForjaModule object forja extends ForjaModule: override def mvnDeps = Seq( - mvn"com.lihaoyi::sourcecode:0.4.4", - mvn"com.lihaoyi::os-lib:0.11.5", - mvn"org.typelevel::cats-core:2.13.0", - mvn"io.github.java-diff-utils:java-diff-utils:4.15", - mvn"dev.zio::izumi-reflect:3.0.9", + // TODO: when we need dependencies back ) object test extends ForjaTests - - private enum State: - case Normal, NextLineIsTemplate - case IsReplacing(count: Int) - - def updateLimit22Apply(check: Boolean = false) = Task.Command: - allSourceFiles().foreach: src => - var didReplace = false - var state = State.Normal - val replacedLines = - os.read.lines - .stream(src.path) - .flatMap: - case line @ s"$_// %%replicate22" if state == State.Normal => - didReplace = true - state = State.NextLineIsTemplate - List(line) - case line if state == State.NextLineIsTemplate => - state = State.IsReplacing(0) - var template = line - line +: (3 to 22).map: i => - template = template - .replace(s"T${i - 1}, U]", s"T${i - 1}, T$i, U]") - .replace( - s"t${i - 1}: T${i - 1})", - s"t${i - 1}: T${i - 1}, t$i: T$i)", - ) - .replace( - s"t${i - 1}: C ?=> T${i - 1})", - s"t${i - 1}: C ?=> T${i - 1}, t$i: C ?=> T$i)", - ) - .replace(s"T${i - 1})", s"T${i - 1}, T$i)") - .replace(s"t${i - 1})", s"t${i - 1}, t$i)") - .replace(s"T${i - 1}]", s"T${i - 1}, T$i]") - template - case line @ s"$_// format: on" - if state.isInstanceOf[State.IsReplacing] => - state = State.Normal - List(line) - case line if state.isInstanceOf[State.IsReplacing] => - assert(state.asInstanceOf[State.IsReplacing].count <= 20) - state = State.IsReplacing( - state.asInstanceOf[State.IsReplacing].count + 1, - ) - Nil - case line if state == State.Normal => - List(line) - case line => - assert(false, s"(in state $state) $line") - .toSeq - - if check && didReplace && replacedLines != os.read.lines(src.path) - then TaskCtx.taskCtx.fail(s"out of date: ${src.path}") - if check - then println(s"ok ${src.path}") - else if didReplace && replacedLines != os.read.lines(src.path) - then - println(s"rewrite ${src.path}") - os.write.over( - src.path, - replacedLines.view.flatMap(line => List(line, System.lineSeparator())), - ) - else println(s"no change ${src.path}") - end updateLimit22Apply end forja diff --git a/debugAdapterVSCode/package.json b/debugAdapterVSCode/package.json deleted file mode 100644 index 418cd0d..0000000 --- a/debugAdapterVSCode/package.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "name": "dcal-debug", - "displayName": "DCal Debug", - "version": "0.0.1", - "publisher": "...", - "description": "DCal DSL debugger", - "engines": { - "vscode": "^1.66.0" - }, - "categories": [ - "Debuggers" - ], - "private": true, - "workspaceTrust": { - "request": "never" - }, - "contributes": { - "breakpoints": [ - { - "language": "scala" - } - ], - "debuggers": [ - { - "type": "dcal", - "languages": [ - "scala" - ], - "label": "DCal Debug", - "configurationAttributes": { - "attach": { - "properties": {} - } - }, - "initialConfigurations": [ - { - "type": "dcal", - "name": "dcal", - "request": "attach", - "debugServer": 4711 - } - ] - } - ] - } -} diff --git a/forja/src/Lang.scala b/forja/src/Lang.scala new file mode 100644 index 0000000..bfacfc1 --- /dev/null +++ b/forja/src/Lang.scala @@ -0,0 +1,261 @@ +package forja + +import scala.util.NotGiven +import scala.annotation.publicInBinary +import scala.deriving.Mirror +import forja.util.Instanceless +import forja.util.InlineConversion +import java.util.Objects +import scala.compiletime.asMatchable + +trait Lang: + final transparent inline given this.type = this +end Lang + +object Lang: + trait Extend[Base <: Lang](using val up: Base) extends Lang + + abstract class Node: + final transparent inline given this.type = this + class ReplaceWith[OtherNode <: Node] + class Retract extends ReplaceWith[Node.Empty.type] + + type T + + inline given Node.TNode.Aux[T, this.type] = + Instanceless[Node.TNode.Aux[T, this.type]] + end Node + + object Node: + sealed trait TNode[T <: Node#T] extends Instanceless: + type N <: Node + end TNode + object TNode: + type Aux[T <: Node#T, N0 <: Node] = TNode[T] { + type N = N0 + } + end TNode + + object Empty extends Node: + type T = Nothing + end Empty + end Node + + abstract class Sum extends Node: + type Case <: Node + object Opaque: + into opaque type T = Any + extension (t: T) + inline def ex(using + inline ng: NotGiven[ReplaceWith[?]], + )(using et: Sum.EffectiveType[Case]): et.T = + t.asInstanceOf + end ex + end extension + end Opaque + override type T = Opaque.T + + inline given subtype: [T] => (inline ng: NotGiven[ReplaceWith[?]]) + => (et: Sum.EffectiveType[Case]) + => InlineConversion.ByCast[et.T, this.T] = InlineConversion.ByCast() + end Sum + + object Sum: + sealed trait EffectiveType[C <: Sum#Case] extends Instanceless: + type T <: Node#T + end EffectiveType + object EffectiveType: + type Aux[C <: Sum#Case, T0 <: Node#T] = EffectiveType[C] { + type T = T0 + } + + inline given inst: [C <: Sum#Case] => (mirror: Mirror.SumOf[C]) + => (tc: TransformedCases[mirror.MirroredElemTypes]) + => EffectiveType.Aux[C, tc.C] = Instanceless[EffectiveType.Aux[C, tc.C]] + end EffectiveType + + sealed trait TransformedCases[Cases <: Tuple] extends Instanceless: + type C <: Node#T + end TransformedCases + object TransformedCases: + type Aux[Cases <: Tuple, C0 <: Node#T] = TransformedCases[Cases] { + type C = C0 + } + + inline def apply[Cases <: Tuple, C <: Node#T](): Aux[Cases, C] = + Instanceless[Aux[Cases, C]] + + inline given empty: TransformedCases.Aux[EmptyTuple, Nothing] = + TransformedCases() + inline given cons: [Hd, Tl <: Tuple] + => (eht: EffectiveNodeType[Hd & Node]) => (HN: eht.To) + => (ttl: TransformedCases[Tl]) + => TransformedCases.Aux[Hd *: Tl, HN.T | ttl.C] = TransformedCases() + end TransformedCases + end Sum + + abstract class Term[Members <: NamedTuple.AnyNamedTuple] extends Node: + self: Singleton => + final class T @publicInBinary private[Term] ( + private[Term] val members: Tuple, + ): + override def toString(): String = s"${self.getClass().getName()}$members" + override def equals(obj: Any): Boolean = + obj.asMatchable match + case other: T => members == other.members + end match + end equals + override def hashCode(): Int = + Objects.hash(self, members) + end hashCode + end T + + inline def apply(using + inline ng: NotGiven[ReplaceWith[?]], + )(using et: EffectiveType[Members])(members: et.To): T = + T(members.asInstanceOf) + end apply + + // Patterns and inline do not go well together. Making this inline will create and then call + // a lambda with the inline body, which is strictly worse than just calling the method. + // This issue only happens in patterns; the apply above translates to new T properly. + def unapply(t: T)(using + ng: NotGiven[ReplaceWith[?]], + )(using et: EffectiveType[Members]): et.To = + t.members.asInstanceOf[et.To] + end unapply + end Term + + sealed trait EffectiveNodeType[From <: Node] extends Instanceless: + type To <: Node + end EffectiveNodeType + object EffectiveNodeType: + type Aux[From <: Node, To0 <: Node] = EffectiveNodeType[From] { + type To = To0 + } + + inline def apply[From <: Node, To <: Node]() + : EffectiveNodeType.Aux[From, To] = + Instanceless[EffectiveNodeType.Aux[From, To]] + end EffectiveNodeType + + inline given effectiveNodeIdentity: [N <: Node] => (N: N) + => (inline ng: NotGiven[N.ReplaceWith[?]]) => EffectiveNodeType.Aux[N, N] = + EffectiveNodeType() + inline given effectiveNodeReplaced + : [N1 <: Node, N2 <: Node] => (N1: N1) => (N1.ReplaceWith[N2]) + => (et: EffectiveNodeType[N2]) => EffectiveNodeType.Aux[N1, et.To] = + EffectiveNodeType() + + sealed trait EffectiveType[From] extends Instanceless: + type To + end EffectiveType + object EffectiveType: + type Aux[From, To0] = EffectiveType[From] { + type To = To0 + } + + trait Ident[T] extends EffectiveType[T]: + type To = T + end Ident + object Ident: + inline def apply[T](): Ident[T] = Instanceless[Ident[T]] + end Ident + + inline def apply[From, To](): EffectiveType.Aux[From, To] = + Instanceless[EffectiveType.Aux[From, To]] + end EffectiveType + + // Wacky: if T is bounded, implicit search fails. Instead, hack it into trying no matter what T is, and fix the TNode bound using an + // intersection. + inline given effectiveNode + : [T] => (tn: Node.TNode[T & Node#T]) => (ent: EffectiveNodeType[tn.N]) + => (N2: ent.To) => EffectiveType.Aux[T, N2.T] = EffectiveType() + + trait EffectiveTupleType[From <: Tuple]: + type To <: Tuple + end EffectiveTupleType + object EffectiveTupleType: + type Aux[From <: Tuple, To0 <: Tuple] = EffectiveTupleType[From] { + type To = To0 + } + + inline def apply[From <: Tuple, To <: Tuple](): Aux[From, To] = + Instanceless[Aux[From, To]] + + inline given effectiveTupleEmpty: Aux[EmptyTuple, EmptyTuple] = + EffectiveTupleType() + inline given effectiveTupleCons: [Hd, Tl <: Tuple] + => (eh: EffectiveType[Hd]) => (et: EffectiveTupleType[Tl]) + => EffectiveTupleType.Aux[Hd *: Tl, eh.To *: et.To] = EffectiveTupleType() + end EffectiveTupleType + + inline given effectiveTuple: [T <: Tuple] => (ett: EffectiveTupleType[T]) + => EffectiveType.Aux[T, ett.To] = EffectiveType() + inline given effectiveNamedTuple: [Nt <: NamedTuple.AnyNamedTuple] + => (et: EffectiveTupleType[NamedTuple.DropNames[Nt]]) + => EffectiveType.Aux[Nt, NamedTuple.NamedTuple[NamedTuple.Names[ + Nt, + ], et.To]] = EffectiveType() + + inline given EffectiveType.Ident[Boolean] = EffectiveType.Ident() + inline given EffectiveType.Ident[Byte] = EffectiveType.Ident() + inline given EffectiveType.Ident[Char] = EffectiveType.Ident() + inline given EffectiveType.Ident[Short] = EffectiveType.Ident() + inline given EffectiveType.Ident[Int] = EffectiveType.Ident() + inline given EffectiveType.Ident[Long] = EffectiveType.Ident() + inline given EffectiveType.Ident[Float] = EffectiveType.Ident() + inline given EffectiveType.Ident[Double] = EffectiveType.Ident() + + inline given EffectiveType.Ident[String] = EffectiveType.Ident() + + inline given [T] => (et: EffectiveType[T]) + => EffectiveType.Aux[Option[T], Option[et.To]] = EffectiveType() + inline given [T] => (et: EffectiveType[T]) + => EffectiveType.Aux[List[T], List[et.To]] = EffectiveType() +end Lang + +object Test: + def main(args: Array[String]): Unit = + trait L1 extends Lang: + object Foo extends Lang.Term[(i: Int, j: Int)] + + object Ping extends Lang.Sum: + sealed trait Case extends Lang.Node + + object Pong extends Lang.Term[(k: Int, foo: Foo.T)], Case + end Ping + end L1 + object L1 extends L1 + + val x = L1.Foo(i = 42, j = 43) + println(x) + val ping: L1.Ping.T = L1.Ping.Pong(k = 12, foo = x) + println(ping) + + trait L2 extends Lang.Extend[L1]: + export up.{Foo as _, *} + object Bar extends Lang.Term[(s: String, opt: Option[up.Foo.T])] + + given r1: up.Foo.ReplaceWith[Bar.type]() + given r2: up.Ping.Pong.ReplaceWith[Bar.type]() + // given up.Foo.Retract + end L2 + object L2 extends L2 + + val y: L2.Bar.T = L2.Bar(s = "hi", opt = Some(L2.Bar("ho", None))) + println(y) + val ping2: L2.Ping.T = y + println(ping2) + + y match + case L2.Bar(s, opt) => + println((s, opt)) + end match + + ping2.ex match + case L2.Bar(s, opt) => + println(s"$s, $opt") + end match + end main +end Test diff --git a/forja/src/ModelChecker.scala b/forja/src/ModelChecker.scala deleted file mode 100644 index 019dda4..0000000 --- a/forja/src/ModelChecker.scala +++ /dev/null @@ -1,73 +0,0 @@ -package forja - -import scala.collection.mutable -import scala.concurrent.ExecutionContext - -transparent trait ModelChecker: - type State - type ErrorState - - extension (state: State) def checkErrorState: Option[ErrorState] - - def initStates(using ExecutionContext): Iterator[State] - def nextStates(state: State)(using ExecutionContext): Iterator[State] - - def assertCheck(): Unit = - check() match - case None => // ok - case Some((errorState, path)) => - val builder = StringBuilder() - path.foreach: state => - builder ++= state.toString() - builder ++= "\n---\n" - builder ++= errorState.toString() - throw AssertionError(builder.result()) - end match - end assertCheck - - def check()(using - ctx: ExecutionContext = ExecutionContext.global, - ): Option[(ErrorState, Seq[State])] = - val stateQueue = mutable.Queue.from(initStates) - val knownStates = mutable.HashMap.from[State, Option[State]]( - stateQueue.iterator.map(_ -> None), - ) - - var result: Option[(ErrorState, Seq[State])] = None - - while stateQueue.nonEmpty && result.isEmpty - do - val state = stateQueue.synchronized(stateQueue.dequeue()) - var hasNextStates = false - nextStates(state).foreach: nextState => - hasNextStates = true - knownStates.getOrElseUpdate( - nextState, { - stateQueue.enqueue(nextState) - Some(state) - }, - ) - - if !hasNextStates - then - state.checkErrorState match - case None => - case Some(errorState) => - val path = - Iterator - .iterate(Some(state): Option[State]): stateOpt => - stateOpt - .flatMap(knownStates.get) - .flatten - .takeWhile(_.nonEmpty) - .flatten - .toSeq - .reverse - result = Some((errorState, path)) - end match - end if - end while - - result - end check -end ModelChecker diff --git a/forja/src/Node.scala b/forja/src/Node.scala deleted file mode 100644 index 411a80f..0000000 --- a/forja/src/Node.scala +++ /dev/null @@ -1,603 +0,0 @@ -package forja - -import java.io.{ByteArrayOutputStream, OutputStream} -import java.nio.charset.StandardCharsets - -import forja.util.{FastPatchTree, MonomorphicIndexedSeq} - -import scala.annotation.{publicInBinary, tailrec} -import scala.collection.concurrent -import scala.compiletime.asMatchable -import scala.reflect.TypeTest - -import Node.* - -final class Node @publicInBinary private[forja] ( - private[forja] val impl: Node.NodeImpl, - private val nodeParentInfo: NodeParentInfo, -) extends geny.Writable: - /* Regular nodeParentInfo may not refer to a parent that refers back to impl. - * If we want a parent ref that is up to date, this will lazily populate such - * a thing. - * Important benefit of doing it like this: this lazy val is internal and does - * not invoke any other lazy vals, so it will always expand just 1 parent - * node. Making nodeParentInfo itself lazy could lead to arbitrary recursion. */ - private lazy val nodeParentInfoStable = - nodeParentInfo match - case NodeParentInfo.IndexParent(parent, index) => - val parentImpl = parent.impl.asInstanceOf[NodeImpl.TokenNode] - if parentImpl.children(index) eq this.impl - then nodeParentInfo - else - NodeParentInfo.IndexParent( - new Node( - impl = parentImpl.copy(children = - parentImpl.children.updated(index, this.impl), - ), - nodeParentInfo = parent.nodeParentInfo, - ), - index, - ) - case NodeParentInfo.AttrParent(parent, attr) => - val parentImpl = parent.impl.asInstanceOf[NodeImpl.TokenNode] - if parentImpl.attrs(attr) eq this.impl - then nodeParentInfo - else - NodeParentInfo.AttrParent( - new Node( - impl = parentImpl.copy(attrs = - parentImpl.attrs.updated(attr, this.impl), - ), - nodeParentInfo = parent.nodeParentInfo, - ), - attr, - ) - case NodeParentInfo.Orphan => - NodeParentInfo.Orphan - end nodeParentInfoStable - - def tokenOption: Option[Token] = - impl match - case NodeImpl.TokenNode(token, children, attrs) => Some(token) - case _: (NodeImpl.EmbedNode | NodeImpl.ErrorNode) => None - end tokenOption - - def valueOption[T](using embed: Embed[T]): Option[T] = - impl match - case _: (NodeImpl.TokenNode | NodeImpl.ErrorNode) => None - case NodeImpl.EmbedNode(value) => - import embed.typeTest - value match - case value: T => Some(value) - case _ => None - end match - end match - end valueOption - - def valueOptionRaw: Option[Any] = - impl match - case _: (NodeImpl.TokenNode | NodeImpl.ErrorNode) => None - case NodeImpl.EmbedNode(value) => - Some(value) - end valueOptionRaw - - def isError: Boolean = - impl match - case _: NodeImpl.ErrorNode => true - case _: (NodeImpl.EmbedNode | NodeImpl.TokenNode) => false - end isError - - export impl.containsError - - def errorMsgOption: Option[String] = - impl match - case NodeImpl.ErrorNode(msg, _*) => Some(msg) - case _: (NodeImpl.EmbedNode | NodeImpl.TokenNode) => None - end errorMsgOption - - def errorNodesOption: Option[Seq[Node]] = - impl match - case NodeImpl.ErrorNode(msg, nodes*) => Some(nodes) - case _: (NodeImpl.EmbedNode | NodeImpl.TokenNode) => None - end errorNodesOption - - def parentOption: Option[Node] = - nodeParentInfoStable match - case NodeParentInfo.IndexParent(parent, index) => Some(parent) - case NodeParentInfo.AttrParent(parent, attr) => Some(parent) - case NodeParentInfo.Orphan => None - end parentOption - - @tailrec - def root: Node = - nodeParentInfoStable match - case NodeParentInfo.IndexParent(parent, index) => parent.root - case NodeParentInfo.AttrParent(parent, attr) => parent.root - case NodeParentInfo.Orphan => this - end root - - def leftSiblingOption: Option[Node] = - nodeParentInfoStable match - case NodeParentInfo.IndexParent(parent, index) => - parent.children.lift(index - 1) - case _: (NodeParentInfo.AttrParent | NodeParentInfo.Orphan.type) => - None - end leftSiblingOption - - def rightSiblingOption: Option[Node] = - nodeParentInfoStable match - case NodeParentInfo.IndexParent(parent, index) => - parent.children.lift(index + 1) - case _: (NodeParentInfo.AttrParent | NodeParentInfo.Orphan.type) => - None - end rightSiblingOption - - def asOrphan: Node = - new Node( - impl = impl, - nodeParentInfo = NodeParentInfo.Orphan, - ) - - def replaceThis(replacement: Node): Node = - new Node( - impl = replacement.impl, - nodeParentInfo = nodeParentInfo, - ) - end replaceThis - - def children: NodeChildren = NodeChildren(this) - def attrs: NodeAttrs = NodeAttrs(this) - - def emptyNodeSpanHere: NodeSpan = nodeParentInfoStable match - case NodeParentInfo.IndexParent(parent, index) => - IndexedParentNodeSpan(parent, index, 0) - case _: (NodeParentInfo.AttrParent | NodeParentInfo.Orphan.type) => - SingletonNodeSpan(this, false) - end emptyNodeSpanHere - - def singletonNodeSpanHere: NodeSpan = nodeParentInfoStable match - case NodeParentInfo.IndexParent(parent, index) => - IndexedParentNodeSpan(parent, index, 1) - case _: (NodeParentInfo.AttrParent | NodeParentInfo.Orphan.type) => - SingletonNodeSpan(this, true) - end singletonNodeSpanHere - - def query[T](query: Query[T]): Option[T] = - query.runQuery(this).value - end query - - override def equals(that: Any): Boolean = - that.asMatchable match - case that: Node => this.impl == that.impl - case _ => false - end equals - - override def hashCode(): Int = - this.impl.hashCode() - end hashCode - - override def toString(): String = - val out = ByteArrayOutputStream() - writeBytesTo(out) - out.toString(StandardCharsets.UTF_8) - // impl match - // case NodeImpl.TokenNode(token, _, _) => - // s"$token(${(this.children.view.map(_.toString()) ++ this.attrs.view.map( - // (k, v) => s"$k -> $v", - // )).mkString(", ")})" - // case NodeImpl.EmbedNode(value) => - // val embed = Node.Embed.embedByValue(value) - // s"${value.getClass().getName()}($value)" - // case NodeImpl.ErrorNode(msg, nodes*) => - // s"#error(\"$msg\", ${nodes.mkString(", ")})" - end toString - - def writeBytesTo(out_ : OutputStream): Unit = - - object out extends OutputStream: - private var indent = 0 - def write(b: Int): Unit = - b match - case '\n' => - out_.write('\n') - (0 until indent).foreach(_ => out_.write(' ')) - case b => - out_.write(b) - end write - - def indentedBy[T](amt: Int = 2)(fn: => T): T = - try - indent += amt - fn - finally indent -= amt - end indentedBy - end out - - var isFirstLine = true - def nl(): Unit = - if isFirstLine - then isFirstLine = false - else out.write('\n') - end nl - - def writeImpl(impl: NodeImpl): Unit = - nl() - impl match - case NodeImpl.TokenNode(token, children, attrs) => - out.write('>') - out.write(token.fullName.getBytes(StandardCharsets.UTF_8)) - out.indentedBy(): - children.foreach: impl => - writeImpl(impl) - attrs.keys.toArray - .sortBy(_.fullName) - .foreach: k => - nl() - out.write('?') - out.write(k.fullName.getBytes(StandardCharsets.UTF_8)) - out.indentedBy(): - writeImpl(attrs(k)) - case NodeImpl.EmbedNode(value) => - val embed = Embed.embedByValue(value) - - out.write('~') - out.write(embed.getClass().getName().getBytes(StandardCharsets.UTF_8)) - out.indentedBy(): - nl() - embed.writeBytesTo(value, out) - case NodeImpl.ErrorNode(msg, nodes*) => - out.indentedBy(): - msg.linesWithSeparators - .foreach: line => - out.write('!') - out.write(line.getBytes(StandardCharsets.UTF_8)) - nodes.foreach: impl => - writeImpl(impl.impl) - end match - end writeImpl - - writeImpl(impl) - end writeBytesTo -end Node - -export Node.NodeSpan - -object Node: - def embed[T <: Matchable](value: T)(using embed: Embed[T]): Node = - Embed.embedByClass.putIfAbsent(value.getClass(), embed) - new Node( - impl = NodeImpl.EmbedNode(value), - nodeParentInfo = NodeParentInfo.Orphan, - ) - end embed - - def error(msg: String)(nodes: Node*): Node = - new Node( - impl = NodeImpl.ErrorNode(msg, nodes*), - nodeParentInfo = NodeParentInfo.Orphan, - ) - end error - - inline def applyTupled[Tp <: Tuple, U](tp: Tp)(using - ctx: syntax.Context, - )(using app: ctx.NodeApply[Tp, U]): U = app(tp) - - // format: off - inline def apply[U]()(using ctx: syntax.Context)(using app: ctx.NodeApply[EmptyTuple, U]): U = app(EmptyTuple) - inline def apply[T1, U](t1: T1)(using ctx: syntax.Context)(using app: ctx.NodeApply[Tuple1[T1], U]): U = app(Tuple1(t1)) - // %%replicate22 - inline def apply[T1, T2, U](t1: T1, t2: T2)(using ctx: syntax.Context)(using app: ctx.NodeApply[(T1, T2), U]): U = app((t1, t2)) - inline def apply[T1, T2, T3, U](t1: T1, t2: T2, t3: T3)(using ctx: syntax.Context)(using app: ctx.NodeApply[(T1, T2, T3), U]): U = app((t1, t2, t3)) - inline def apply[T1, T2, T3, T4, U](t1: T1, t2: T2, t3: T3, t4: T4)(using ctx: syntax.Context)(using app: ctx.NodeApply[(T1, T2, T3, T4), U]): U = app((t1, t2, t3, t4)) - inline def apply[T1, T2, T3, T4, T5, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5)(using ctx: syntax.Context)(using app: ctx.NodeApply[(T1, T2, T3, T4, T5), U]): U = app((t1, t2, t3, t4, t5)) - inline def apply[T1, T2, T3, T4, T5, T6, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6)(using ctx: syntax.Context)(using app: ctx.NodeApply[(T1, T2, T3, T4, T5, T6), U]): U = app((t1, t2, t3, t4, t5, t6)) - inline def apply[T1, T2, T3, T4, T5, T6, T7, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7)(using ctx: syntax.Context)(using app: ctx.NodeApply[(T1, T2, T3, T4, T5, T6, T7), U]): U = app((t1, t2, t3, t4, t5, t6, t7)) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8)(using ctx: syntax.Context)(using app: ctx.NodeApply[(T1, T2, T3, T4, T5, T6, T7, T8), U]): U = app((t1, t2, t3, t4, t5, t6, t7, t8)) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8, t9: T9)(using ctx: syntax.Context)(using app: ctx.NodeApply[(T1, T2, T3, T4, T5, T6, T7, T8, T9), U]): U = app((t1, t2, t3, t4, t5, t6, t7, t8, t9)) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8, t9: T9, t10: T10)(using ctx: syntax.Context)(using app: ctx.NodeApply[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10), U]): U = app((t1, t2, t3, t4, t5, t6, t7, t8, t9, t10)) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8, t9: T9, t10: T10, t11: T11)(using ctx: syntax.Context)(using app: ctx.NodeApply[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11), U]): U = app((t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11)) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8, t9: T9, t10: T10, t11: T11, t12: T12)(using ctx: syntax.Context)(using app: ctx.NodeApply[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12), U]): U = app((t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12)) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8, t9: T9, t10: T10, t11: T11, t12: T12, t13: T13)(using ctx: syntax.Context)(using app: ctx.NodeApply[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13), U]): U = app((t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13)) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8, t9: T9, t10: T10, t11: T11, t12: T12, t13: T13, t14: T14)(using ctx: syntax.Context)(using app: ctx.NodeApply[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14), U]): U = app((t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14)) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8, t9: T9, t10: T10, t11: T11, t12: T12, t13: T13, t14: T14, t15: T15)(using ctx: syntax.Context)(using app: ctx.NodeApply[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15), U]): U = app((t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15)) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8, t9: T9, t10: T10, t11: T11, t12: T12, t13: T13, t14: T14, t15: T15, t16: T16)(using ctx: syntax.Context)(using app: ctx.NodeApply[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16), U]): U = app((t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15, t16)) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8, t9: T9, t10: T10, t11: T11, t12: T12, t13: T13, t14: T14, t15: T15, t16: T16, t17: T17)(using ctx: syntax.Context)(using app: ctx.NodeApply[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17), U]): U = app((t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15, t16, t17)) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8, t9: T9, t10: T10, t11: T11, t12: T12, t13: T13, t14: T14, t15: T15, t16: T16, t17: T17, t18: T18)(using ctx: syntax.Context)(using app: ctx.NodeApply[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18), U]): U = app((t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15, t16, t17, t18)) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8, t9: T9, t10: T10, t11: T11, t12: T12, t13: T13, t14: T14, t15: T15, t16: T16, t17: T17, t18: T18, t19: T19)(using ctx: syntax.Context)(using app: ctx.NodeApply[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19), U]): U = app((t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15, t16, t17, t18, t19)) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8, t9: T9, t10: T10, t11: T11, t12: T12, t13: T13, t14: T14, t15: T15, t16: T16, t17: T17, t18: T18, t19: T19, t20: T20)(using ctx: syntax.Context)(using app: ctx.NodeApply[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20), U]): U = app((t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15, t16, t17, t18, t19, t20)) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8, t9: T9, t10: T10, t11: T11, t12: T12, t13: T13, t14: T14, t15: T15, t16: T16, t17: T17, t18: T18, t19: T19, t20: T20, t21: T21)(using ctx: syntax.Context)(using app: ctx.NodeApply[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21), U]): U = app((t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15, t16, t17, t18, t19, t20, t21)) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8, t9: T9, t10: T10, t11: T11, t12: T12, t13: T13, t14: T14, t15: T15, t16: T16, t17: T17, t18: T18, t19: T19, t20: T20, t21: T21, t22: T22)(using ctx: syntax.Context)(using app: ctx.NodeApply[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22), U]): U = app((t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15, t16, t17, t18, t19, t20, t21, t22)) - // format: on - - trait Embed[T](using val typeTest: TypeTest[Matchable, T]): - self: Singleton => - locally: - val modField = getClass().getField("MODULE$") - require( - modField ne null, - s"${getClass().getName()} must have a static MODULE$$ field, or it will not be deserializable", - ) - require( - modField.get(null) eq self, - s"${getClass().getName()}'s static MODULE$$ field must refer to itself", - ) - // Compiler crash? - // given typeTest: TypeTest[Matchable, T] = deferred - def prettyString(value: T): String - def writeBytesTo(value: T, out: OutputStream): Unit - end Embed - - object Embed: - private[Node] val embedByClass = concurrent.TrieMap[Class[?], Embed[?]]() - private[forja] def embedByValue[T](value: T): Node.Embed[T] = - embedByClass(value.getClass()).asInstanceOf[Node.Embed[T]] - end embedByValue - - trait EmbedPrimitive[T] extends Embed[T]: - self: Singleton => - def prettyString(value: T): String = - s"${value.getClass()}($value)" - end prettyString - def writeBytesTo(value: T, out: OutputStream): Unit = - out.write(value.toString().getBytes(StandardCharsets.UTF_8)) - end writeBytesTo - end EmbedPrimitive - - given embedBoolean: EmbedPrimitive[Boolean] {} - given embedByte: EmbedPrimitive[Byte] {} - given embedInt: EmbedPrimitive[Int] {} - given embedLong: EmbedPrimitive[Long] {} - given embedFloat: EmbedPrimitive[Float] {} - given embedDouble: EmbedPrimitive[Double] {} - given embedChar: EmbedPrimitive[Char] {} - end Embed - - sealed trait Attrs extends Map[Token, Node]: - - end Attrs - - private[forja] enum NodeParentInfo: - case IndexParent(parent: Node, index: Int) - case AttrParent(parent: Node, attr: Token) - case Orphan - end NodeParentInfo - - private[forja] enum NodeImpl: - this match - case _: (TokenNode | ErrorNode) => - // nothing to do here - case EmbedNode(value) => - assert( - Embed.embedByClass.contains(value.getClass()), - s"!!internal error: using unregistered embed of ${value.getClass()}", - ) - - case TokenNode( - token: Token, - children: FastPatchTree[NodeImpl], - attrs: Map[Token, NodeImpl], - ) - case EmbedNode(value: Matchable) - case ErrorNode(msg: String, nodes: Node*) - - val containsError: Boolean = - this match - case _: ErrorNode => true - case _: EmbedNode => false - case TokenNode(token, children, attrs) => - children.exists(_.containsError) - || attrs.values.exists(_.containsError) - end containsError - end NodeImpl - - final class NodeChildren private[Node] (parent: Node) - extends IndexedSeq[Node]: - private[Node] val implSeq: FastPatchTree[NodeImpl] = parent.impl match - case NodeImpl.TokenNode(token, children, attrs) => children - case _: (NodeImpl.EmbedNode | NodeImpl.ErrorNode) => FastPatchTree.empty - end implSeq - - def apply(i: Int): Node = - new Node( - impl = implSeq(i), - nodeParentInfo = NodeParentInfo.IndexParent(parent, i), - ) - end apply - - export implSeq.length - - def asEmptyNodeSpan: NodeSpan = - IndexedParentNodeSpan(parent, 0, 0) - end asEmptyNodeSpan - - def asNodeSpan: NodeSpan = - IndexedParentNodeSpan(parent, 0, length) - end asNodeSpan - end NodeChildren - - final class NodeAttrs private[Node] (parent: Node) extends Map[Token, Node]: - private[Node] val implMap: Map[Token, NodeImpl] = parent.impl match - case NodeImpl.TokenNode(token, children, attrs) => attrs - case _: (NodeImpl.EmbedNode | NodeImpl.ErrorNode) => Map.empty - end implMap - - def get(key: Token): Option[Node] = - implMap - .get(key) - .map: impl => - new Node( - impl = impl, - nodeParentInfo = NodeParentInfo.AttrParent(parent, key), - ) - end get - - def iterator: Iterator[(Token, Node)] = - implMap.iterator - .map: (token, impl) => - token -> new Node( - impl = impl, - nodeParentInfo = NodeParentInfo.AttrParent(parent, token), - ) - end iterator - - private lazy val reifiedMap = iterator.toMap - export reifiedMap.{updated, removed} - end NodeAttrs - - sealed trait NodeSpan extends MonomorphicIndexedSeq[Node, NodeSpan]: - def replaceThis(nodes: Iterable[Node]): NodeSpan - def expandLeftOption(n: Int): Option[NodeSpan] - def expandRightOption(n: Int): Option[NodeSpan] - def expandRightMax: NodeSpan - def parentOption: Option[Node] - def root: Node - override protected def className: String = "NodeSpan" - end NodeSpan - - object NodeSpan: - inline def applyTupled[Tp <: Tuple, U](tp: Tp)(using - ctx: syntax.Context, - )(using app: ctx.NodeSpanApply[Tp, U]): U = app(tp) - - // format: off - inline def apply[U]()(using ctx: syntax.Context)(using app: ctx.NodeSpanApply[EmptyTuple, U]): U = app(EmptyTuple) - inline def apply[T1, U](t1: T1)(using ctx: syntax.Context)(using app: ctx.NodeSpanApply[Tuple1[T1], U]): U = app(Tuple1(t1)) - // %%replicate22 - inline def apply[T1, T2, U](t1: T1, t2: T2)(using ctx: syntax.Context)(using app: ctx.NodeSpanApply[(T1, T2), U]): U = app((t1, t2)) - inline def apply[T1, T2, T3, U](t1: T1, t2: T2, t3: T3)(using ctx: syntax.Context)(using app: ctx.NodeSpanApply[(T1, T2, T3), U]): U = app((t1, t2, t3)) - inline def apply[T1, T2, T3, T4, U](t1: T1, t2: T2, t3: T3, t4: T4)(using ctx: syntax.Context)(using app: ctx.NodeSpanApply[(T1, T2, T3, T4), U]): U = app((t1, t2, t3, t4)) - inline def apply[T1, T2, T3, T4, T5, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5)(using ctx: syntax.Context)(using app: ctx.NodeSpanApply[(T1, T2, T3, T4, T5), U]): U = app((t1, t2, t3, t4, t5)) - inline def apply[T1, T2, T3, T4, T5, T6, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6)(using ctx: syntax.Context)(using app: ctx.NodeSpanApply[(T1, T2, T3, T4, T5, T6), U]): U = app((t1, t2, t3, t4, t5, t6)) - inline def apply[T1, T2, T3, T4, T5, T6, T7, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7)(using ctx: syntax.Context)(using app: ctx.NodeSpanApply[(T1, T2, T3, T4, T5, T6, T7), U]): U = app((t1, t2, t3, t4, t5, t6, t7)) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8)(using ctx: syntax.Context)(using app: ctx.NodeSpanApply[(T1, T2, T3, T4, T5, T6, T7, T8), U]): U = app((t1, t2, t3, t4, t5, t6, t7, t8)) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8, t9: T9)(using ctx: syntax.Context)(using app: ctx.NodeSpanApply[(T1, T2, T3, T4, T5, T6, T7, T8, T9), U]): U = app((t1, t2, t3, t4, t5, t6, t7, t8, t9)) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8, t9: T9, t10: T10)(using ctx: syntax.Context)(using app: ctx.NodeSpanApply[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10), U]): U = app((t1, t2, t3, t4, t5, t6, t7, t8, t9, t10)) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8, t9: T9, t10: T10, t11: T11)(using ctx: syntax.Context)(using app: ctx.NodeSpanApply[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11), U]): U = app((t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11)) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8, t9: T9, t10: T10, t11: T11, t12: T12)(using ctx: syntax.Context)(using app: ctx.NodeSpanApply[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12), U]): U = app((t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12)) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8, t9: T9, t10: T10, t11: T11, t12: T12, t13: T13)(using ctx: syntax.Context)(using app: ctx.NodeSpanApply[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13), U]): U = app((t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13)) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8, t9: T9, t10: T10, t11: T11, t12: T12, t13: T13, t14: T14)(using ctx: syntax.Context)(using app: ctx.NodeSpanApply[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14), U]): U = app((t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14)) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8, t9: T9, t10: T10, t11: T11, t12: T12, t13: T13, t14: T14, t15: T15)(using ctx: syntax.Context)(using app: ctx.NodeSpanApply[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15), U]): U = app((t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15)) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8, t9: T9, t10: T10, t11: T11, t12: T12, t13: T13, t14: T14, t15: T15, t16: T16)(using ctx: syntax.Context)(using app: ctx.NodeSpanApply[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16), U]): U = app((t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15, t16)) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8, t9: T9, t10: T10, t11: T11, t12: T12, t13: T13, t14: T14, t15: T15, t16: T16, t17: T17)(using ctx: syntax.Context)(using app: ctx.NodeSpanApply[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17), U]): U = app((t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15, t16, t17)) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8, t9: T9, t10: T10, t11: T11, t12: T12, t13: T13, t14: T14, t15: T15, t16: T16, t17: T17, t18: T18)(using ctx: syntax.Context)(using app: ctx.NodeSpanApply[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18), U]): U = app((t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15, t16, t17, t18)) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8, t9: T9, t10: T10, t11: T11, t12: T12, t13: T13, t14: T14, t15: T15, t16: T16, t17: T17, t18: T18, t19: T19)(using ctx: syntax.Context)(using app: ctx.NodeSpanApply[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19), U]): U = app((t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15, t16, t17, t18, t19)) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8, t9: T9, t10: T10, t11: T11, t12: T12, t13: T13, t14: T14, t15: T15, t16: T16, t17: T17, t18: T18, t19: T19, t20: T20)(using ctx: syntax.Context)(using app: ctx.NodeSpanApply[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20), U]): U = app((t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15, t16, t17, t18, t19, t20)) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8, t9: T9, t10: T10, t11: T11, t12: T12, t13: T13, t14: T14, t15: T15, t16: T16, t17: T17, t18: T18, t19: T19, t20: T20, t21: T21)(using ctx: syntax.Context)(using app: ctx.NodeSpanApply[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21), U]): U = app((t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15, t16, t17, t18, t19, t20, t21)) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8, t9: T9, t10: T10, t11: T11, t12: T12, t13: T13, t14: T14, t15: T15, t16: T16, t17: T17, t18: T18, t19: T19, t20: T20, t21: T21, t22: T22)(using ctx: syntax.Context)(using app: ctx.NodeSpanApply[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22), U]): U = app((t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15, t16, t17, t18, t19, t20, t21, t22)) - // format: on - end NodeSpan - - private final class SingletonNodeSpan(node: Node, includesMe: Boolean) - extends NodeSpan: - def apply(i: Int): Node = - if includesMe && i == 0 - then node - else throw IndexOutOfBoundsException(s"$i out of bounds") - end apply - - def length: Int = if includesMe then 1 else 0 - - protected def sliceImpl(from: Int, until: Int): NodeSpan = - val slicedIndices = indices.slice(from, until) - if includesMe && slicedIndices.isEmpty - then SingletonNodeSpan(node, false) - else this - end sliceImpl - - def expandLeftOption(n: Int): Option[NodeSpan] = - if n != 0 - then None - else Some(this) - end expandLeftOption - - def expandRightOption(n: Int): Option[NodeSpan] = - if !includesMe && n == 1 - then Some(SingletonNodeSpan(node, true)) - else if n == 0 - then Some(this) - else None - end expandRightOption - - def expandRightMax: NodeSpan = - if !includesMe - then SingletonNodeSpan(node, true) - else this - end expandRightMax - - def replaceThis(nodes: Iterable[Node]): NodeSpan = - require(includesMe, "tried to rewrite an empty orphaned span") - require( - nodes.size == 1, - s"tried to make an orphaned span have a number of nodes other than 1 (${nodes.size})", - ) - SingletonNodeSpan(nodes.head, true) - end replaceThis - - def parentOption: Option[Node] = None - def root: Node = node - end SingletonNodeSpan - - private final class IndexedParentNodeSpan( - parent: Node, - start: Int, - val length: Int, - ) extends NodeSpan: - def apply(i: Int): Node = - parent.children.view - .slice(start, start + length) - .apply(i) - end apply - - protected def sliceImpl(from: Int, until: Int): NodeSpan = - val indicesInParent = parent.children.indices.slice(start, start + length) - val slicedIndices = indicesInParent.slice(from, until) - IndexedParentNodeSpan( - parent, - slicedIndices.headOption.getOrElse( - (start + from).max(start + slicedIndices.length), - ), - slicedIndices.length, - ) - end sliceImpl - - def expandLeftOption(n: Int): Option[NodeSpan] = - if start - n >= 0 - then Some(IndexedParentNodeSpan(parent, start - n, length + n)) - else None - end expandLeftOption - - def expandRightOption(n: Int): Option[NodeSpan] = - if start + length + n <= parent.children.length - then Some(IndexedParentNodeSpan(parent, start, length + n)) - else None - end expandRightOption - - def expandRightMax: NodeSpan = - val fullView = parent.children.view.drop(start) - if fullView.length != length - then IndexedParentNodeSpan(parent, start, fullView.length) - else this - end expandRightMax - - def replaceThis(nodes: Iterable[Node]): NodeSpan = - val token = parent.tokenOption.get - - IndexedParentNodeSpan( - parent = new Node( - impl = NodeImpl.TokenNode( - token = token, - children = parent.children.implSeq - .patch(start, nodes.iterator.map(_.impl), length), - attrs = parent.attrs.implMap, - ), - nodeParentInfo = parent.nodeParentInfo, - ), - start = start, - length = nodes.size, - ) - end replaceThis - - def parentOption: Option[Node] = Some(parent) - def root: Node = parent.root - end IndexedParentNodeSpan -end Node diff --git a/forja/src/Pass.scala b/forja/src/Pass.scala deleted file mode 100644 index b93e4ba..0000000 --- a/forja/src/Pass.scala +++ /dev/null @@ -1,278 +0,0 @@ -package forja - -import forja.Wf.TokenWf -import forja.util.ReflectiveEnumeration - -import scala.annotation.tailrec -import scala.collection.mutable -import scala.compiletime.{asMatchable, deferred} -import scala.concurrent.ExecutionContext - -import Pass.* - -trait Pass extends ReflectiveEnumeration.Enumerable: - final def perform(root: Node): Node = - if root.containsError - then root - else performImpl(root) - end perform - - protected def performImpl(root: Node): Node -end Pass - -object Pass: - trait RewritePass extends Pass, ReflectiveEnumeration: - private lazy val rewritesAgg: Pattern[String] = - valuesByType[Query.rewrite[?]].view - .map: (fieldName, rw) => - rw.pattern.map(_ => fieldName) - .reduceOption(_ | _) - .getOrElse(Pattern.empty) - end rewritesAgg - - final protected def performImpl(node: Node): Node = - @tailrec - def impl( - nodeSpan: NodeSpan, - madeChangesThisIteration: Boolean, - ): NodeSpan = - rewritesAgg.runPattern(nodeSpan) match - case None => - assert(nodeSpan.isEmpty) - def firstChild = nodeSpan - .expandRightOption(1) - .map(_.head.children.asEmptyNodeSpan) - end firstChild - - def firstAvailableParentsSiblingOrRoot( - nodeSpan: NodeSpan, - ): NodeSpan = - nodeSpan.parentOption match - case None => nodeSpan - case Some(parent) => - parent.rightSiblingOption match - case None => - firstAvailableParentsSiblingOrRoot( - parent.emptyNodeSpanHere, - ) - case Some(sibling) => sibling.emptyNodeSpanHere - end firstAvailableParentsSiblingOrRoot - - val nextSpan = - firstChild.getOrElse(firstAvailableParentsSiblingOrRoot(nodeSpan)) - if nextSpan.parentOption.isEmpty - then - if madeChangesThisIteration - then impl(nextSpan, madeChangesThisIteration = false) - else nextSpan - else impl(nextSpan, madeChangesThisIteration) - case Some((_, nodeSpan)) => - impl(nodeSpan.take(0), madeChangesThisIteration = true) - end impl - - val nodeSpan = - impl(node.asOrphan.emptyNodeSpanHere, madeChangesThisIteration = false) - val replacementNode = - nodeSpan.headOption - .getOrElse(nodeSpan.expandRightOption(1).get.head) - // Put our parents back - node.replaceThis(replacementNode) - end performImpl - end RewritePass - - object RewritePass: - trait EmbedGenerator[T <: Matchable] - extends ReflectiveEnumeration.Enumerable: - given embed: Node.Embed[T] = deferred - def generate: Iterator[T] - end EmbedGenerator - - final case class MCState(fieldName: String, node: Node, passNum: Int): - override def toString(): String = - s"$fieldName\n${node.toString()}" - override def hashCode(): Int = node.hashCode() - override def equals(that: Any): Boolean = - that.asMatchable match - case that: MCState => - node.equals(that.node) - case _ => false - end match - end equals - end MCState - - trait ModelChecker extends forja.ModelChecker, ReflectiveEnumeration: - def inputWf: TokenWf - def outputWf: TokenWf - - type State = MCState - type ErrorState = Node - - extension (state: MCState) - final def checkErrorState: Option[ErrorState] = - if !state.node.containsError - then - val errorState = outputWf.validate - .perform(state.node) - if errorState.containsError - then Some(errorState) - else None - else None - end checkErrorState - end extension - - def initTreeLevels: Int - def initRepMax: Int - - private val embedGenerators: Map[Node.Embed[ - ?, - ], (name: String, gen: RewritePass.EmbedGenerator[?])] = - valuesByType[RewritePass.EmbedGenerator[?]].iterator - .map: (name, gen) => - gen.embed -> (name, gen) - .toMap - end embedGenerators - - private val rewritePasses: IArray[(String, RewritePass)] = - valuesByType[RewritePass] - - def initStates(using ExecutionContext): Iterator[MCState] = - type Ident = Token | Node.Embed[?] - - type StructureMap = Map[Ident, List[Node]] - object StructureMap: - def empty: StructureMap = Map.empty - end StructureMap - - type StructureFn = StructureMap => Iterator[Node] - - def buildStructureFn(wf: TokenWf): StructureFn = - val visited = mutable.HashSet[Ident]() - val buf = mutable.ListBuffer[StructureFn]() - def implForWf(wf: TokenWf): Unit = - if !visited(wf.token) - then - visited += wf.token - val fns = wf.stableShapeSeq.shapes - .map: shape => - shape match - case shape: (Wf.EmbedWf[?] | TokenWf | Wf.Choice) => - val fn = implForShape(shape) - fn.andThen(_.map(List(_))) - case shape: Wf.RepeatedShape => - val fn = implForShape(shape.shape) - structureMap => - (0 until initRepMax).iterator - .map: len => - (0 until len).foldLeft(List(Nil): List[List[Node]]): - (prefixes, _) => - prefixes.flatMap: prefix => - fn(structureMap).map(_ :: prefix) - .flatten - case (token: Token, _) => - ??? - end match - val fn: StructureFn = structureMap => - val childLists = fns.foldLeft(List(Nil): List[List[Node]]): - (prefixes, fn) => - prefixes.flatMap: prefix => - fn(structureMap).map(_ ::: prefix) - childLists.iterator - .map: childList => - Node(wf.token, childList.reverse) - buf += fn - end if - end implForWf - def implForShape(shape: Wf.Shape): StructureFn = - shape match - case shape: Wf.EmbedWf[?] => - embedGenerators.get(shape.embed) match - case None => - throw AssertionError( - s"need to define embed generator for ${shape.embed}", - ) - case Some((name, gen: RewritePass.EmbedGenerator[t])) => - structureMap => - gen.generate.map(Node.embed(_)(using gen.embed)) - case shape: Wf.TokenWf => - implForWf(shape) - structureMap => - structureMap - .getOrElse(shape.token, Nil) - .iterator - case shape: Wf.Choice => - val impls = shape.choices - .map(implForShape) - structureMap => impls.iterator.map(_(structureMap)).flatten - end match - end implForShape - implForWf(wf) - - structureMap => - buf.iterator - .map(_(structureMap)) - .flatten - end buildStructureFn - - val structureFn = buildStructureFn(inputWf) - - Iterator - .iterate(StructureMap.empty): structureMap => - structureFn(structureMap).foldLeft(structureMap): - (structureMap, node) => - val token = node.tokenOption.get - structureMap.updated( - token, - node :: structureMap.getOrElse(token, Nil), - ) - .dropWhile(structureMap => !structureMap.contains(inputWf.token)) - .take(initTreeLevels) - .reduce((l, r) => r) - .apply(inputWf.token) - .iterator - .map: node => - MCState("", node, 0) - end initStates - - def nextStates(state: MCState)(using - ExecutionContext, - ): Iterator[MCState] = - if state.passNum == rewritePasses.length - then return Iterator.empty - if state.node.containsError - then return Iterator.empty - - val passNum = state.passNum - val (passName, pass) = rewritePasses(state.passNum) - - def impl(state: Node): Iterator[MCState] = - pass.rewritesAgg - .runPattern(state.emptyNodeSpanHere) - .map: (fieldName, nodeSpan) => - MCState(s"$passName.$fieldName", nodeSpan.root, passNum) - .iterator - ++ state.children.iterator - .flatMap(impl) - ++ state.attrs.iterator - .map(_._2) - .flatMap(impl) - end impl - - val iter = impl(state.node) - if iter.hasNext - then iter - else nextStates(MCState(state.fieldName, state.node, state.passNum + 1)) - end nextStates - end ModelChecker - end RewritePass - - trait MultiPass extends Pass, ReflectiveEnumeration: - private lazy val passes = valuesByType[Pass].map(_._2) - final protected def performImpl(root: Node): Node = - var node = root - passes.foreach: pass => - if !node.containsError - then node = pass.perform(node) - node - end performImpl - end MultiPass -end Pass diff --git a/forja/src/Pattern.scala b/forja/src/Pattern.scala deleted file mode 100644 index e793675..0000000 --- a/forja/src/Pattern.scala +++ /dev/null @@ -1,352 +0,0 @@ -package forja - -import cats.data.Chain - -import scala.annotation.tailrec -import scala.collection.mutable -import scala.compiletime.asMatchable - -import Pattern.* - -sealed abstract class Pattern[+T]: - pattern => - private[forja] def altOptions: Chain[Pattern[T]] = - this match - case pattern: alt[t] => - pattern.left.altOptions ++ pattern.right.altOptions - case pattern => Chain.one(pattern) - end altOptions - - final def unary_+ : Include[T] = Include(pattern) - - final def unary_![U >: T <: Tuple](using ev: T <:< U): Include[Node *: U] = - Include(Pattern.captureNode(pattern).map(p => p._1 *: ev(p._2))) - end unary_! - - final def |[U >: T](other: Pattern[U]) = new alt(pattern, other) - - final def map[U](fn: T => U): Pattern[U] = new map(pattern, fn) - - final def rewrite[T2 >: T <: Matchable]( - fn: syntax.ValueContext.type ?=> Pattern.RWType[T2] => Node | - Iterable[Node] | syntax.unchanged.type, - ): Pattern[Unit] = - rewriteMap[T2, Unit]: t => - ((), fn(t)) - end rewrite - - final def rewriteMap[T2 >: T <: Matchable, U]( - fn: syntax.ValueContext.type ?=> Pattern.RWType[T2] => ( - U, - Node | Iterable[Node] | syntax.unchanged.type, - ), - ): Pattern[U] = - new rewriteMap( - pattern.map(t => RWType.adjust(t: T2)), - fn(using syntax.ValueContext), - ) - end rewriteMap - - final def filter(pred: T => Boolean): Pattern[T] = new filter(pattern, pred) - - final def here[U](using ev: T <:< Node)(query: Query[U]): Pattern[U] = - new here(pattern.map(ev), query) - - def runPattern(nodeSpan: NodeSpan): Option[(T, Node.NodeSpan)] -end Pattern - -object Pattern: - type RWType[T <: Matchable] <: Matchable = T match - case EmptyTuple => Unit - case Tuple1[t] => t & Matchable - case Any => T - end RWType - - object RWType: - def adjust[T <: Matchable](arg: T): RWType[T] = - arg match - case arg: EmptyTuple => () - case arg: Tuple1[t1] => arg._1.asMatchable - case _: Any => arg - end adjust - end RWType - - final class Include[+T](val pattern: Pattern[T]) - - object empty extends Pattern[Nothing]: - def runPattern(nodeSpan: NodeSpan): Option[(Nothing, NodeSpan)] = - None - end empty - - private[forja] final class filter[T]( - val pattern: Pattern[T], - pred: T => Boolean, - ) extends Pattern[T]: - def runPattern(nodeSpan: NodeSpan): Option[(T, NodeSpan)] = - pattern - .runPattern(nodeSpan) - .filter: (value, _) => - pred(value) - end runPattern - end filter - - private[forja] final class captureNode[T](val pattern: Pattern[T]) - extends Pattern[(Node, T)]: - def runPattern(nodeSpan: NodeSpan): Option[((Node, T), NodeSpan)] = - val nodeIdx = nodeSpan.size - pattern - .runPattern(nodeSpan) - .flatMap: (value, nodeSpan) => - nodeSpan - .lift(nodeIdx) - .map: node => - ((node, value), nodeSpan) - end runPattern - end captureNode - - private[forja] final class Tupled[Result <: Tuple]( - tupleArity: Int, - isTotal: Boolean, - val cases: List[Tupled.Case], - ) extends Pattern[Result]: - def runPattern(nodeSpan: NodeSpan): Option[(Result, NodeSpan)] = - import Tupled.* - val resultsArr = Array.ofDim[Any](tupleArity) - @tailrec - def impl( - cases: List[Tupled.Case], - resultIdx: Int, - nodeSpan: NodeSpan, - searchStack: List[ - (resultIdx: Int, nodeSpan: NodeSpan, cases: List[Tupled.Case]), - ], - ): Option[(Result, NodeSpan)] = - inline def failCase: Option[(Result, NodeSpan)] = - searchStack match - case Nil => - None - case hd :: searchStackTl => - impl(hd.cases, hd.resultIdx, hd.nodeSpan, searchStackTl) - end failCase - - cases match - case Nil => - if isTotal && nodeSpan.expandRightMax.size == nodeSpan.size - then - Some((Tuple.fromArray(resultsArr).asInstanceOf[Result], nodeSpan)) - else if !isTotal - then - Some((Tuple.fromArray(resultsArr).asInstanceOf[Result], nodeSpan)) - else failCase - case IncludeTupleCase(pattern) :: casesTl => - pattern.runPattern(nodeSpan) match - case None => failCase - case Some((tpl, nodeSpan)) => - (0 until tpl.size).foreach: i => - resultsArr(resultIdx + i) = tpl.productElement(i) - impl( - casesTl, - resultIdx = resultIdx + tpl.size, - nodeSpan = nodeSpan, - searchStack = searchStack, - ) - case IncludeCase(pattern) :: casesTl => - pattern.runPattern(nodeSpan) match - case None => failCase - case Some((elem, nodeSpan)) => - resultsArr(resultIdx) = elem - impl( - casesTl, - resultIdx = resultIdx + 1, - nodeSpan = nodeSpan, - searchStack = searchStack, - ) - case SkipCase(pattern) :: casesTl => - pattern.runPattern(nodeSpan) match - case None => failCase - case Some((_, nodeSpan)) => - impl( - casesTl, - resultIdx = resultIdx, - nodeSpan = nodeSpan, - searchStack = searchStack, - ) - case WildcardCase :: casesTl => - // If there is a right-ward position to try, add a search stack record - // where this wildcard evaluates there. If just ignoring this wildcard - // makes the pattern fail, then we will retry one spot to the right, - // including seeing this wildcard and adding another stack entry 1 further, - // and so on. The only case we don't retry is if we are at end of seq, - // and no right-ward step is possible. - val nextSearchStack = - nodeSpan.expandRightOption(1) match - case None => searchStack - case Some(nextNodeSpan) => - ( - resultIdx = resultIdx, - nodeSpan = nextNodeSpan, - cases = cases, - ) :: searchStack - end nextSearchStack - impl( - casesTl, - resultIdx = resultIdx, - nodeSpan = nodeSpan, - searchStack = nextSearchStack, - ) - end impl - - impl(cases, resultIdx = 0, nodeSpan = nodeSpan, searchStack = Nil) - end runPattern - end Tupled - - private[forja] object Tupled: - sealed trait Case - - final case class IncludeTupleCase[T <: Tuple](pattern: Pattern[T]) - extends Case - final case class IncludeCase[T](pattern: Pattern[T]) extends Case - final case class SkipCase[T](pattern: Pattern[T]) extends Case - object WildcardCase extends Case - end Tupled - - private[forja] final class alt[T](val left: Pattern[T], val right: Pattern[T]) - extends Pattern[T]: - private lazy val decisionTree = Query.DecisionTree.fromPattern(this) - def runPattern(nodeSpan: NodeSpan): Option[(T, NodeSpan)] = - // decisionTree.run(nodeSpan, _.runPattern(nodeSpan, left)) - left.runPattern(nodeSpan).orElse(right.runPattern(nodeSpan)) - end runPattern - end alt - - private[forja] final class map[T, U](val pattern: Pattern[T], val fn: T => U) - extends Pattern[U]: - def runPattern(nodeSpan: NodeSpan): Option[(U, NodeSpan)] = - pattern - .runPattern(nodeSpan) - .map: (value, nodeSpan) => - (fn(value), nodeSpan) - end runPattern - // FIXME: delete - override def toString(): String = pattern.toString() - end map - - private[forja] final class tokenExact[T]( - val token: Token, - val pattern: Pattern[T], - ) extends Pattern[T]: - def runPattern(nodeSpan: NodeSpan): Option[(T, NodeSpan)] = - val sizeToLeft = nodeSpan.size - nodeSpan.expandRightOption(1) match - case Some(nodeSpan) if nodeSpan.last.tokenOption.contains(token) => - pattern - .runPattern(nodeSpan.last.children.asEmptyNodeSpan) - .flatMap: (value, nodeSpan) => - nodeSpan.parentOption - .map(_.singletonNodeSpanHere) - .flatMap(_.expandLeftOption(sizeToLeft)) - .map((value, _)) - case None | Some(_) => None - end runPattern - end tokenExact - - private[forja] final class tokenAny[T](val pattern: Pattern[T]) - extends Pattern[T]: - def runPattern(nodeSpan: NodeSpan): Option[(T, NodeSpan)] = - val sizeToLeft = nodeSpan.size - nodeSpan.expandRightOption(1) match - case None => None - case Some(nodeSpan) => - pattern - .runPattern(nodeSpan.last.children.asEmptyNodeSpan) - .flatMap: (value, nodeSpan) => - nodeSpan.parentOption - .map(_.singletonNodeSpanHere) - .flatMap(_.expandLeftOption(sizeToLeft)) - .map((value, _)) - end runPattern - end tokenAny - - private[forja] final class not[T](val pattern: Pattern[T]) - extends Pattern[Unit]: - def runPattern(nodeSpan: NodeSpan): Option[(Unit, NodeSpan)] = - pattern.runPattern(nodeSpan) match - case None => Some(((), nodeSpan)) - case Some(_) => None - end runPattern - end not - - private[forja] final class rewriteMap[T, U]( - val pattern: Pattern[T], - val fn: T => (U, Node | Iterable[Node] | syntax.unchanged.type), - ) extends Pattern[U]: - def runPattern(nodeSpan: NodeSpan): Option[(U, NodeSpan)] = - val elementsToTheLeft = nodeSpan.size - pattern - .runPattern(nodeSpan.drop(elementsToTheLeft)) - .map: (value, nodeSpan) => - val result = fn(value) match - case (u, node: Node) => - (u, nodeSpan.replaceThis(List(node))) - case (u, nodes: Iterable[Node]) => - (u, nodeSpan.replaceThis(nodes)) - case (u, syntax.unchanged) => - (u, nodeSpan) - - (result._1, result._2.expandLeftOption(elementsToTheLeft).get) - end runPattern - end rewriteMap - - private[forja] final class rep[T](val elem: Pattern[T]) - extends Pattern[List[T]]: - def runPattern(nodeSpan: NodeSpan): Option[(List[T], NodeSpan)] = - var shouldContinue = true - var nodeSpanAcc = nodeSpan - val resultAcc = mutable.ListBuffer[T]() - - while shouldContinue - do - elem.runPattern(nodeSpanAcc) match - case None => - shouldContinue = false - case Some((elem, nodeSpanAcc2)) => - resultAcc += elem - nodeSpanAcc = nodeSpanAcc2 - end while - - Some((resultAcc.result(), nodeSpanAcc)) - end runPattern - end rep - - private[forja] final class embed[T: Node.Embed] extends Pattern[T]: - def runPattern(nodeSpan: NodeSpan): Option[(T, NodeSpan)] = - nodeSpan - .expandRightOption(1) - .flatMap: nodeSpan => - nodeSpan.last.valueOption[T].map((_, nodeSpan)) - end runPattern - end embed - - private[forja] final class EmbedLiteralPattern[T: Node.Embed](value: T) - extends Pattern[T]: - def runPattern(nodeSpan: NodeSpan): Option[(T, NodeSpan)] = - nodeSpan - .expandRightOption(1) - .flatMap: nodeSpan => - nodeSpan.last.valueOption[T] match - case Some(`value`) => - Some((value, nodeSpan)) - case None | Some(_) => None - end runPattern - end EmbedLiteralPattern - - private[forja] final class here[T](pattern: Pattern[Node], query: Query[T]) - extends Pattern[T]: - def runPattern(nodeSpan: NodeSpan): Option[(T, NodeSpan)] = - pattern - .runPattern(nodeSpan) - .flatMap: (node, nodeSpan) => - query.runQuery(node).value.map((_, nodeSpan)) - end runPattern - end here -end Pattern diff --git a/forja/src/Prod.scala b/forja/src/Prod.scala deleted file mode 100644 index 55936fc..0000000 --- a/forja/src/Prod.scala +++ /dev/null @@ -1,303 +0,0 @@ -package forja - -import scala.deriving.Mirror -import scala.compiletime.ops.int.`+` -import scala.util.NotGiven -import scala.reflect.ClassTag -import scala.reflect.TypeTest -import scala.reflect.Typeable -import scala.compiletime.deferred -import scala.quoted.Type -import scala.quoted.Quotes -import scala.quoted.Expr -import forja.util.TupleOf -import java.util.concurrent.atomic.AtomicInteger -import scala.compiletime.summonFrom -import scala.compiletime.summonInline -import scala.compiletime.erasedValue -import scala.compiletime.asMatchable -import izumi.reflect.Tag -import scala.annotation.publicInBinary - -into sealed abstract class Prod[T]: - private val epoch = Prod.epoch.get() - - override def equals(obj: Any): Boolean = ??? - - def tag: Tag[T] - - def reify(): T - - final def rewritePre(rw: Prod.Rewrite): Prod[T] = - rw.rewrite(this).rewriteChildren([u] => uu => uu.rewritePre(rw)) - end rewritePre - - final def rewritePost(rw: Prod.Rewrite): Prod[T] = - rw.rewrite(rewriteChildren([u] => uu => uu.rewritePost(rw))) - end rewritePost - - final def fixpoint(rw: Prod.Rewrite): Prod[T] = - def impl[T](self: Prod[T], rw: Prod.Rewrite, fromEpoch: Int): Prod[T] = - if self.epoch >= fromEpoch - then - val nextUnstableEpoch = Prod.epoch.incrementAndGet() - var currSelf = self - while - val prevSelf = currSelf - currSelf = rw.rewrite(currSelf) - prevSelf ne currSelf - do () - currSelf = currSelf.rewriteChildren([u] => uu => impl(uu, rw, fromEpoch)) - if currSelf ne self - then impl(currSelf, rw, fromEpoch = nextUnstableEpoch) - else self - else self - end impl - - // Start epoch at -1 for unconditional scan. - // Rescans from there will only account for nodes created on or after the original epoch. - // That is, we will only look at nodes that have any possibility of coming from a rewrite - // we just performed. - impl(this, rw, fromEpoch = -1) - end fixpoint - - protected def rewriteChildren(fn: [U] => Prod[U] => Prod[U]): Prod[T] - - protected def unapplyLifted[U](ops: Prod.LiftableOps[U]): Option[ops.U] - - def cast[U : Tag]: Prod[U] - - final def upcast[U >: T : Tag]: Prod[U] = cast[U] -end Prod - -object Prod: - private val epoch = AtomicInteger(0) - - given lift: [T] => (liftable: Liftable[T]) => Conversion[T, Prod[T]]: - def apply(t: T): Prod[T] = liftable.lift(t) - end lift - - def apply[T](using ops: LiftableOps.Aux[T, EmptyTuple])(): Prod[T] = - Lifted(ops, EmptyTuple) - end apply - - given upcast: [T : Tag, U <: T] => Conversion[Prod[U], Prod[T]]: - def apply(prod: Prod[U]): Prod[T] = prod.upcast[T] - end upcast - - type StripTuple1[U] = U match - case Tuple1[u] => u - case _ => U - end StripTuple1 - - inline def apply[T](using ops: LiftableOps[T])(u: StripTuple1[ops.U]): Prod[T] = - inline erasedValue[ops.U & Matchable] match - case _: Tuple1[_] => applyImpl(Tuple1(u).asInstanceOf) - case _ => applyImpl(u.asInstanceOf) - end match - end apply - - private def applyImpl[T](using ops: LiftableOps[T])(u: ops.U): Prod[T] = - Lifted(ops, u) - end applyImpl - - inline def unapply[T](prod: Prod[?])(using ops: LiftableOps[T]): Option[StripTuple1[ops.U]] = - prod.unapplyLifted(ops).map: x => - x.asMatchable match - case Tuple1(u) => u.asInstanceOf[StripTuple1[ops.U]] - case u => u.asInstanceOf[StripTuple1[ops.U]] - end match - end unapply - - final case class CannotReify(prod: Prod[?]) extends RuntimeException(s"cannot reify $prod") - - trait Rewrite: - def rewrite[U](prod: Prod[U]): Prod[U] - end Rewrite - - object Rewrite: - def rw[T : Tag](fn: PartialFunction[Prod[T], Prod[T]]): Rewrite = - Rewrite.PartialFunctionRewrite(fn) - end rw - - final class PartialFunctionRewrite[T : Tag](fn: PartialFunction[Prod[T], Prod[T]]) extends Rewrite: - def rewrite[U](prod: Prod[U]): Prod[U] = - if prod.tag <:< Tag[T] - then fn - .asInstanceOf[PartialFunction[Prod[U], Prod[U]]] - .applyOrElse(prod, identity) - else prod - end rewrite - end PartialFunctionRewrite - - final class RewriteSeq(rewrites: Seq[Rewrite]) extends Rewrite: - def rewrite[U](prod: Prod[U]): Prod[U] = - rewrites.foldLeft(prod)((prod, rw) => rw.rewrite(prod)) - end rewrite - end RewriteSeq - end Rewrite - - private final class Reject[T](val tag: Tag[T], val message: String) extends Prod[T]: - def reify(): T = throw CannotReify(this) - protected def rewriteChildren(fn: [U] => Prod[U] => Prod[U]): Prod[T] = this - protected def unapplyLifted[U](ops: LiftableOps[U]): Option[ops.U] = None - def cast[U: Tag]: Prod[U] = Reject(Tag[U], message) - end Reject - - private final class Lifted[T, U](val ops: LiftableOps.Aux[T, U], val u: U) extends Prod[T]: - def tag = ops.tag - def reify(): T = ops.reify(u) - protected def rewriteChildren(fn: [U] => (x: Prod[U]) => Prod[U]): Prod[T] = - val rw = ops.rewriteChildren(u, fn) - if rw.asInstanceOf[AnyRef] ne u.asInstanceOf[AnyRef] - then Lifted(ops, rw) - else this - end rewriteChildren - protected def unapplyLifted[V](ops: LiftableOps[V]): Option[ops.U] = - if this.ops.tag <:< ops.tag - then Some(u.asInstanceOf[ops.U]) - else None - end unapplyLifted - def cast[U: Tag]: Prod[U] = - if ops.tag =:= Tag[U] - then this.asInstanceOf - else if ops.tag <:< Tag[U] - then Super(Tag[U], this.asInstanceOf) - else Cast(Tag[U], this) - end cast - end Lifted - - private final class Super[T, V <: T](val tag: Tag[T], val prod: Prod[V]) extends Prod[T]: - def reify(): T = prod.reify() - protected def rewriteChildren(fn: [U] => Prod[U] => Prod[U]): Prod[T] = - val rw = fn[V](prod) - if rw ne prod - then rw.upcast[T](using tag) - else this - end rewriteChildren - export prod.unapplyLifted - def cast[U: Tag]: Prod[U] = - if Tag[U] == tag - then this.asInstanceOf - else prod.cast[U] - end cast - end Super - - private final class Cast[T, V](val tag: Tag[T], val prod: Prod[V]) extends Prod[T]: - def reify(): T = throw CannotReify(this) - protected def rewriteChildren(fn: [U] => Prod[U] => Prod[U]): Prod[T] = - val rw = fn[V](prod) - if rw ne prod - then rw.cast[T](using tag) - else this - end rewriteChildren - export prod.unapplyLifted - def cast[U: Tag]: Prod[U] = - if Tag[U] == tag - then this.asInstanceOf - else prod.cast[U] - end cast - end Cast - - trait Liftable[T]: - def lift(t: T): Prod[T] - end Liftable - - object Liftable: - transparent inline def derived[T : Tag](using mirror: Mirror.Of[T])(using =>TupleOf[Tuple.Map[mirror.MirroredElemTypes, Liftable]]): Liftable[T] | LiftableWithOps.Aux[T, Tuple.Map[mirror.MirroredElemTypes, Prod]] = - inline mirror match - case mirror: Mirror.ProductOf[t & Product] => - liftableProductOf[t & Product](using mirror) - .asInstanceOf[LiftableWithOps.Aux[T, Tuple.Map[mirror.MirroredElemTypes, Prod]]] - case mirror: Mirror.SumOf[T] => - liftableSumOf[T](using mirror) - end match - end derived - end Liftable - - trait LiftableOps[T]: - type U - def reify(u: U): T - def rewriteChildren(u: U, fn: [u] => Prod[u] => Prod[u]): U - given tag: Tag[T] = deferred - end LiftableOps - - object LiftableOps: - type Aux[T, U0] = LiftableOps[T] { - type U = U0 - } - end LiftableOps - - trait LiftableWithOps[T] extends Liftable[T], LiftableOps[T] - - object LiftableWithOps: - type Aux[T, U0] = LiftableWithOps[T] { - type U = U0 - } - end LiftableWithOps - - trait IdentityLiftable[T] extends LiftableWithOps[T]: - type U = T - def lift(t: T) = Lifted(this, t) - def reify(u: U): T = u - def rewriteChildren(u: U, fn: [u] => Prod[u] => Prod[u]): U = u - end IdentityLiftable - - given identityLiftableByte: IdentityLiftable[Byte] {} - given identityLiftableInt: IdentityLiftable[Int] {} - given identityLiftableLong: IdentityLiftable[Long] {} - given identityLiftableShort: IdentityLiftable[Short] {} - given identityLiftableDouble: IdentityLiftable[Double] {} - given identityLiftableFloat: IdentityLiftable[Float] {} - given identityLiftableString: IdentityLiftable[String] {} - - given liftableProductOf: [T <: Product : Tag] => (mirror: Mirror.ProductOf[T]) => (innerLiftables: =>TupleOf[Tuple.Map[mirror.MirroredElemTypes, Liftable]]) => LiftableWithOps.Aux[T, Tuple.Map[mirror.MirroredElemTypes, Prod]] = - new LiftableWithOps[T]: - type U = Tuple.Map[mirror.MirroredElemTypes, Prod] - def lift(t: T): Prod[T] = - val elems = t - .productIterator - .zip(innerLiftables.value.productIterator.asInstanceOf[Iterator[Liftable[Any]]]) - .map: (elem, liftable) => - liftable.lift(elem) - .toArray - end elems - Lifted(this, Tuple.fromArray(elems).asInstanceOf[U]) - end lift - def reify(u: U): T = - mirror.fromTuple(scala.runtime.Tuples.map(u, [t] => tt => tt.asInstanceOf[Prod[Any]].reify().asInstanceOf).asInstanceOf[mirror.MirroredElemTypes]) - end reify - def rewriteChildren(u: U, fn: [u] => Prod[u] => Prod[u]): U = - val elems = u - .toArray - .mapInPlace(x => fn[Object](x.asInstanceOf[Prod[Object]])) - end elems - if elems.iterator.zipWithIndex.exists((p, i) => p ne u.productElement(i).asInstanceOf[Object]) - then Tuple.fromArray(elems).asInstanceOf[U] - else u - end rewriteChildren - end new - end liftableProductOf - - given liftableSumOf: [T : Tag] => (mirror: Mirror.SumOf[T]) => (innerLiftables: =>TupleOf[Tuple.Map[mirror.MirroredElemTypes, Liftable]]) => Liftable[T]: - def lift(t: T): Prod[T] = - innerLiftables - .value - .productElement(mirror.ordinal(t)) - .asInstanceOf[Liftable[T]] - .lift(t) - .cast[T] - end lift - end liftableSumOf - - given liftableList: [T] => Tag[List[T]] => (liftable: Liftable[T]) => LiftableWithOps.Aux[List[T], List[Prod[T]]] = - new LiftableWithOps[List[T]]: - type U = List[Prod[T]] - def lift(t: List[T]): Prod[List[T]] = Lifted(this, t.map(elem => liftable.lift(elem))) - def reify(u: List[Prod[T]]): List[T] = u.map(_.reify()) - def rewriteChildren(u: List[Prod[T]], fn: [u] => Prod[u] => Prod[u]): U = - u.mapConserve(_.rewriteChildren(fn)) - end rewriteChildren - end new - end liftableList -end Prod diff --git a/forja/src/Query.scala b/forja/src/Query.scala deleted file mode 100644 index ab30b18..0000000 --- a/forja/src/Query.scala +++ /dev/null @@ -1,403 +0,0 @@ -package forja - -import cats.data.Chain -import cats.{Alternative, Eval, Foldable} - -import forja.util.ReflectiveEnumeration - -import scala.collection.mutable -import scala.reflect.TypeTest - -import Query.* - -sealed trait Query[+T]: - query => - private[Query] final def altOptions: Chain[Query[T]] = - query match - case query: alt[u] => - query.left.altOptions ++ query.right.altOptions - case _ => Chain.one(query) - end altOptions - - final infix def |[U >: T](other: Query[U]): Query[U] = new alt(query, other) - - final def map[U](fn: T => U): Query[U] = new map(query, fn) - - final def cast[TT >: T, U](using U <:< TT): Query[U] = query.asInstanceOf - - final def here[TT >: T, U](at: Query[U])(using ev: TT <:< Node): Query[U] = - new here(query.cast(using ev), at) - - final def runQuery(node: Node): Eval[Option[T]] = - runQueryImpl(node) - - protected def runQueryImpl(node: Node): Eval[Option[T]] -end Query - -object Query: - given alternative: Alternative[Query]: - def ap[A, B](ff: Query[A => B])(fa: Query[A]): Query[B] = new ap(ff, fa) - def combineK[A](x: Query[A], y: Query[A]): Query[A] = x | y - def empty[A]: Query[A] = Query.empty - def pure[A](x: A): Query[A] = Query.pure(x) - end alternative - - object empty extends Query[Nothing]: - protected def runQueryImpl(node: Node): Eval[Option[Nothing]] = - Eval.now(None) - end runQueryImpl - end empty - - final class pure[T](value: T) extends Query[T]: - protected def runQueryImpl(node: Node): Eval[Option[T]] = - Eval.now(Some(value)) - end runQueryImpl - end pure - - final class on[+T](val pattern: Pattern[T]) extends Query[T]: - protected def runQueryImpl(node: Node): Eval[Option[T]] = - Eval.now(pattern.runPattern(node.emptyNodeSpanHere).map(_._1)) - end runQueryImpl - - def rewrite[U >: T <: Matchable]( - fn: syntax.ValueContext.type ?=> Pattern.RWType[U] => on.ResultType, - ): rewrite[U] = - new rewrite(pattern, fn) - end rewrite - - def rewriteInPattern[U >: T <: Matchable]: rewrite[U] = - rewrite: _ => - syntax.unchanged - end on - - object on: - type ResultType = Node | Iterable[Node] | syntax.unchanged.type - private type C = syntax.PatternContext.type - private given C = syntax.PatternContext - - inline def applyTupled[Tp <: Tuple, U](arg: C ?=> Tp)(using - app: syntax.PatternContext.NodeSpanApply[Tp, Pattern[U]], - ): on[U] = - new on[U](app(arg)) - - // format: off - inline def apply[U]()(using app: syntax.PatternContext.NodeSpanApply[EmptyTuple, Pattern[U]]): on[U] = new on[U](app(EmptyTuple)) - inline def apply[T1, U](t1: C ?=> T1)(using app: syntax.PatternContext.NodeSpanApply[Tuple1[T1], Pattern[U]]): on[U] = new on[U](app(Tuple1(t1))) - // %%replicate22 - inline def apply[T1, T2, U](t1: C ?=> T1, t2: C ?=> T2)(using app: syntax.PatternContext.NodeSpanApply[(T1, T2), Pattern[U]]): on[U] = new on[U](app((t1, t2))) - inline def apply[T1, T2, T3, U](t1: C ?=> T1, t2: C ?=> T2, t3: C ?=> T3)(using app: syntax.PatternContext.NodeSpanApply[(T1, T2, T3), Pattern[U]]): on[U] = new on[U](app((t1, t2, t3))) - inline def apply[T1, T2, T3, T4, U](t1: C ?=> T1, t2: C ?=> T2, t3: C ?=> T3, t4: C ?=> T4)(using app: syntax.PatternContext.NodeSpanApply[(T1, T2, T3, T4), Pattern[U]]): on[U] = new on[U](app((t1, t2, t3, t4))) - inline def apply[T1, T2, T3, T4, T5, U](t1: C ?=> T1, t2: C ?=> T2, t3: C ?=> T3, t4: C ?=> T4, t5: C ?=> T5)(using app: syntax.PatternContext.NodeSpanApply[(T1, T2, T3, T4, T5), Pattern[U]]): on[U] = new on[U](app((t1, t2, t3, t4, t5))) - inline def apply[T1, T2, T3, T4, T5, T6, U](t1: C ?=> T1, t2: C ?=> T2, t3: C ?=> T3, t4: C ?=> T4, t5: C ?=> T5, t6: C ?=> T6)(using app: syntax.PatternContext.NodeSpanApply[(T1, T2, T3, T4, T5, T6), Pattern[U]]): on[U] = new on[U](app((t1, t2, t3, t4, t5, t6))) - inline def apply[T1, T2, T3, T4, T5, T6, T7, U](t1: C ?=> T1, t2: C ?=> T2, t3: C ?=> T3, t4: C ?=> T4, t5: C ?=> T5, t6: C ?=> T6, t7: C ?=> T7)(using app: syntax.PatternContext.NodeSpanApply[(T1, T2, T3, T4, T5, T6, T7), Pattern[U]]): on[U] = new on[U](app((t1, t2, t3, t4, t5, t6, t7))) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, U](t1: C ?=> T1, t2: C ?=> T2, t3: C ?=> T3, t4: C ?=> T4, t5: C ?=> T5, t6: C ?=> T6, t7: C ?=> T7, t8: C ?=> T8)(using app: syntax.PatternContext.NodeSpanApply[(T1, T2, T3, T4, T5, T6, T7, T8), Pattern[U]]): on[U] = new on[U](app((t1, t2, t3, t4, t5, t6, t7, t8))) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, U](t1: C ?=> T1, t2: C ?=> T2, t3: C ?=> T3, t4: C ?=> T4, t5: C ?=> T5, t6: C ?=> T6, t7: C ?=> T7, t8: C ?=> T8, t9: C ?=> T9)(using app: syntax.PatternContext.NodeSpanApply[(T1, T2, T3, T4, T5, T6, T7, T8, T9), Pattern[U]]): on[U] = new on[U](app((t1, t2, t3, t4, t5, t6, t7, t8, t9))) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, U](t1: C ?=> T1, t2: C ?=> T2, t3: C ?=> T3, t4: C ?=> T4, t5: C ?=> T5, t6: C ?=> T6, t7: C ?=> T7, t8: C ?=> T8, t9: C ?=> T9, t10: C ?=> T10)(using app: syntax.PatternContext.NodeSpanApply[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10), Pattern[U]]): on[U] = new on[U](app((t1, t2, t3, t4, t5, t6, t7, t8, t9, t10))) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, U](t1: C ?=> T1, t2: C ?=> T2, t3: C ?=> T3, t4: C ?=> T4, t5: C ?=> T5, t6: C ?=> T6, t7: C ?=> T7, t8: C ?=> T8, t9: C ?=> T9, t10: C ?=> T10, t11: C ?=> T11)(using app: syntax.PatternContext.NodeSpanApply[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11), Pattern[U]]): on[U] = new on[U](app((t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11))) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, U](t1: C ?=> T1, t2: C ?=> T2, t3: C ?=> T3, t4: C ?=> T4, t5: C ?=> T5, t6: C ?=> T6, t7: C ?=> T7, t8: C ?=> T8, t9: C ?=> T9, t10: C ?=> T10, t11: C ?=> T11, t12: C ?=> T12)(using app: syntax.PatternContext.NodeSpanApply[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12), Pattern[U]]): on[U] = new on[U](app((t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12))) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, U](t1: C ?=> T1, t2: C ?=> T2, t3: C ?=> T3, t4: C ?=> T4, t5: C ?=> T5, t6: C ?=> T6, t7: C ?=> T7, t8: C ?=> T8, t9: C ?=> T9, t10: C ?=> T10, t11: C ?=> T11, t12: C ?=> T12, t13: C ?=> T13)(using app: syntax.PatternContext.NodeSpanApply[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13), Pattern[U]]): on[U] = new on[U](app((t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13))) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, U](t1: C ?=> T1, t2: C ?=> T2, t3: C ?=> T3, t4: C ?=> T4, t5: C ?=> T5, t6: C ?=> T6, t7: C ?=> T7, t8: C ?=> T8, t9: C ?=> T9, t10: C ?=> T10, t11: C ?=> T11, t12: C ?=> T12, t13: C ?=> T13, t14: C ?=> T14)(using app: syntax.PatternContext.NodeSpanApply[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14), Pattern[U]]): on[U] = new on[U](app((t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14))) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, U](t1: C ?=> T1, t2: C ?=> T2, t3: C ?=> T3, t4: C ?=> T4, t5: C ?=> T5, t6: C ?=> T6, t7: C ?=> T7, t8: C ?=> T8, t9: C ?=> T9, t10: C ?=> T10, t11: C ?=> T11, t12: C ?=> T12, t13: C ?=> T13, t14: C ?=> T14, t15: C ?=> T15)(using app: syntax.PatternContext.NodeSpanApply[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15), Pattern[U]]): on[U] = new on[U](app((t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15))) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, U](t1: C ?=> T1, t2: C ?=> T2, t3: C ?=> T3, t4: C ?=> T4, t5: C ?=> T5, t6: C ?=> T6, t7: C ?=> T7, t8: C ?=> T8, t9: C ?=> T9, t10: C ?=> T10, t11: C ?=> T11, t12: C ?=> T12, t13: C ?=> T13, t14: C ?=> T14, t15: C ?=> T15, t16: C ?=> T16)(using app: syntax.PatternContext.NodeSpanApply[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16), Pattern[U]]): on[U] = new on[U](app((t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15, t16))) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, U](t1: C ?=> T1, t2: C ?=> T2, t3: C ?=> T3, t4: C ?=> T4, t5: C ?=> T5, t6: C ?=> T6, t7: C ?=> T7, t8: C ?=> T8, t9: C ?=> T9, t10: C ?=> T10, t11: C ?=> T11, t12: C ?=> T12, t13: C ?=> T13, t14: C ?=> T14, t15: C ?=> T15, t16: C ?=> T16, t17: C ?=> T17)(using app: syntax.PatternContext.NodeSpanApply[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17), Pattern[U]]): on[U] = new on[U](app((t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15, t16, t17))) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, U](t1: C ?=> T1, t2: C ?=> T2, t3: C ?=> T3, t4: C ?=> T4, t5: C ?=> T5, t6: C ?=> T6, t7: C ?=> T7, t8: C ?=> T8, t9: C ?=> T9, t10: C ?=> T10, t11: C ?=> T11, t12: C ?=> T12, t13: C ?=> T13, t14: C ?=> T14, t15: C ?=> T15, t16: C ?=> T16, t17: C ?=> T17, t18: C ?=> T18)(using app: syntax.PatternContext.NodeSpanApply[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18), Pattern[U]]): on[U] = new on[U](app((t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15, t16, t17, t18))) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, U](t1: C ?=> T1, t2: C ?=> T2, t3: C ?=> T3, t4: C ?=> T4, t5: C ?=> T5, t6: C ?=> T6, t7: C ?=> T7, t8: C ?=> T8, t9: C ?=> T9, t10: C ?=> T10, t11: C ?=> T11, t12: C ?=> T12, t13: C ?=> T13, t14: C ?=> T14, t15: C ?=> T15, t16: C ?=> T16, t17: C ?=> T17, t18: C ?=> T18, t19: C ?=> T19)(using app: syntax.PatternContext.NodeSpanApply[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19), Pattern[U]]): on[U] = new on[U](app((t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15, t16, t17, t18, t19))) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, U](t1: C ?=> T1, t2: C ?=> T2, t3: C ?=> T3, t4: C ?=> T4, t5: C ?=> T5, t6: C ?=> T6, t7: C ?=> T7, t8: C ?=> T8, t9: C ?=> T9, t10: C ?=> T10, t11: C ?=> T11, t12: C ?=> T12, t13: C ?=> T13, t14: C ?=> T14, t15: C ?=> T15, t16: C ?=> T16, t17: C ?=> T17, t18: C ?=> T18, t19: C ?=> T19, t20: C ?=> T20)(using app: syntax.PatternContext.NodeSpanApply[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20), Pattern[U]]): on[U] = new on[U](app((t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15, t16, t17, t18, t19, t20))) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, U](t1: C ?=> T1, t2: C ?=> T2, t3: C ?=> T3, t4: C ?=> T4, t5: C ?=> T5, t6: C ?=> T6, t7: C ?=> T7, t8: C ?=> T8, t9: C ?=> T9, t10: C ?=> T10, t11: C ?=> T11, t12: C ?=> T12, t13: C ?=> T13, t14: C ?=> T14, t15: C ?=> T15, t16: C ?=> T16, t17: C ?=> T17, t18: C ?=> T18, t19: C ?=> T19, t20: C ?=> T20, t21: C ?=> T21)(using app: syntax.PatternContext.NodeSpanApply[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21), Pattern[U]]): on[U] = new on[U](app((t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15, t16, t17, t18, t19, t20, t21))) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22, U](t1: C ?=> T1, t2: C ?=> T2, t3: C ?=> T3, t4: C ?=> T4, t5: C ?=> T5, t6: C ?=> T6, t7: C ?=> T7, t8: C ?=> T8, t9: C ?=> T9, t10: C ?=> T10, t11: C ?=> T11, t12: C ?=> T12, t13: C ?=> T13, t14: C ?=> T14, t15: C ?=> T15, t16: C ?=> T16, t17: C ?=> T17, t18: C ?=> T18, t19: C ?=> T19, t20: C ?=> T20, t21: C ?=> T21, t22: C ?=> T22)(using app: syntax.PatternContext.NodeSpanApply[(T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22), Pattern[U]]): on[U] = new on[U](app((t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15, t16, t17, t18, t19, t20, t21, t22))) - // format: on - end on - - final class rewrite[T <: Matchable]( - srcPattern: Pattern[T], - fn: syntax.ValueContext.type ?=> Pattern.RWType[T] => Node | - Iterable[Node] | syntax.unchanged.type, - ) extends ReflectiveEnumeration.Enumerable: - val pattern = srcPattern.rewrite(fn) - end rewrite - - private final class map[T, U](val query: Query[T], fn: T => U) - extends Query[U]: - protected def runQueryImpl(node: Node): Eval[Option[U]] = - Eval.defer(query.runQuery(node)).map(_.map(fn)) - end runQueryImpl - end map - - private final class ap[A, B](val ff: Query[A => B], val fa: Query[A]) - extends Query[B]: - protected def runQueryImpl(node: Node): Eval[Option[B]] = - Eval - .defer(ff.runQuery(node)) - .flatMap: - case None => Eval.now(None) - case Some(ff) => - Eval.defer(fa.runQuery(node)).map(_.map(ff)) - end runQueryImpl - end ap - - private final class here[T](val place: Query[Node], val query: Query[T]) - extends Query[T]: - protected def runQueryImpl(node: Node): Eval[Option[T]] = - place - .runQuery(node) - .flatMap: - case Some(node) => - query.runQuery(node) - case None => Eval.now(None) - end runQueryImpl - end here - - private final class alt[T](val left: Query[T], val right: Query[T]) - extends Query[T]: - import DecisionTree.* - private lazy val decisionTree: FrozenRecordList[T, Query] = - DecisionTree.fromQuery(this) - - protected def runQueryImpl(node: Node): Eval[Option[T]] = - decisionTree.run(node.emptyNodeSpanHere, _.runQuery(node)) - end runQueryImpl - end alt - - private[forja] object DecisionTree: - private def scanPattern[T, Q[_] <: Query[?] | Pattern[?]]( - buf: RecordList[T, Q], - pattern: Pattern[?], - ): Chain[Either[RecordList[T, Q], RecordList[T, Q]]] = - pattern match - case Pattern.empty => - ??? - case pattern: Pattern.Tupled[?] => - ??? - // pattern.elems.foldLeft( - // Chain.one(Right(buf): Either[RecordList[T, Q], RecordList[T, Q]]), - // ): (acc, elem) => - // acc.flatMap: - // case Left(buf) => Chain.one(Left(buf)) - // case Right(buf) => - // elem match - // case pattern: Pattern[?] => - // scanPattern(buf, pattern) - // case include: Pattern.Include[?] => - // scanPattern(buf, include.pattern) - // case (_: Token, _) => - // // attrs have no impact on decision tree - // Chain.one(Right(buf)) - case pattern: Pattern.alt[?] => - scanPattern(buf, pattern.left) - ++ scanPattern(buf, pattern.right) - case pattern: Pattern.map[?, ?] => - scanPattern(buf, pattern.pattern) - case pattern: Pattern.tokenExact[?] => - Chain.one(Right(buf.ensureBranch.upsert(pattern.token))) - case _: (Pattern.tokenAny[?] | Pattern.rep[?] | - Pattern.rewriteMap[?, ?]) => - Chain.one(Left(buf)) - case _: Pattern.embed[?] => - Chain.one(Left(buf)) - case pattern: Pattern.EmbedLiteralPattern[?] => - // TODO: add embeds to tree - Chain.one(Left(buf)) - case pattern: Pattern.filter[?] => - scanPattern(buf, pattern.pattern) - case pattern: Pattern.captureNode[?] => - scanPattern(buf, pattern.pattern) - case _: Pattern.not[?] => - Chain.one(Right(buf)) - case pattern: Pattern.here[?] => - scanPattern(buf, pattern).map(_.forceLeft) - end scanPattern - - private def scanQuery[T, Q[_] <: Query[?] | Pattern[?]]( - buf: RecordList[T, Q], - query: Query[?], - ): Chain[Either[RecordList[T, Q], RecordList[T, Q]]] = - query match - case Query.empty | (_: pure[?]) | Query.leftSibling | - Query.rightSibling | Query.parent | (_: NodeAccessorQuery[?]) => - Chain.one(Right(buf)) - case query: on[?] => - scanPattern(buf, query.pattern) - case query: map[?, ?] => - scanQuery(buf, query.query) - case query: ap[?, ?] => - val left = scanQuery(buf, query.ff) - val right = scanQuery(buf, query.fa) - val leftMoved = left.exists(_.both ne buf) - val rightMoved = right.exists(_.both ne buf) - val hasLeft = left.exists(_.isLeft) || right.exists(_.isLeft) - - // If both sides "moved", then don't know what to do. - // Exclude ourselves from decision tree at this point. - if leftMoved && rightMoved - then Chain.one(Left(buf)) - else if leftMoved - then if hasLeft then left.map(_.forceLeft) else left - else if hasLeft then right.map(_.forceLeft) - else right - case query: here[?] => - scanQuery(buf, query.place).map(_.forceLeft) - case query: alt[?] => - scanQuery(buf, query.left) - ++ scanQuery(buf, query.right) - case Query.currentNode => - Chain.one(Left(buf)) - end scanQuery - - def fromQuery[T](query: Query[T]): FrozenRecordList[T, Query] = - val acc = new RecordList[T, Query] - query.altOptions.iterator.foreach: opt => - scanQuery(acc, opt).iterator - .map(_.both) - .foreach(_ += opt) - acc.result().map(_.freeze) - end fromQuery - - def fromPattern[T](pattern: Pattern[T]): FrozenRecordList[T, Pattern] = - val acc = new RecordList[T, Pattern] - pattern.altOptions.iterator.foreach: opt => - scanPattern(acc, opt).iterator - .map(_.both) - .foreach(_ += opt) - acc.result().map(_.freeze) - end fromPattern - - type RecordList[T, Q[_] <: Query[?] | Pattern[?]] = - mutable.ListBuffer[Record[T, Q]] - type FrozenRecordList[T, Q[_] <: Query[?] | Pattern[?]] = - List[FrozenRecord[T, Q]] - type Record[T, Q[_] <: Query[?] | Pattern[?]] = Q[T] | Branch[T, Q] - final case class Branch[T, Q[_] <: Query[?] | Pattern[?]]( - map: mutable.HashMap[Token | EndOfFieldsMarker, RecordList[T, Q]], - ): - def upsert(key: Token | EndOfFieldsMarker): RecordList[T, Q] = - map.getOrElseUpdate(key, mutable.ListBuffer.empty) - end upsert - end Branch - - extension [T, Q[_] <: Query[?] | Pattern[?]](recordList: RecordList[T, Q]) - def ensureBranch: Branch[T, Q] = - recordList.lastOption - .filter(_.isInstanceOf[Branch[T, Q]]) - .map(_.asInstanceOf[Branch[T, Q]]) - .getOrElse: - val branch = new Branch[T, Q]( - mutable.HashMap.empty[Token | EndOfFieldsMarker, RecordList[T, Q]], - ) - recordList += branch - branch - end ensureBranch - end extension - - extension [T, Q[_] <: Query[?] | Pattern[?]]( - either: Either[RecordList[T, Q], RecordList[T, Q]] - ) - def forceLeft: Either[RecordList[T, Q], RecordList[T, Q]] = - if either.isRight - then either.swap - else either - end forceLeft - - def both: RecordList[T, Q] = - either.fold(identity, identity) - end both - end extension - - extension [T, Q[_] <: Query[?] | Pattern[?]]( - record: Record[T, Q] - )(using TypeTest[Record[T, Q], Q[T]]) - def freeze: FrozenRecord[T, Q] = - record match - case Branch[T, Q](map) => - FrozenBranch(map.view.mapValues(_.map(_.freeze).result()).toMap) - case q: Q[T] => q - end freeze - end extension - - type FrozenRecord[T, Q[_] <: Query[?] | Pattern[?]] = Q[T] | - FrozenBranch[T, Q] - final case class FrozenBranch[T, Q[_] <: Query[?] | Pattern[?]]( - map: Map[Token | EndOfFieldsMarker, FrozenRecordList[T, Q]], - ) - - extension [T, Q[_] <: Query[?] | Pattern[?]]( - recs: FrozenRecordList[T, Q] - )(using TypeTest[FrozenRecord[T, Q], Q[T]]) - def run[U]( - nodeSpan: NodeSpan, - runFn: Q[T] => Eval[Option[U]], - ): Eval[Option[U]] = - def impl( - nodeSpan: NodeSpan, - recs: FrozenRecordList[T, Q], - ): Eval[Option[U]] = - Foldable[List].collectFirstSomeM(recs): - case query: Q[T] => - runFn(query) - case FrozenBranch[T, Q](map) => - nodeSpan.expandRightOption(1) match - case None => - map.get(EndOfFieldsMarker) match - case None => Eval.now(None) - case Some(recs) => - impl(nodeSpan, recs) - case Some(nodeSpan) => - nodeSpan.last.tokenOption match - case None => Eval.now(None) - case Some(token) => - map.get(token) match - case None => Eval.now(None) - case Some(query) => - impl(nodeSpan, recs) - end impl - impl(nodeSpan, recs) - end run - end extension - - type EndOfFieldsMarker = EndOfFieldsMarker.type - object EndOfFieldsMarker - end DecisionTree - - object currentNode extends Query[Node]: - protected def runQueryImpl(node: Node): Eval[Option[Node]] = - Eval.now(Some(node)) - end runQueryImpl - end currentNode - - transparent trait NodeAccessorQuery[T](query: Query[T]) extends Query[T]: - protected def access(node: Node): Option[Node] - protected final def runQueryImpl(node: Node): Eval[Option[T]] = - access(node) match - case None => Eval.now(None) - case Some(node) => Eval.defer(query.runQuery(node)) - end runQueryImpl - end NodeAccessorQuery - - final class leftSibling[T](query: Query[T]) - extends NodeAccessorQuery[T](query): - protected def access(node: Node): Option[Node] = node.leftSiblingOption - end leftSibling - - object leftSibling extends Query[Node]: - protected def runQueryImpl(node: Node): Eval[Option[Node]] = - Eval.now(node.leftSiblingOption) - end leftSibling - - final class rightSibling[T](query: Query[T]) - extends NodeAccessorQuery[T](query): - protected def access(node: Node): Option[Node] = node.rightSiblingOption - end rightSibling - - object rightSibling extends Query[Node]: - protected def runQueryImpl(node: Node): Eval[Option[Node]] = - Eval.now(node.rightSiblingOption) - end rightSibling - - final class parent[T](query: Query[T]) extends NodeAccessorQuery[T](query): - protected def access(node: Node): Option[Node] = node.parentOption - end parent - - object parent extends Query[Node]: - protected def runQueryImpl(node: Node): Eval[Option[Node]] = - Eval.now(node.parentOption) - end parent -end Query diff --git a/forja/src/Source.scala b/forja/src/Source.scala deleted file mode 100644 index 0cef055..0000000 --- a/forja/src/Source.scala +++ /dev/null @@ -1,104 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja - -import java.nio.ByteBuffer -import java.nio.channels.FileChannel -import java.nio.channels.FileChannel.MapMode -import java.nio.charset.StandardCharsets - -import scala.util.Using - -trait Source: - def origin: Option[os.Path] - def byteBuffer: ByteBuffer - - object lines: - val nlOffsets: IArray[Int] = - IArray.from: - SourceRange - .entire(Source.this) - .iterator - .zipWithIndex - .collect: - case ('\n', idx) => idx - end nlOffsets - - def lineColAtOffset(offset: Int): (Int, Int) = - import scala.collection.Searching.* - val lineIdx = - nlOffsets.search(offset) match - case Found(foundIndex) => foundIndex - case InsertionPoint(insertionPoint) => insertionPoint - val colIdx = - if lineIdx == 0 - then offset - else offset - 1 - nlOffsets(lineIdx - 1) - - (lineIdx, colIdx) - end lineColAtOffset - - def lineStartOffset(lineIdx: Int): Int = - if lineIdx == 0 - then 0 - else if lineIdx - 1 == nlOffsets.length - then SourceRange.entire(Source.this).length - else nlOffsets(lineIdx - 1) + 1 - end lineStartOffset - end lines -end Source - -object Source: - object empty extends Source: - def origin: Option[os.Path] = None - val byteBuffer: ByteBuffer = - ByteBuffer.wrap(IArray.emptyByteIArray.unsafeArray) - end empty - - def fromString(string: String): Source = - StringSource(string) - end fromString - - def fromByteBuffer(byteBuffer: ByteBuffer): Source = - ByteBufferSource(None, byteBuffer) - end fromByteBuffer - - def fromIArray(bytes: IArray[Byte]): Source = - ByteBufferSource(None, ByteBuffer.wrap(bytes.unsafeArray)) - end fromIArray - - def fromWritable(writable: geny.Writable): Source = - val out = java.io.ByteArrayOutputStream() - writable.writeBytesTo(out) - fromByteBuffer(ByteBuffer.wrap(out.toByteArray())) - end fromWritable - - def mapFromFile(path: os.Path): Source = - Using.resource(FileChannel.open(path.toNIO)): channel => - val mappedBuf = channel.map(MapMode.READ_ONLY, 0, channel.size()) - ByteBufferSource(Some(path), mappedBuf) - end mapFromFile - - final class StringSource(val string: String) extends Source: - def origin: Option[os.Path] = None - lazy val byteBuffer: ByteBuffer = - StandardCharsets.UTF_8.encode(string) - end StringSource - - final class ByteBufferSource( - val origin: Option[os.Path], - override val byteBuffer: ByteBuffer, - ) extends Source -end Source diff --git a/forja/src/SourceRange.scala b/forja/src/SourceRange.scala deleted file mode 100644 index 2fef703..0000000 --- a/forja/src/SourceRange.scala +++ /dev/null @@ -1,300 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja - -import java.io.OutputStream -import java.nio.ByteBuffer -import java.nio.channels.Channels -import java.nio.charset.{Charset, StandardCharsets} - -import forja.util.MonomorphicIndexedSeq - -import scala.collection.mutable - -final class SourceRange( - val source: Source, - val offset: Int, - val length: Int, -) extends MonomorphicIndexedSeq[Byte, SourceRange]: - require( - 0 <= offset && 0 <= length && offset + length <= source.byteBuffer.limit(), - s"invalid offset = $offset, length = $length into source with limit ${source.byteBuffer.limit()}", - ) - - override def toString(): String = - s"SourceRange(${{ decodeString() }})" - end toString - - def apply(i: Int): Byte = - source.byteBuffer - .get(offset + i) - end apply - - def byteBuffer(): ByteBuffer = - source.byteBuffer - .duplicate() - .position(offset) - .limit(offset + length) - .slice() - end byteBuffer - - def decodeString(charset: Charset = StandardCharsets.UTF_8): String = - charset.decode(byteBuffer()).toString() - end decodeString - - def writeBytesTo(out: java.io.OutputStream): Unit = - val channel = Channels.newChannel(out) - val bufSlice = this.byteBuffer() - // It seems .write will make a significant effort to write all bytes, - // but just to be safe let's retry if it writes less. - var count = 0 - while count < length - do count += channel.write(bufSlice) - end writeBytesTo - - def emptyAtOffset: SourceRange = - dropRight(length) - end emptyAtOffset - - def extendLeftBy(count: Int): SourceRange = - SourceRange(source, offset - count, length) - end extendLeftBy - - def extendLeft: SourceRange = - extendLeftBy(1) - end extendLeft - - def extendRightBy(count: Int): SourceRange = - SourceRange(source, offset, length + count) - end extendRightBy - - def extendRight: SourceRange = - extendRightBy(1) - end extendRight - - protected def sliceImpl(from: Int, until: Int): SourceRange = - require( - 0 <= from && from <= until && until <= length, - s"[$from, $until) is outside the range [$offset,${offset + length})", - ) - SourceRange(source, offset + from, until - from) - end sliceImpl - - @scala.annotation.targetName("combine") - def <+>(other: SourceRange): SourceRange = - if source eq Source.empty - then other - else if other.source eq Source.empty - then this - else if source eq other.source - then - // convert to [start,end) spans so we can do min/max merge. - // Doesn't work on lengths because those are relative to offset, - // but start / end are independent. - val (startL, endL) = (offset, offset + length) - val (startR, endR) = (other.offset, other.offset + other.length) - val (startC, endC) = (startL.min(startR), endL.max(endR)) - SourceRange(source, startC, endC - startC) - else this - end <+> - - def presentationStringShort: String = - val builder = StringBuilder() - builder ++= source.origin.map(_.toString).getOrElse("") - builder += ':' - - val (lineIdx, colIdx) = source.lines.lineColAtOffset(offset) - builder.append(lineIdx + 1) - builder += ':' - builder.append(colIdx + 1) - - if length > 0 - then - builder += '-' - val (lineIdx2, colIdx2) = source.lines.lineColAtOffset(offset + length) - if lineIdx == lineIdx2 - then builder.append(colIdx2 + 1) - else - builder.append(lineIdx2 + 1) - builder += ':' - builder.append(colIdx2 + 1) - - builder.result() - end presentationStringShort - - def presentationStringLong: String = - s"$presentationStringShort:\n$showInSource" - end presentationStringLong - - def showInSource: String = - extension (it: Iterator[Byte]) - // When we print a line, we don't want the trailing newline. - // We also don't want platform specific variants like \r. - def removeCrLf: Iterator[Byte] = - it.filter(ch => ch != '\r' && ch != '\n') - - def decodeString: String = - String(it.toArray, StandardCharsets.UTF_8) - - val entireSource = SourceRange.entire(source) - val builder = StringBuilder() - val (line1Idx, startCol) = source.lines.lineColAtOffset(offset) - val (line2Idx, endCol) = source.lines.lineColAtOffset(offset + length) - - val line1Start = source.lines.lineStartOffset(line1Idx) - val line1AfterEnd = source.lines.lineStartOffset(line1Idx + 1) - - if line1Idx == line2Idx - then - val lineFrag = entireSource.slice(line1Start, line1AfterEnd) - builder.addAll(lineFrag.iterator.removeCrLf.decodeString) - builder += '\n' - lineFrag - .slice(0, startCol) - .iterator - .removeCrLf - .foreach(_ => builder += ' ') - if startCol == endCol - then builder += '^' - else - lineFrag - .slice(startCol, endCol) - .iterator - .removeCrLf - .foreach(_ => builder += '^') - else - val line1Frag = entireSource.slice(line1Start, line1AfterEnd) - line1Frag - .slice(0, startCol) - .iterator - .removeCrLf - .foreach(_ => builder += ' ') - val line1EndCol = line1AfterEnd - line1Start - line1Frag - .slice(startCol, line1EndCol) - .iterator - .removeCrLf - .foreach(_ => builder += 'v') - builder += '\n' - builder.addAll(line1Frag.iterator.removeCrLf.decodeString) - - if line2Idx - line1Idx > 1 - then builder.addAll("\n...\n") - else builder += '\n' - - val line2Start = source.lines.lineStartOffset(line2Idx) - val line2AfterEnd = source.lines.lineStartOffset(line2Idx + 1) - val line2Frag = entireSource.slice(line2Start, line2AfterEnd) - builder.addAll(line2Frag.iterator.removeCrLf.decodeString) - builder += '\n' - line2Frag - .slice(0, endCol) - .iterator - .removeCrLf - .foreach(_ => builder += '^') - - builder.result() - end showInSource -end SourceRange - -object SourceRange: - final class Builder extends mutable.Builder[Byte, SourceRange]: - private val arrayBuilder = Array.newBuilder[Byte] - def addOne(elem: Byte): this.type = - arrayBuilder.addOne(elem) - this - def clear(): Unit = arrayBuilder.clear() - def result(): SourceRange = - SourceRange.entire( - Source.fromByteBuffer(ByteBuffer.wrap(arrayBuilder.result())), - ) - - def apply(str: String): SourceRange = - entire(Source.fromString(str)) - - def apply(bytes: IArray[Byte]): SourceRange = - entire(Source.fromIArray(bytes)) - end apply - - def apply(source: Source, offset: Int, length: Int): SourceRange = - new SourceRange(source, offset, length) - - def newBuilder: Builder = Builder() - - def entire(source: Source): SourceRange = - new SourceRange(source, 0, source.byteBuffer.limit()) - - given embed: Node.Embed[SourceRange]: - def prettyString(value: SourceRange): String = - value.toString() - end prettyString - def writeBytesTo(value: SourceRange, out: OutputStream): Unit = - value.iterator.foreach(out.write(_)) - end writeBytesTo - end embed - - extension (ctx: StringContext) - def src: srcImpl = - srcImpl(ctx) - end src - end extension - - final class srcImpl(val ctx: StringContext) extends AnyVal: - def unapplySeq(sourceRange: SourceRange): Option[Seq[SourceRange]] = - val parts = ctx.parts - assert(parts.nonEmpty) - - extension (part: String) - def bytes: SourceRange = - SourceRange.entire: - Source.fromByteBuffer: - StandardCharsets.UTF_8.encode: - StringContext.processEscapes(part) - - val firstPart = parts.head.bytes - - if !sourceRange.startsWith(firstPart) - then return None - - var currRange = sourceRange.drop(firstPart.length) - val matchedParts = mutable.ListBuffer.empty[SourceRange] - val didMatchFail = - parts.iterator - .drop(1) - .map(_.bytes) - .map: part => - if part.isEmpty - then - val idx = currRange.length - matchedParts += currRange - currRange = currRange.drop(currRange.length) - idx - else - val idx = currRange.indexOfSlice(part) - if idx != -1 - then - matchedParts += currRange.take(idx) - currRange = currRange.drop(idx) - - idx - .contains(-1) - || currRange.nonEmpty - - if didMatchFail - then None - else Some(matchedParts.result()) - end unapplySeq - end srcImpl -end SourceRange diff --git a/forja/src/Test.scala b/forja/src/Test.scala deleted file mode 100644 index 4f52593..0000000 --- a/forja/src/Test.scala +++ /dev/null @@ -1,55 +0,0 @@ -package forja - -import java.lang.classfile.ClassFile -import java.lang.constant.ClassDesc -import java.lang.constant.ConstantDescs -import java.lang.constant.MethodTypeDesc -import java.lang.classfile.TypeKind -import forja.Prod.LiftableWithOps - -object Test: - enum AST derives Prod.Liftable: - case A(x: Int) - case B(x: String) - end AST - - final case class B(x: String) derives Prod.Liftable - - summon[LiftableWithOps[AST.A]] - - def main(args: Array[String]): Unit = - val x: Prod[AST] = Prod[AST.A](42) - val y = x.fixpoint: - Prod.Rewrite.rw[AST]: - case Prod[AST.A](x) => - Prod[AST.B](x.reify().toString) - println(y.reify()) - - val bytes = - ClassFile.of().build(ClassDesc.of("forja.dummy.Test"), { clb => - clb - .withFlags(ClassFile.ACC_PUBLIC) - .withMethod(ConstantDescs.INIT_NAME, ConstantDescs.MTD_void, ClassFile.ACC_PUBLIC, { mb => - mb.withCode: cob => - cob - .aload(0) - .invokespecial(ConstantDescs.CD_Object, ConstantDescs.INIT_NAME, ConstantDescs.MTD_void) - .return_() - }) - .withMethod("test", MethodTypeDesc.of(ConstantDescs.CD_int), ClassFile.ACC_PUBLIC + ClassFile.ACC_STATIC, { mb => - mb.withCode: cb => - cb - .loadConstant(42) - .return_(TypeKind.INT) - }) - }) - end bytes - - object loader extends ClassLoader: - val cls: Class[?] = defineClass(null, bytes, 0, bytes.length) - end loader - - println(bytes.length) - println(loader.cls.getMethod("test").invoke(null)) - end main -end Test diff --git a/forja/src/TestUtil.scala b/forja/src/TestUtil.scala deleted file mode 100644 index 6f55095..0000000 --- a/forja/src/TestUtil.scala +++ /dev/null @@ -1,26 +0,0 @@ -package forja - -import com.github.difflib.{DiffUtils, UnifiedDiffUtils} -import scala.jdk.CollectionConverters.given - -object TestUtil: - def diffView[T](original: T, result: T): String = - val originalLines = original.toString().linesIterator.toBuffer.asJava - val patch = DiffUtils.diff( - originalLines, - result.toString().linesIterator.toBuffer.asJava, - ) - val diffLines = UnifiedDiffUtils.generateUnifiedDiff( - "original", - "result", - originalLines, - patch, - 5, - ) - diffLines.asScala.mkString("\n") - end diffView - - def assertEqualDiff[T](original: T, result: T): Unit = - assert(original == result, diffView(original, result)) - end assertEqualDiff -end TestUtil diff --git a/forja/src/Token.scala b/forja/src/Token.scala deleted file mode 100644 index bf46356..0000000 --- a/forja/src/Token.scala +++ /dev/null @@ -1,51 +0,0 @@ -package forja - -import scala.collection.concurrent - -final class Token private (val fullName: String): - inline def applyTupled[Tp <: Tuple, U](tp: Tp)(using - ctx: syntax.Context, - )(using app: ctx.NodeApply[Token *: Tp, U]): U = app(this *: tp) - - // format: off - inline def apply[U]()(using ctx: syntax.Context)(using app: ctx.NodeApply[Tuple1[Token], U]): U = app(Tuple1(this)) - inline def apply[T1, U](t1: T1)(using ctx: syntax.Context)(using app: ctx.NodeApply[(Token, T1), U]): U = app((this, t1)) - // %%replicate22 - inline def apply[T1, T2, U](t1: T1, t2: T2)(using ctx: syntax.Context)(using app: ctx.NodeApply[(Token, T1, T2), U]): U = app((this, t1, t2)) - inline def apply[T1, T2, T3, U](t1: T1, t2: T2, t3: T3)(using ctx: syntax.Context)(using app: ctx.NodeApply[(Token, T1, T2, T3), U]): U = app((this, t1, t2, t3)) - inline def apply[T1, T2, T3, T4, U](t1: T1, t2: T2, t3: T3, t4: T4)(using ctx: syntax.Context)(using app: ctx.NodeApply[(Token, T1, T2, T3, T4), U]): U = app((this, t1, t2, t3, t4)) - inline def apply[T1, T2, T3, T4, T5, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5)(using ctx: syntax.Context)(using app: ctx.NodeApply[(Token, T1, T2, T3, T4, T5), U]): U = app((this, t1, t2, t3, t4, t5)) - inline def apply[T1, T2, T3, T4, T5, T6, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6)(using ctx: syntax.Context)(using app: ctx.NodeApply[(Token, T1, T2, T3, T4, T5, T6), U]): U = app((this, t1, t2, t3, t4, t5, t6)) - inline def apply[T1, T2, T3, T4, T5, T6, T7, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7)(using ctx: syntax.Context)(using app: ctx.NodeApply[(Token, T1, T2, T3, T4, T5, T6, T7), U]): U = app((this, t1, t2, t3, t4, t5, t6, t7)) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8)(using ctx: syntax.Context)(using app: ctx.NodeApply[(Token, T1, T2, T3, T4, T5, T6, T7, T8), U]): U = app((this, t1, t2, t3, t4, t5, t6, t7, t8)) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8, t9: T9)(using ctx: syntax.Context)(using app: ctx.NodeApply[(Token, T1, T2, T3, T4, T5, T6, T7, T8, T9), U]): U = app((this, t1, t2, t3, t4, t5, t6, t7, t8, t9)) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8, t9: T9, t10: T10)(using ctx: syntax.Context)(using app: ctx.NodeApply[(Token, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10), U]): U = app((this, t1, t2, t3, t4, t5, t6, t7, t8, t9, t10)) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8, t9: T9, t10: T10, t11: T11)(using ctx: syntax.Context)(using app: ctx.NodeApply[(Token, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11), U]): U = app((this, t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11)) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8, t9: T9, t10: T10, t11: T11, t12: T12)(using ctx: syntax.Context)(using app: ctx.NodeApply[(Token, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12), U]): U = app((this, t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12)) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8, t9: T9, t10: T10, t11: T11, t12: T12, t13: T13)(using ctx: syntax.Context)(using app: ctx.NodeApply[(Token, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13), U]): U = app((this, t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13)) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8, t9: T9, t10: T10, t11: T11, t12: T12, t13: T13, t14: T14)(using ctx: syntax.Context)(using app: ctx.NodeApply[(Token, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14), U]): U = app((this, t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14)) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8, t9: T9, t10: T10, t11: T11, t12: T12, t13: T13, t14: T14, t15: T15)(using ctx: syntax.Context)(using app: ctx.NodeApply[(Token, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15), U]): U = app((this, t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15)) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8, t9: T9, t10: T10, t11: T11, t12: T12, t13: T13, t14: T14, t15: T15, t16: T16)(using ctx: syntax.Context)(using app: ctx.NodeApply[(Token, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16), U]): U = app((this, t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15, t16)) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8, t9: T9, t10: T10, t11: T11, t12: T12, t13: T13, t14: T14, t15: T15, t16: T16, t17: T17)(using ctx: syntax.Context)(using app: ctx.NodeApply[(Token, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17), U]): U = app((this, t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15, t16, t17)) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8, t9: T9, t10: T10, t11: T11, t12: T12, t13: T13, t14: T14, t15: T15, t16: T16, t17: T17, t18: T18)(using ctx: syntax.Context)(using app: ctx.NodeApply[(Token, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18), U]): U = app((this, t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15, t16, t17, t18)) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8, t9: T9, t10: T10, t11: T11, t12: T12, t13: T13, t14: T14, t15: T15, t16: T16, t17: T17, t18: T18, t19: T19)(using ctx: syntax.Context)(using app: ctx.NodeApply[(Token, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19), U]): U = app((this, t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15, t16, t17, t18, t19)) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8, t9: T9, t10: T10, t11: T11, t12: T12, t13: T13, t14: T14, t15: T15, t16: T16, t17: T17, t18: T18, t19: T19, t20: T20)(using ctx: syntax.Context)(using app: ctx.NodeApply[(Token, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20), U]): U = app((this, t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15, t16, t17, t18, t19, t20)) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8, t9: T9, t10: T10, t11: T11, t12: T12, t13: T13, t14: T14, t15: T15, t16: T16, t17: T17, t18: T18, t19: T19, t20: T20, t21: T21)(using ctx: syntax.Context)(using app: ctx.NodeApply[(Token, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21), U]): U = app((this, t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15, t16, t17, t18, t19, t20, t21)) - inline def apply[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22, U](t1: T1, t2: T2, t3: T3, t4: T4, t5: T5, t6: T6, t7: T7, t8: T8, t9: T9, t10: T10, t11: T11, t12: T12, t13: T13, t14: T14, t15: T15, t16: T16, t17: T17, t18: T18, t19: T19, t20: T20, t21: T21, t22: T22)(using ctx: syntax.Context)(using app: ctx.NodeApply[(Token, T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22), U]): U = app((this, t1, t2, t3, t4, t5, t6, t7, t8, t9, t10, t11, t12, t13, t14, t15, t16, t17, t18, t19, t20, t21, t22)) - // format: on - - override def toString(): String = fullName -end Token - -object Token: - private val byNameStore = concurrent.TrieMap[String, Token]() - def byName(fullName: String): Token = - byNameStore.getOrElseUpdate(fullName, new Token(fullName)) - end byName - - def apply(shapes: => Wf.Shapes*)(using - fullName: sourcecode.FullName, - ): Wf.TokenWf = - Wf.TokenWf(byName(fullName.value), Wf.ShapeSeq(shapes*)) - end apply -end Token diff --git a/forja/src/Wf.scala b/forja/src/Wf.scala deleted file mode 100644 index 439a3b4..0000000 --- a/forja/src/Wf.scala +++ /dev/null @@ -1,178 +0,0 @@ -package forja - -import scala.annotation.{publicInBinary, tailrec} - -import Wf.* - -trait Wf: - protected given localContext: syntax.WfContext.type = syntax.WfContext -end Wf - -object Wf: - sealed trait Shape: - def |(other: TokenWf | EmbedWf[?]): Shape - end Shape - - final class EmbedWf[T <: Matchable] @publicInBinary private[forja] (using - val embed: Node.Embed[T], - ) extends Shape: - def |(other: TokenWf | EmbedWf[?]): Shape = - Choice(this, other) - end | - end EmbedWf - - final class TokenWf private[forja] (val token: Token, shapeSeq: => ShapeSeq) - extends Shape: - tokenWf => - def |(other: TokenWf | EmbedWf[?]): Shape = Choice(this, other) - - export token.apply - - def replace(shapes: => Shapes*): TokenWf = - new TokenWf(token, ShapeSeq(shapes*)) - end replace - - private[forja] lazy val stableShapeSeq = shapeSeq - - object validate extends Pass: - protected def performImpl(root: Node): Node = - def implToken(node: Node, wf: TokenWf): Node = - if node.containsError - then return node - - node.tokenOption match - case Some(wf.token) => - @tailrec - def impl(node: Node, idx: Int, shapes: Seq[Shapes]): Node = - def implSingle(node: Node, shape: Shape): Node = - if node.containsError - then node - else - shape match - case shape: EmbedWf[t] => - given Node.Embed[t] = shape.embed - node.valueOption[t] match - case Some(_) => node - case None => - node.replaceThis: - Node.error( - s"expected ${shape.embed.getClass().getName()}", - )(node) - case shape: TokenWf => - implToken(node, shape) - case shape: Choice => - val ok = shape.choices.iterator - .exists: - case shape: EmbedWf[t] => - given Node.Embed[t] = shape.embed - node.valueOption[t].nonEmpty - case shape: TokenWf => - node.tokenOption.contains(shape.token) - if ok - then node - else - node.replaceThis: - val choicesStrs = shape.choices.map: - case shape: EmbedWf[t] => - shape.embed.getClass().getName() - case shape: TokenWf => - shape.token.fullName - Node.error( - s"expected one of ${choicesStrs.mkString(" | ")}", - )(node) - end match - end if - end implSingle - - shapes match - case Seq() => - if idx == node.children.size - then node - else - // TODO: consider appending the error onto the end of the node's children - node.replaceThis: - Node.error( - s"unmatched children remain, matched up to here (idx = $idx)", - )((node.children.lift(idx).toList :+ node)*) - end if - case shape +: restShapes => - if !node.children.indices.contains(idx) - then - shape match - case _: RepeatedShape => - // End of repetition, ok actually. - // It'll still fail if anything in restShapes requires elements though. - impl( - node = node, - idx = idx, - shapes = restShapes, - ) - case _ => - node.replaceThis: - Node.error( - s"wrong child count (index $idx out of range, shapes remaining)", - )(node) - end match - else - shape match - case shape: Shape => - impl( - node = implSingle( - node.children(idx), - shape, - ).parentOption.get, - idx = idx + 1, - shapes = restShapes, - ) - case shape: (Token, Shape) => - ??? - case shape: RepeatedShape => - val resultNode = - implSingle(node.children(idx), shape.shape) - if resultNode.isError // not just contains, the error must be at top level - then - impl( - node = node, - idx = idx, - shapes = restShapes, - ) - else - impl( - node = resultNode.parentOption.get, - idx = idx + 1, - shapes = shapes, - ) - end if - end match - end if - end match - end impl - - impl(node = node, idx = 0, shapes = wf.stableShapeSeq.shapes) - case Some(otherToken) => - node.replaceThis: - Node.error(s"expected $token")(node) - case None => - node.replaceThis: - Node.error(s"expected $token")(node) - end match - end implToken - - root.replaceThis(implToken(root.asOrphan, tokenWf)) - end performImpl - end validate - end TokenWf - - private[forja] final class Choice(val choices: TokenWf | EmbedWf[?]*) - extends Shape: - def |(other: TokenWf | EmbedWf[?]): Shape = Choice((choices :+ other)*) - end Choice - - type Shapes = Shape | (Token, Shape) | RepeatedShape - - private[forja] final class ShapeSeq(val shapes: Shapes*) - - private[forja] final class RepeatedShape @publicInBinary private[forja] ( - val shape: Shape, - ) -end Wf diff --git a/forja/src/syntax.scala b/forja/src/syntax.scala deleted file mode 100644 index 3cb32e4..0000000 --- a/forja/src/syntax.scala +++ /dev/null @@ -1,240 +0,0 @@ -package forja - -import forja.util.{FastPatchTree, TupleOf} - -import scala.collection.mutable -import scala.util.NotGiven - -object syntax: - export Wf.TokenWf - export Query.on - - object unchanged - case object `...` - type `...` = `...`.type - - transparent inline def cc(using c: Context): c.type = c - - sealed abstract class Context: - given localContext: this.type = this - - trait NodeApply[-T, +U] extends (T => U) - trait NodeSpanApply[-T, +U] extends (T => U) - end Context - object Context: - given default: ValueContext.type = ValueContext - end Context - - object ValueContext extends Context: - def lit[T <: Matchable: Node.Embed](value: T): Node = - Node.embed(value) - end lit - - sealed trait NodeApplyImpl[-T]: - def children(arg: T, builder: mutable.Growable[Node.NodeImpl]): Unit - def attrs(arg: T, builder: mutable.Growable[(Token, Node.NodeImpl)]): Unit - end NodeApplyImpl - - sealed trait NodeSpanApplyImpl[-T] extends NodeApplyImpl[T]: - final def attrs( - arg: T, - builder: mutable.Growable[(Token, Node.NodeImpl)], - ): Unit = () - end NodeSpanApplyImpl - - given nodeSpanApplyImplNode: NodeSpanApplyImpl[Node]: - def children(arg: Node, builder: mutable.Growable[Node.NodeImpl]): Unit = - builder += arg.impl - end children - end nodeSpanApplyImplNode - - given nodeSpanApplyImplNodeIter - : [T <: IterableOnce[Node]] => NodeSpanApplyImpl[IterableOnce[Node]]: - def children( - arg: IterableOnce[Node], - builder: mutable.Growable[Node.NodeImpl], - ): Unit = - builder ++= arg.iterator.map(_.impl) - end children - end nodeSpanApplyImplNodeIter - - given nodeApplyImplAttr: NodeApplyImpl[(Token, Node)]: - def children( - arg: (Token, Node), - builder: mutable.Growable[Node.NodeImpl], - ): Unit = () - def attrs( - arg: (Token, Node), - builder: mutable.Growable[(Token, Node.NodeImpl)], - ): Unit = - builder += ((arg._1, arg._2.impl)) - end attrs - end nodeApplyImplAttr - - given nodeSpanApplyImplEmbed - : [T <: Matchable: Node.Embed] => NodeSpanApplyImpl[T]: - def children(arg: T, builder: mutable.Growable[Node.NodeImpl]): Unit = - builder += Node.embed(arg).impl - end children - end nodeSpanApplyImplEmbed - - given nodeApply - : [Rest <: Tuple] => (impls: TupleOf[Tuple.Map[Rest, NodeApplyImpl]]) - => NodeApply[Token *: Rest, Node]: - def apply(arg: Token *: Rest): Node = - val childrenBuilder = FastPatchTree.newBuilder[Node.NodeImpl] - val attrsBuilder = Map.newBuilder[Token, Node.NodeImpl] - (arg.productIterator - .drop(1) - .zip(impls.value.productIterator)) - .foreach: (arg, impl) => - impl.asInstanceOf[NodeApplyImpl[Any]].children(arg, childrenBuilder) - impl.asInstanceOf[NodeApplyImpl[Any]].attrs(arg, attrsBuilder) - new Node( - impl = Node.NodeImpl.TokenNode( - token = arg.head, - children = childrenBuilder.result(), - attrs = attrsBuilder.result(), - ), - nodeParentInfo = Node.NodeParentInfo.Orphan, - ) - end apply - end nodeApply - - given nodeSpanApply - : [T <: Tuple] => (impls: TupleOf[Tuple.Map[T, NodeSpanApplyImpl]]) - => NodeSpanApply[T, Seq[Node]]: - def apply(arg: T): Seq[Node] = - val builder = Seq.newBuilder[Node.NodeImpl] - (arg.productIterator - .zip(impls.value.productIterator)) - .foreach: (arg, impl) => - impl.asInstanceOf[NodeSpanApplyImpl[Any]].children(arg, builder) - - builder - .result() - .map: impl => - new Node( - impl = impl, - nodeParentInfo = Node.NodeParentInfo.Orphan, - ) - end apply - end nodeSpanApply - end ValueContext - - object PatternContext extends Context: - def lit[T <: Matchable: Node.Embed](value: T, values: T*): Pattern[T] = - values.foldLeft(new Pattern.EmbedLiteralPattern(value): Pattern[T]): - (acc, v) => acc | new Pattern.EmbedLiteralPattern(v) - end lit - - def embed[T <: Matchable: Node.Embed]: Pattern[T] = - Pattern.embed[T] - end embed - - def rep[T <: Matchable]( - elem: Pattern[T], - ): Pattern[List[Pattern.RWType[T]]] = - Pattern.rep(elem.map(Pattern.RWType.adjust)) - end rep - - def rep1[T <: Matchable]( - elem: Pattern[T], - ): Pattern[List[Pattern.RWType[T]]] = - val elemM = elem.map(Pattern.RWType.adjust) - NodeSpan(+elemM, +cc.rep(elem)).map(_ :: _) - end rep1 - - def not[T](elem: Pattern[T]): Pattern[Unit] = - Pattern.not(elem) - end not - - sealed trait NodeSpanApplyImpl[-T <: Tuple, +U <: Tuple]: - def tupleArity: Int - def cases(arg: T): List[Pattern.Tupled.Case] - final def apply(total: Boolean, arg: T): Pattern[U] = - Pattern.Tupled( - isTotal = total, - tupleArity = tupleArity, - cases = cases(arg), - ) - end apply - end NodeSpanApplyImpl - - given nodeSpanApplyEmpty: NodeSpanApplyImpl[EmptyTuple, EmptyTuple]: - def tupleArity: Int = 0 - def cases(arg: EmptyTuple) = Nil - end nodeSpanApplyEmpty - - given nodeSpanApplyIncludeNonTuple: [T, Rest <: Tuple, RestU <: Tuple] - => NotGiven[T <:< Tuple] => (impl: NodeSpanApplyImpl[Rest, RestU]) - => NodeSpanApplyImpl[Pattern.Include[T] *: Rest, T *: RestU]: - def tupleArity: Int = 1 + impl.tupleArity - def cases(arg: Pattern.Include[T] *: Rest) = - Pattern.Tupled.IncludeCase(arg.head.pattern) - :: impl.cases(arg.tail) - end nodeSpanApplyIncludeNonTuple - - given nodeSpanApplyIncludeTuple: [T <: Tuple, Rest <: Tuple, RestU <: Tuple] - => (includedSize: ValueOf[Tuple.Size[T]]) - => ( - impl: NodeSpanApplyImpl[Rest, RestU], - ) => NodeSpanApplyImpl[Pattern.Include[T] *: Rest, Tuple.Concat[T, RestU]]: - def tupleArity: Int = includedSize.value + impl.tupleArity - def cases(arg: Pattern.Include[T] *: Rest) = - Pattern.Tupled.IncludeTupleCase(arg.head.pattern) - :: impl.cases(arg.tail) - end nodeSpanApplyIncludeTuple - - given nodeSpanApplySkip: [T, Rest <: Tuple, RestU <: Tuple] - => ( - impl: NodeSpanApplyImpl[Rest, RestU], - ) => NodeSpanApplyImpl[Pattern[T] *: Rest, RestU]: - def tupleArity: Int = impl.tupleArity - def cases(arg: Pattern[T] *: Rest) = - Pattern.Tupled.SkipCase(arg.head) - :: impl.cases(arg.tail) - end nodeSpanApplySkip - - given `nodeSpanApply...`: [T, Rest <: Tuple, RestU <: Tuple] - => ( - impl: NodeSpanApplyImpl[Rest, RestU], - ) => NodeSpanApplyImpl[`...`.type *: Rest, RestU]: - def tupleArity: Int = impl.tupleArity - def cases(arg: `...`.type *: Rest) = - Pattern.Tupled.WildcardCase - :: impl.cases(arg.tail) - end `nodeSpanApply...` - - given nodeSpanApply: [T <: Tuple, U <: Tuple] - => (impl: NodeSpanApplyImpl[T, U]) => NodeSpanApply[T, Pattern[U]]: - def apply(arg: T): Pattern[U] = - impl(total = false, arg = arg) - end apply - end nodeSpanApply - - given nodeApplyToken - : [Rest <: Tuple, U <: Tuple] => (impl: NodeSpanApplyImpl[Rest, U]) - => NodeApply[Token *: Rest, Pattern[U]]: - def apply(arg: Token *: Rest): Pattern[U] = - Pattern.tokenExact(arg.head, impl(total = true, arg = arg.tail)) - end apply - end nodeApplyToken - - given nodeApplyAny - : [T <: Tuple, U <: Tuple] => NotGiven[Tuple.Head[T] <:< Token] - => (impl: NodeSpanApplyImpl[T, U]) => NodeApply[T, Pattern[U]]: - def apply(arg: T): Pattern[U] = - Pattern.tokenAny(impl(total = true, arg = arg)) - end apply - end nodeApplyAny - end PatternContext - - object WfContext extends Context: - def embed[T <: Matchable: Node.Embed]: Wf.EmbedWf[T] = - Wf.EmbedWf[T] - - def rep[T](elem: Wf.Shape): Wf.RepeatedShape = - new Wf.RepeatedShape(elem) - end WfContext -end syntax diff --git a/forja/src/util/FastPatchTree.scala b/forja/src/util/FastPatchTree.scala deleted file mode 100644 index 7d9c0fc..0000000 --- a/forja/src/util/FastPatchTree.scala +++ /dev/null @@ -1,92 +0,0 @@ -package forja.util - -import scala.collection.immutable.IndexedSeqOps -import scala.collection.{ - IterableFactoryDefaults, - StrictOptimizedSeqFactory, - mutable, -} - -final class FastPatchTree[T](left: Vector[T], right: Vector[T]) - extends IndexedSeq[T], - IndexedSeqOps[T, FastPatchTree, FastPatchTree[T]], - IterableFactoryDefaults[T, FastPatchTree]: - override def iterableFactory = FastPatchTree - - def apply(i: Int): T = - if i < left.length - then left(i) - else right(i - left.length) - end apply - - override def updated[B >: T](index: Int, elem: B): FastPatchTree[B] = - if left.indices.contains(index) - then - new FastPatchTree( - left = left.updated(index, elem), - right = right, - ) - else if right.indices.contains(index - left.length) - then - new FastPatchTree( - left = left, - right = right.updated(index - left.length, elem), - ) - else throw IndexOutOfBoundsException(index.toString()) - end updated - - def length: Int = - left.length + right.length - - override def slice(from: Int, until: Int): FastPatchTree[T] = - new FastPatchTree( - left = left.slice(from, until), - right = right.slice(from - left.length, until - left.length), - ) - end slice - - override def patch[B >: T]( - from: Int, - other: IterableOnce[B], - replaced: Int, - ): FastPatchTree[B] = - new FastPatchTree( - left = left - .take(from) - .appendedAll(right.take(from - left.length)) - .appendedAll(other), - right = right - .drop(from + replaced - left.length) - .prependedAll(left.drop(from + replaced)), - ) - end patch - - override def take(n: Int): FastPatchTree[T] = - slice(0, n) - - override def drop(n: Int): FastPatchTree[T] = - slice(n, length) - - override def dropRight(n: Int): FastPatchTree[T] = - slice(0, length - n) - - override def takeRight(n: Int): FastPatchTree[T] = - slice(length - n, length) -end FastPatchTree - -object FastPatchTree extends StrictOptimizedSeqFactory[FastPatchTree]: - def empty[A]: FastPatchTree[A] = - new FastPatchTree(left = Vector.empty, right = Vector.empty) - def from[A](source: IterableOnce[A]): FastPatchTree[A] = - new FastPatchTree(left = Vector.empty, right = Vector.from(source)) - def newBuilder[A]: mutable.Builder[A, FastPatchTree[A]] = - new mutable.Builder[A, FastPatchTree[A]]: - private val vectorBuilder = Vector.newBuilder[A] - export vectorBuilder.clear - def addOne(elem: A): this.type = - vectorBuilder.addOne(elem) - this - def result(): FastPatchTree[A] = - new FastPatchTree(left = Vector.empty, right = vectorBuilder.result()) - end newBuilder -end FastPatchTree diff --git a/forja/src/util/InlineConversion.scala b/forja/src/util/InlineConversion.scala new file mode 100644 index 0000000..5d7191f --- /dev/null +++ b/forja/src/util/InlineConversion.scala @@ -0,0 +1,14 @@ +package forja.util + +abstract class InlineConversion[-T, +U] extends Conversion[T, U]: + inline def apply(t: T): U +end InlineConversion + +object InlineConversion: + sealed abstract class ByCast[-T, +U] private extends InlineConversion[T, U]: + inline def apply(t: T): U = t.asInstanceOf[U] + end ByCast + object ByCast: + inline def apply[T, U](): ByCast[T, U] = null.asInstanceOf + end ByCast +end InlineConversion diff --git a/forja/src/util/Instanceless.scala b/forja/src/util/Instanceless.scala new file mode 100644 index 0000000..d489a26 --- /dev/null +++ b/forja/src/util/Instanceless.scala @@ -0,0 +1,12 @@ +package forja.util + +import scala.annotation.implicitNotFound + +trait Instanceless(using poison: Instanceless.Poison) + +object Instanceless: + inline def apply[T]: T = null.asInstanceOf[T] + + @implicitNotFound("this trait should never have a meaningful instance") + opaque type Poison = Nothing +end Instanceless diff --git a/forja/src/util/MonomorphicIndexedSeq.scala b/forja/src/util/MonomorphicIndexedSeq.scala deleted file mode 100644 index e043b44..0000000 --- a/forja/src/util/MonomorphicIndexedSeq.scala +++ /dev/null @@ -1,43 +0,0 @@ -package forja.util - -trait MonomorphicIndexedSeq[+T, C <: MonomorphicIndexedSeq[T, C]] - extends IndexedSeq[T]: - self: C => - final override def slice(from: Int, until: Int): C = sliceImpl(from, until) - protected def sliceImpl(from: Int, until: Int): C - - final override def tail: C = drop(1) - final override def tails: Iterator[C] = - Iterator.unfold(Some(this): Option[C]): - case None => None - case Some(acc) => - if acc.isEmpty - then Some((acc, None)) - else Some((acc, Some(acc.tail))) - end tails - final override def init: C = dropRight(1) - final override def inits: Iterator[C] = - Iterator.unfold(Some(this): Option[C]): - case None => None - case Some(acc) => - if acc.isEmpty - then Some((acc, None)) - else Some((acc, Some(acc.init))) - end inits - - final override def drop(n: Int): C = slice(n, length) - final override def dropRight(n: Int): C = slice(0, length - n) - - final override def take(n: Int): C = slice(0, n) - final override def takeRight(n: Int): C = slice(length - n, length) - - final override def takeWhile(pred: T => Boolean): C = - val count = iterator.takeWhile(pred).size - take(count) - end takeWhile - - final override def dropWhile(pred: T => Boolean): C = - val count = iterator.takeWhile(pred).size - drop(count) - end dropWhile -end MonomorphicIndexedSeq diff --git a/forja/src/util/ReflectiveEnumeration.scala b/forja/src/util/ReflectiveEnumeration.scala deleted file mode 100644 index 14e1489..0000000 --- a/forja/src/util/ReflectiveEnumeration.scala +++ /dev/null @@ -1,85 +0,0 @@ -package forja.util - -import scala.compiletime.deferred -import scala.jdk.CollectionConverters.* -import scala.quoted.{Expr, Quotes, Type, Varargs, quotes} -import scala.reflect.{ClassTag, TypeTest} - -import ReflectiveEnumeration.* - -transparent trait ReflectiveEnumeration: - reflectiveEnumeration => - private[ReflectiveEnumeration] given fieldList - : FieldList[reflectiveEnumeration.type] = deferred - - def valuesByType[T <: Enumerable: ClassTag](using - TypeTest[Enumerable, T], - ): IArray[(String, T)] = - fieldList - .fieldValues(this) - .collect: - case (fieldName, value: T) => (fieldName, value) - end valuesByType -end ReflectiveEnumeration - -object ReflectiveEnumeration: - trait Enumerable - - trait FieldList[R <: ReflectiveEnumeration]: - def fieldValues(r: R): IArray[(String, Enumerable)] - end FieldList - - inline given inferredFieldList: [R <: ReflectiveEnumeration] => FieldList[R] = - ${ inferredFieldListImpl[R] } - - private[forja] def inferredFieldListImpl[R <: ReflectiveEnumeration: Type]( - using Quotes, - ): Expr[FieldList[R]] = - import quotes.reflect.* - '{ - new FieldList[R]: - def fieldValues(r: R): IArray[(String, Enumerable)] = - ${ - val tp = TypeRepr.of[R] - if !tp.isSingleton - then - report.errorAndAbort( - s"${tp.show} must be a singleton", - Symbol.spliceOwner.pos.get, - ) - - val syms = - (tp.typeSymbol.methodMembers ++ tp.typeSymbol.fieldMembers) - .filter: m => - tp.select(m) <:< TypeRepr.of[Enumerable] - .filterNot(_.flags.is(Flags.Protected | Flags.Private)) - .sortBy: m => - m.pos - .map: pos => - (pos.sourceFile.name, pos.start, pos.end) - .getOrElse: - report.errorAndAbort(s"could not get position for $m") - - syms - .groupBy(_.pos.get) - .foreach: (pos, syms) => - if syms.size > 1 - then - report.errorAndAbort( - s"multiple symbols have position $pos: ${syms.mkString(", ")}", - ) - end if - - val exprs = syms.map: sym => - '{ - ( - ${ Expr(sym.name) }, - ${ 'r.asTerm.select(sym).asExprOf[Enumerable] }, - ) - } - '{ IArray(${ Varargs(exprs) }*) } - } - end fieldValues - } - end inferredFieldListImpl -end ReflectiveEnumeration diff --git a/forja/src/util/TupleOf.scala b/forja/src/util/TupleOf.scala deleted file mode 100644 index 940cadc..0000000 --- a/forja/src/util/TupleOf.scala +++ /dev/null @@ -1,9 +0,0 @@ -package forja.util - -import scala.compiletime.summonAll - -final class TupleOf[T <: Tuple](val value: T) extends AnyVal - -object TupleOf: - inline given tupleOf: [T <: Tuple] => TupleOf[T] = TupleOf(summonAll[T]) -end TupleOf diff --git a/forja/test/src/NodeSpanTest.scala b/forja/test/src/NodeSpanTest.scala deleted file mode 100644 index c88fff5..0000000 --- a/forja/test/src/NodeSpanTest.scala +++ /dev/null @@ -1,46 +0,0 @@ -package forja - -import utest.* - -import forja.syntax.* - -import NodeSpanTest.* - -class NodeSpanTest extends TestSuite: - def tests = Tests: - test("slice") { - val data = (1 to 5).map(cc.lit) - val span = AST.T(data).children.asNodeSpan - - for - from <- (0 to data.size + 1) - until <- (0 to data.size + 1) - do - val slicedData = data.slice(from, until) - val slicedSpan = span.slice(from, until) - - (from, until, slicedSpan.toList) ==> (from, until, slicedData.toList) - - // Do it again, uncovers 0 related assumptions - for - from <- (0 to data.size + 1) - until <- (0 to data.size + 1) - do - val slicedData2 = slicedData.slice(from, until) - val slicedSpan2 = slicedSpan.slice(from, until) - - if slicedSpan2.toList != slicedData2.toList - then - Predef.assert( - false, - s"$slicedSpan.slice($from, $until) --> $slicedSpan2\n$slicedData.slice($from, $until) --> $slicedData2", - ) - } - end tests -end NodeSpanTest - -object NodeSpanTest: - object AST extends Wf: - lazy val T = Token() - end AST -end NodeSpanTest diff --git a/forja/test/src/PatternTest.scala b/forja/test/src/PatternTest.scala deleted file mode 100644 index c043324..0000000 --- a/forja/test/src/PatternTest.scala +++ /dev/null @@ -1,190 +0,0 @@ -package forja - -import utest.* - -import forja.syntax.* -import PatternTest.* - -class PatternTest extends TestSuite: - extension (node: Node) - def rwChildren[T <: Matchable](rw: Query.rewrite[T]): Node = - rw.pattern - .runPattern(node.children.asEmptyNodeSpan) - .map(_._2) - .flatMap(_.parentOption) - .get - end rwChildren - end extension - - def tests = Tests: - test("single") { - AST - .Root() - .rwChildren: - on().rewrite(_ => NodeSpan(42)) - ==> AST.Root(42) - - AST - .Root( - AST.T1(), - ) - .rwChildren: - on(AST.T1()).rewrite(_ => NodeSpan(AST.T2())) - ==> AST.Root(AST.T2()) - - AST - .Root( - 42, - ) - .rwChildren: - on(cc.lit(42)).rewrite(_ => NodeSpan(43)) - ==> AST.Root(43) - } - - test("reps") { - AST - .Root() - .rwChildren: - on(cc.rep(cc.lit(42))).rewrite(_ => NodeSpan()) - ==> AST.Root() - - AST - .Root( - AST.T1(), - ) - .rwChildren: - on(cc.rep(AST.T1())).rewrite(_ => NodeSpan()) - ==> AST.Root() - - AST - .Root( - AST.T1(), - AST.T1(), - ) - .rwChildren: - on(cc.rep(AST.T1())).rewrite(_ => NodeSpan()) - ==> AST.Root() - - AST - .Root( - AST.T1(), - AST.T1(), - AST.T2(), - AST.T1(), - ) - .rwChildren: - on(cc.rep(AST.T1())).rewrite(_ => NodeSpan()) - ==> AST.Root( - AST.T2(), - AST.T1(), - ) - } - - test("nested") { - AST - .Root( - 42, - ) - .rwChildren: - on( - cc.lit(42).rewrite(_ => cc.lit(43)), - ).rewriteInPattern - ==> AST.Root(43) - - AST - .Root( - AST.T1( - 42, - ), - ) - .rwChildren: - on( - AST.T1( - cc.lit(42).rewrite(_ => cc.lit(43)), - ), - ).rewriteInPattern - ==> AST.Root(AST.T1(43)) - - AST - .Root( - AST.T1(1), - AST.T2(2), - ) - .rwChildren: - on( - AST.T1(`...`), - AST.T2(+cc.embed[Int]).rewrite(i => NodeSpan(i)), - ).rewriteInPattern - ==> AST.Root(AST.T1(1), 2) - } - - test("captures") { - AST - .Root( - AST.T1(-1), - AST.T1(42), - ) - .rwChildren: - on( - !AST.T1(cc.lit(-1)), - +AST.T1(+cc.embed[Int]), - ).rewrite: (t1, i) => - NodeSpan(i, t1) - ==> AST.Root(42, AST.T1(-1)) - } - - test("wildcards") { - AST - .Root( - 1, 2, 3, 4, 5, - ) - .rwChildren: - on( - +cc.lit(1), - `...`, - +cc.lit(5), - ).rewrite: (i1, i2) => - NodeSpan(i1, i2) - ==> AST.Root(1, 5) - - AST - .Root( - 1, 2, 3, 4, 5, - ) - .rwChildren: - on( - cc.lit(1), - `...`, - +cc.embed[Int], - cc.lit(5), - ).rewrite: i => - NodeSpan(i) - ==> AST.Root(4) - - AST - .Root( - 1, 2, 3, 4, 5, - ) - .rwChildren: - on( - `...`, - cc.lit(2), - +cc.embed[Int], - `...`, - +cc.embed[Int], - cc.lit(5), - ).rewrite: (i1, i2) => - NodeSpan(i1, i2) - ==> AST.Root(3, 4) - } - end tests -end PatternTest - -object PatternTest: - trait AST extends Wf: - lazy val Root = Token() - lazy val T1 = Token() - lazy val T2 = Token() - end AST - object AST extends AST -end PatternTest diff --git a/forja/test/src/WfTest.scala b/forja/test/src/WfTest.scala deleted file mode 100644 index 4abb198..0000000 --- a/forja/test/src/WfTest.scala +++ /dev/null @@ -1,88 +0,0 @@ -package forja - -import utest.* -import forja.syntax.* -import WfTest.* -import forja.TestUtil.assertEqualDiff - -class WfTest extends TestSuite: - extension (node: Node) - def shouldValidate(tokenWf: TokenWf): Unit = - val validated = tokenWf.validate.perform(node) - assertEqualDiff(node, validated) - end extension - - def tests = Tests: - test("test1") { - Test1.Root(Test1.Foo(), Test1.Bar()).shouldValidate(Test1.Root) - } - test("test2 bar empty") { - Test2 - .Root( - Test2.Foo(), - Test2.Bar(), - ) - .shouldValidate(Test2.Root) - } - test("test2 bar single") { - Test2 - .Root( - Test2.Foo(), - Test2.Bar( - Test2.Foo(), - ), - ) - .shouldValidate(Test2.Root) - } - test("test2 bar double") { - Test2 - .Root( - Test2.Foo(), - Test2.Bar( - Test2.Foo(), - Test2.Foo(), - ), - ) - .shouldValidate(Test2.Root) - } - test("test3 with an int") { - Test3 - .Root( - Test3.Foo(), - Test3.Bar( - Test3.Foo(), - ), - Test3.Ping(42), - ) - .shouldValidate(Test3.Root) - } - end tests -end WfTest - -object WfTest: - trait Test1 extends Wf: - lazy val Root = Token( - Foo, - Bar, - ) - lazy val Foo = Token() - lazy val Bar = Token() - end Test1 - object Test1 extends Test1 - - trait Test2 extends Test1: - override lazy val Bar = - Test1.Bar.replace(cc.rep(Foo)) - end Test2 - object Test2 extends Test2 - - trait Test3 extends Test2: - override lazy val Root = Test2.Root.replace( - Foo, - Bar, - Ping, - ) - lazy val Ping = Token(cc.embed[Int]) - end Test3 - object Test3 extends Test3 -end WfTest diff --git a/forja/test/src/util/FastPatchTreeTest.scala b/forja/test/src/util/FastPatchTreeTest.scala deleted file mode 100644 index 098b6ba..0000000 --- a/forja/test/src/util/FastPatchTreeTest.scala +++ /dev/null @@ -1,31 +0,0 @@ -package forja.util - -import utest.* - -final class FastPatchTreeTest extends TestSuite: - def tests = Tests: - test("patch") { - val data1 = Seq(1, 2, 3) - val data2 = Seq(4, 5, 6) - val data3 = Seq(7, 8, 9) - def forall(fn: (Int, Int, Int) => Unit): Unit = - (0 until 3).foreach: from => - (0 until data2.length).foreach: take => - (0 until 3).foreach: replaced => - fn(from, take, replaced) - end forall - - forall: (from, take, replaced) => - val fp1 = FastPatchTree(data1*).patch(from, data2.take(take), replaced) - val d1 = data1.patch(from, data2.take(take), replaced) - fp1.toSeq ==> d1 - - forall: (from, take, replaced) => - fp1.patch(from, data3.take(take), replaced).toSeq ==> d1.patch( - from, - data3.take(take), - replaced, - ) - } - end tests -end FastPatchTreeTest diff --git a/format_src.sh b/format_src.sh index cf57df3..cefb0e4 100755 --- a/format_src.sh +++ b/format_src.sh @@ -3,6 +3,6 @@ set -x -e ./mill --meta-level 1 mill.scalalib.scalafmt/ -./mill forja.updateLimit22Apply -./mill __.fix +# TODO: into syntax is preview, and ScalaFix does not pass the option properly +# ./mill __.fix ./mill mill.scalalib.scalafmt/ diff --git a/format_src_check.sh b/format_src_check.sh index b36edbf..262b31e 100755 --- a/format_src_check.sh +++ b/format_src_check.sh @@ -3,6 +3,6 @@ set -x -e ./mill --meta-level 1 mill.scalalib.scalafmt/checkFormatAll -./mill forja.updateLimit22Apply --check true -./mill __.fix --check +# see format_src.sh +# ./mill __.fix --check ./mill mill.scalalib.scalafmt/checkFormatAll \ No newline at end of file diff --git a/langs/calc/CalcEvaluator.scala b/langs/calc/CalcEvaluator.scala deleted file mode 100644 index 8b0bff9..0000000 --- a/langs/calc/CalcEvaluator.scala +++ /dev/null @@ -1,101 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.langs.calc - -import cats.syntax.all.given - -import forja.* -import forja.dsl.* -import forja.source.Reader -import forja.wf.Wellformed - -import lang.* - -object CalcEvaluator extends PassSeq: - import Reader.* - import CalcReader.* - - def inputWellformed: Wellformed = lang.wf - - val passes = List( - ConstantsPass, - EvaluatorPass, - StripExpressionPass, - ) - - object ConstantsPass extends Pass: - val wellformed = prevWellformed.makeDerived: - Expression.removeCases(Number) - Expression.addCases(EmbedMeta[Int]) - val rules = pass(once = true, strategy = pass.bottomUp) - .rules: - on( - tok(Expression) *> onlyChild(tok(Number)), - ).rewrite: num => - splice(Expression(Node.Embed(num.sourceRange.decodeString().toInt))) - - object EvaluatorPass extends Pass: - val wellformed = prevWellformed.makeDerived: - Expression ::=! embedded[Int] - val rules = pass(once = false, strategy = pass.bottomUp) - .rules: - on( - tok(Expression) *> onlyChild( - tok(Add).withChildren: - field(tok(Expression) *> onlyChild(embed[Int])) - ~ field(tok(Expression) *> onlyChild(embed[Int])) - ~ eof, - ), - ).rewrite: (left, right) => - splice(Expression(Node.Embed[Int](left + right))) - | on( - tok(Expression) *> onlyChild( - tok(Sub).withChildren: - field(tok(Expression) *> onlyChild(embed[Int])) - ~ field(tok(Expression) *> onlyChild(embed[Int])) - ~ eof, - ), - ).rewrite: (left, right) => - splice(Expression(Node.Embed[Int](left - right))) - | on( - tok(Expression) *> onlyChild( - tok(Mul).withChildren: - field(tok(Expression) *> onlyChild(embed[Int])) - ~ field(tok(Expression) *> onlyChild(embed[Int])) - ~ eof, - ), - ).rewrite: (left, right) => - splice(Expression(Node.Embed[Int](left * right))) - | on( - tok(Expression) *> onlyChild( - tok(Div).withChildren: - field(tok(Expression) *> onlyChild(embed[Int])) - ~ field(tok(Expression) *> onlyChild(embed[Int])) - ~ eof, - ), - ).rewrite: (left, right) => - splice(Expression(Node.Embed[Int](left / right))) - end rules - end EvaluatorPass - - object StripExpressionPass extends Pass: - val wellformed = prevWellformed.makeDerived: - Node.Top ::=! embedded[Int] - val rules = pass(once = true, strategy = pass.bottomUp) - .rules: - on(tok(Expression) *> onlyChild(embed[Int])).rewrite: i => - splice(Node.Embed(i)) - end StripExpressionPass -end CalcEvaluator diff --git a/langs/calc/CalcParser.scala b/langs/calc/CalcParser.scala deleted file mode 100644 index a8ed8fb..0000000 --- a/langs/calc/CalcParser.scala +++ /dev/null @@ -1,154 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.langs.calc - -import cats.syntax.all.given - -import forja.* -import forja.dsl.* -import forja.source.Reader -import forja.wf.Wellformed - -import lang.* - -object CalcParser extends PassSeq: - import Reader.* - import CalcReader.* - - lazy val passes = List( - GroupIsExpression, - MulDivPass, - AddSubPass, - StripGroups, - ) - - def inputWellformed: Wellformed = CalcReader.wellformed - - object GroupIsExpression extends Pass: - val wellformed = prevWellformed.makeDerived: - Node.Top.removeCases(Group) - Expression.addCases(Group) - val rules = pass(strategy = pass.bottomUp, once = true) - .rules: - on(tok(Group)).rewrite: group => - splice(Expression(group.unparent())) - end GroupIsExpression - - object MulDivPass extends Pass: - val wellformed = prevWellformed.makeDerived: - Node.Top.removeCases(MulOp, DivOp) - Group.removeCases(MulOp, DivOp) - Expression.addCases(Mul, Div) - Mul ::= fields( - Expression, - Expression, - ) - Div ::= fields( - Expression, - Expression, - ) - end wellformed - val rules = pass(once = false, strategy = pass.topDown) - .rules: - on( - field(tok(Expression)) - ~ skip(tok(MulOp)) - ~ field(tok(Expression)) - ~ trailing, - ).rewrite: (left, right) => - splice( - Expression( - Mul( - left.unparent(), - right.unparent(), - ), - ), - ) - | on( - field(tok(Expression)) - ~ skip(tok(DivOp)) - ~ field(tok(Expression)) - ~ trailing, - ).rewrite: (left, right) => - splice( - Expression( - Div( - left.unparent(), - right.unparent(), - ), - ), - ) - end rules - end MulDivPass - - object AddSubPass extends Pass: - val wellformed = prevWellformed.makeDerived: - Node.Top.removeCases(AddOp, SubOp) - Expression.addCases(Add, Sub) - Add ::= fields( - Expression, - Expression, - ) - Sub ::= fields( - Expression, - Expression, - ) - end wellformed - val rules = pass(once = false, strategy = pass.topDown) - .rules: - on( - field(tok(Expression)) - ~ skip(tok(AddOp)) - ~ field(tok(Expression)) - ~ trailing, - ).rewrite: (left, right) => - splice( - Expression( - Add( - left.unparent(), - right.unparent(), - ), - ), - ) - | on( - field(tok(Expression)) - ~ skip(tok(SubOp)) - ~ field(tok(Expression)) - ~ trailing, - ).rewrite: (left, right) => - splice( - Expression( - Sub( - left.unparent(), - right.unparent(), - ), - ), - ) - end rules - end AddSubPass - - object StripGroups extends Pass: - val wellformed = prevWellformed.makeDerived: - Expression.removeCases(Group) - val rules = pass(strategy = pass.bottomUp, once = true) - .rules: - on( - tok(Expression) *> onlyChild: - tok(Group) *> onlyChild: - tok(Expression), - ).rewrite: expr => - splice(expr.unparent()) - end StripGroups -end CalcParser diff --git a/langs/calc/CalcReader.scala b/langs/calc/CalcReader.scala deleted file mode 100644 index 7eb2e48..0000000 --- a/langs/calc/CalcReader.scala +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.langs.calc - -import cats.syntax.all.given - -import forja.* -import forja.Builtin.{Error, SourceMarker} -import forja.dsl.* -import forja.source.{Reader, SourceRange} -import forja.wf.Wellformed - -import lang.* - -object CalcReader extends Reader: - import Reader.* - object AddOp extends Token, Token.ShowSource - object SubOp extends Token, Token.ShowSource - object MulOp extends Token, Token.ShowSource - object DivOp extends Token, Token.ShowSource - object Group extends Token - - override lazy val wellformed = Wellformed: - val content = - repeated(choice(Expression, AddOp, SubOp, MulOp, DivOp, Group)) - Node.Top ::= content - - Number ::= Atom - AddOp ::= Atom - SubOp ::= Atom - MulOp ::= Atom - DivOp ::= Atom - Group ::= content - - Expression ::= choice(Number) - end wellformed - - private val digit: Set[Char] = ('0' to '9').toSet - private val whitespace: Set[Char] = Set(' ', '\n', '\t') - - private lazy val unexpectedEOF: Manip[SourceRange] = - consumeMatch: m => - addChild(Error("unexpected EOF", SourceMarker(m))) - *> Manip.pure(m) - - protected lazy val rules: Manip[SourceRange] = - commit: - bytes - .selecting[SourceRange] - .onOneOf(whitespace): - extendThisNodeWithMatch(rules) - .onOneOf(digit): - numberMode - .on('+'): - consumeMatch: m => - addChild(AddOp(m)) - *> rules - .on('-'): - consumeMatch: m => - addChild(SubOp(m)) - *> rules - .on('*'): - consumeMatch: m => - addChild(MulOp(m)) - *> rules - .on('/'): - consumeMatch: m => - addChild(DivOp(m)) - *> rules - .on('('): - consumeMatch: m => - addChild(Group(m)) - *> atFirstChild(rules) - .on(')'): - extendThisNodeWithMatch: - atParent(rules) - | consumeMatch: m => - addChild(Error("unexpected end of group", SourceMarker(m))) - *> rules - .fallback: - bytes.selectOne: - consumeMatch: m => - addChild(Error("invalid byte", SourceMarker(m))) - *> rules - | consumeMatch: m => - on(theTop).check - *> Manip.pure(m) - | unexpectedEOF - - private lazy val numberMode: Manip[SourceRange] = - commit: - bytes - .selecting[SourceRange] - .onOneOf(digit)(numberMode) - .fallback: - consumeMatch: m => - m.decodeString().toIntOption match - case Some(value) => - addChild( - Expression( - Number(m), - ), - ) - *> rules - case None => - addChild(Error("invalid number format", SourceMarker(m))) - *> rules diff --git a/langs/calc/README.md b/langs/calc/README.md deleted file mode 100644 index a4f7426..0000000 --- a/langs/calc/README.md +++ /dev/null @@ -1,69 +0,0 @@ -# ```calc/``` - -## Overview -A parser and evaluator for arithmetic expressions given in string inputs; supporting addition, subtraction, multiplication, and division operations. - - -## Motivation -The calculator exists as a practical example demonstrating how Forja can be leveraged to parse languages and manipulate ASTs, with many of Forja's core features such as ```Wellformed```, ```PassSeq```, and ```SeqPattern``` being used. - - -## Components - -### 1. ```package.scala``` -Defines all of the ```Token``` types that are used and provides a wellformed definition representing how the AST should be structured once fully built. - - -### 2. ```CalcReader.scala``` -```CalcReader``` is the lexer that converts input strings into tokens. - -It contains a wellformed definition of the initial token types with ```Number``` tokens that're wrapped around ```Expression``` and ```Op``` tokens for operations. -To support arithmetic grouping, `()` are matched as nested `Group` tokens, which is used by the parser to force precedences not implied by the operators (like most programming language parsers). - -The ```rules``` method uses byte-level pattern matching to create tokens for numbers and operators, skipping whitespace and flagging unrecognized bytes as invalid. - -### 3. ```CalcParser.scala``` -```CalcParser``` is the parser, transforming the flat list of tokens into a structured AST. - -The wellformed definition adds new ```Operation``` token types that have 2 ```Expression``` children. Also, ```Expression``` tokens have a new definition, being able to wrap both ```Number``` tokens as well as ```Operation``` tokens. - -The passes are a sequence of transformations defined in the `passes` field, in this consisting of 2 binary operation parsing passes, and 2 passes related to `Group` tokens, which represent grouping with `()`. - -First, the `GroupIsExpression` pass makes `Group` nodes into `Expression` nodes, so that arithmetic operator parsing treats them as already parsed objects. -Then, ```MulDivPass``` and ```AddSubPass``` both create nested expressions, replacing instances of `Expression Op Expression` with a single `Expression` node that looks like this: -``` -Expression( - Operation( - Expression, - Expression - ) -) -``` - -```MulDivPass``` is executed before ```AddSubPass``` to create precedence, allowing multiplication and division operations to be nested deeper than addition and subtraction operations in the AST. - -Finally, the `StripGroups` pass removes nested `Expression(Group(Expression(...)))` objects, replacing them with the innermost `Expression`. -This is the only valid case, where the group could be parsed as a single nested expression. -In cases where there is a group but the nested expression could not be parsed (or there was no nested expression, as in the text `( )`), we could add extra rules to perform custom error reporting. -Interestingly, unlike in other parsing methods, this rule will have easy idiomatic access to both the incomplete parse, and the surrounding `()` context. -With the right rules, this can enable rich error reporting. - -### 4. ```CalcEvaluator.scala``` -```CalcEvaluator``` simplifies the AST and computes the value of the arithmetic expression. - -The wellformed definition is imported from ```package.scala```, picking up with the AST structure of where ```CalcParser``` left off. -Each pass is annotated with its own input / output grammars, expressed as changes to the previous pass's output grammar. -- `ConstantsPass` converts each number to a native Scala `Int` for easier arithmetic, showing an example of the `Node.Embed` feature. -- `EvaluatorPass` is a ruleset that describes basic arithmetic evaluation, which will fold a wellformed tree into a single node of the form `Expression(Node.Embed[Int](???))`. -- `StripExpressionPass` simply cleans up the previous pass's tree, leaving just `Node.Embed[Int](???)` containing the result of evaluating the expression. - - -## Usage -To learn how to use the calculator, ```package.test.scala``` contains methods (```parse```, ```read```, ```evaluate```) that execute the different components of the calculator. - - -## Example -The following images demonstrates the state of the AST after each pass with the input "5 + 3 * 4". Viewing the state of the AST after each pass can also be done by running calculator test cases with the tracer enabled. - -![example1](img/example1.jpg) -![exampel2](img/example2.jpg) diff --git a/langs/calc/img/example1.jpg b/langs/calc/img/example1.jpg deleted file mode 100644 index 6ba2279..0000000 Binary files a/langs/calc/img/example1.jpg and /dev/null differ diff --git a/langs/calc/img/example2.jpg b/langs/calc/img/example2.jpg deleted file mode 100644 index ac2ef1b..0000000 Binary files a/langs/calc/img/example2.jpg and /dev/null differ diff --git a/langs/calc/package.mill b/langs/calc/package.mill deleted file mode 100644 index 6f1591a..0000000 --- a/langs/calc/package.mill +++ /dev/null @@ -1,9 +0,0 @@ -package build.langs.calc - -import mill.*, scalalib.* - -object `package` extends build.ForjaModule: - def moduleDeps = Seq(build.forja) - - object test extends ForjaTests -end `package` diff --git a/langs/calc/package.scala b/langs/calc/package.scala deleted file mode 100644 index 99edb16..0000000 --- a/langs/calc/package.scala +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.langs.calc - -import cats.syntax.all.given - -import forja.* -import forja.dsl.* -import forja.source.SourceRange -import forja.wf.WellformedDef - -object lang extends WellformedDef: - lazy val topShape: Shape = repeated(Expression) - - object Expression - extends t( - choice( - Number, - Add, - Sub, - Mul, - Div, - ), - ) - - object Number extends t(Atom), Token.ShowSource - - object Add - extends t( - fields( - Expression, - Expression, - ), - ) - object Sub - extends t( - fields( - Expression, - Expression, - ), - ) - object Mul - extends t( - fields( - Expression, - Expression, - ), - ) - object Div - extends t( - fields( - Expression, - Expression, - ), - ) - -object read: - def fromSourceRange(sourceRange: SourceRange): Node.Top = - CalcReader(sourceRange) diff --git a/langs/calc/package.test.scala b/langs/calc/package.test.scala deleted file mode 100644 index 1d53b51..0000000 --- a/langs/calc/package.test.scala +++ /dev/null @@ -1,360 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.langs.calc - -import forja.* -import forja.dsl.* -import forja.manip.RewriteDebugTracer -import forja.source.{Source, SourceRange} - -import Builtin.{Error, SourceMarker} - -class CalcTests extends munit.FunSuite: - extension (str: String) - def read: Node.Top = - forja.langs.calc.read - .fromSourceRange(SourceRange.entire(Source.fromString(str))) - - def parse: Node.Top = - val top = read - - // format: off - // instrumentWithTracer(Manip.RewriteDebugTracer(os.pwd / "dbg_calc_parser_passes")): - CalcParser(top) - // format: on - - // os.write.over( - // os.pwd / "dbg_calc_parser" / "test_output.dbg", - // top.toPrettyWritable(CalcReader.wellformed), - // createFolders = true - // ) - - top - - def evaluate: Node.Top = - val top = parse - - // format: off - instrumentWithTracer(RewriteDebugTracer(os.pwd / "dbg_calc_evaluator_passes")): - CalcEvaluator(top) - // format: on - - // os.write.over( - // os.pwd / "dbg_calc_evaluator" / "test_output.dbg", - // top.toPrettyWritable(CalcReader.wellformed), - // createFolders = true - // ) - - top - - test("empty string"): - assertEquals("".read, Node.Top()) - - test("only whitespace"): - assertEquals(" \n\t".read, Node.Top()) - - test("error: invalid character"): - assertEquals("k".read, Node.Top(Error("invalid byte", SourceMarker("k")))) - - test("single number"): - assertEquals( - "5".read, - Node.Top( - lang.Expression( - lang.Number("5"), - ), - ), - ) - - test("read basic addition"): - assertEquals( - "5 + 11".read, - Node.Top( - lang.Expression( - lang.Number("5"), - ), - CalcReader.AddOp("+"), - lang.Expression( - lang.Number("11"), - ), - ), - ) - - test("read grouped"): - assertEquals( - "(2 + 3) * 4".read, - Node.Top( - CalcReader.Group( - lang.Expression(lang.Number("2")), - CalcReader.AddOp("+"), - lang.Expression(lang.Number("3")), - ), - CalcReader.MulOp("*"), - lang.Expression(lang.Number("4")), - ), - ) - - test("read grouped grouped"): - assertEquals( - "((2 + 3) * 4)".read, - Node.Top( - CalcReader.Group( - CalcReader.Group( - lang.Expression(lang.Number("2")), - CalcReader.AddOp("+"), - lang.Expression(lang.Number("3")), - ), - CalcReader.MulOp("*"), - lang.Expression(lang.Number("4")), - ), - ), - ) - - test("read basic multiplication"): - assertEquals( - "5 * 11".read, - Node.Top( - lang.Expression( - lang.Number("5"), - ), - CalcReader.MulOp("*"), - lang.Expression( - lang.Number("11"), - ), - ), - ) - - test("read full calculation"): - assertEquals( - "5 + 11 * 4".read, - Node.Top( - lang.Expression( - lang.Number("5"), - ), - CalcReader.AddOp("+"), - lang.Expression( - lang.Number("11"), - ), - CalcReader.MulOp("*"), - lang.Expression( - lang.Number("4"), - ), - ), - ) - - test("simple addition parse"): - assertEquals( - "5 + 11".parse, - Node.Top( - lang - .Expression( - lang - .Add( - lang.Expression( - lang.Number("5"), - ), - lang.Expression( - lang.Number("11"), - ), - ) - .at("5 + 11"), - ) - .at("5 + 11"), - ), - ) - - test("simple multiplication parse"): - assertEquals( - "5 * 11".parse, - Node.Top( - lang - .Expression( - lang - .Mul( - lang.Expression( - lang.Number("5"), - ), - lang.Expression( - lang.Number("11"), - ), - ) - .at("5 * 11"), - ) - .at("5 * 11"), - ), - ) - - test("full calculation parse"): - assertEquals( - "5 + 11 * 4".parse, - Node.Top( - lang - .Expression( - lang - .Add( - lang.Expression( - lang.Number("5"), - ), - lang - .Expression( - lang - .Mul( - lang.Expression( - lang.Number("11"), - ), - lang.Expression( - lang.Number("4"), - ), - ) - .at("11 * 4"), - ) - .at("11 * 4"), - ) - .at("5 + 11 * 4"), - ) - .at("5 + 11 * 4"), - ), - ) - - test("grouped parse"): - assertEquals( - "((2 + 3) * 4)".parse, - Node.Top( - lang.Expression( - lang.Mul( - lang.Expression( - lang.Add( - lang.Expression(lang.Number("2")), - lang.Expression(lang.Number("3")), - ), - ), - lang.Expression(lang.Number("4")), - ), - ), - ), - ) - - test("full calculation 4 parse"): - assertEquals( - "5 * 4 + 4 / 2 - 6 * 2".parse, - Node.Top( - lang - .Expression( - lang - .Sub( - lang - .Expression( - lang - .Add( - lang - .Expression( - lang - .Mul( - lang.Expression( - lang.Number("5"), - ), - lang.Expression( - lang.Number("4"), - ), - ) - .at("5 * 4"), - ) - .at("5 * 4"), - lang - .Expression( - lang - .Div( - lang.Expression( - lang.Number("4"), - ), - lang.Expression( - lang.Number("2"), - ), - ) - .at("4 / 2"), - ) - .at("4 / 2"), - ) - .at("5 * 4 + 4 / 2"), - ) - .at("5 * 4 + 4 / 2"), - lang - .Expression( - lang - .Mul( - lang.Expression( - lang.Number("6"), - ), - lang.Expression( - lang.Number("2"), - ), - ) - .at("6 * 2"), - ) - .at("6 * 2"), - ) - .at("5 * 4 + 4 / 2 - 6 * 2"), - ) - .at("5 * 4 + 4 / 2 - 6 * 2"), - ), - ) - - test("addition calculation"): - assertEquals( - "5 + 11".evaluate, - Node.Top( - Node.Embed(16), - ), - ) - - test("multiplication calculation"): - assertEquals( - "5 * 11".evaluate, - Node.Top( - Node.Embed(55), - ), - ) - - test("full calculation"): - assertEquals( - "5 + 11 * 4".evaluate, - Node.Top( - Node.Embed(49), - ), - ) - - test("full calculation 2"): - assertEquals( - "5 * 4 + 4 / 2".evaluate, - Node.Top( - Node.Embed(22), - ), - ) - - test("full calculation 3"): - assertEquals( - "5 * 4 + 4 / 2 - 6".evaluate, - Node.Top( - Node.Embed(16), - ), - ) - - test("full calculation 4"): - assertEquals( - "5 * 4 + 4 / 2 - 6 * 2".evaluate, - Node.Top( - Node.Embed(10), - ), - ) diff --git a/langs/calc/src/CalcAST.scala b/langs/calc/src/CalcAST.scala deleted file mode 100644 index 32f7d96..0000000 --- a/langs/calc/src/CalcAST.scala +++ /dev/null @@ -1,33 +0,0 @@ -package forja.langs.calc - -import forja.syntax.* -import forja.{Token, Wf} - -trait CalcAST extends Wf: - lazy val Expression: TokenWf = Token( - Number - | Add - | Sub - | Mul - | Div, - ) - lazy val Number = Token(cc.embed[Int]) - lazy val Add = Token( - Expression, - Expression, - ) - lazy val Sub = Token( - Expression, - Expression, - ) - lazy val Mul = Token( - Expression, - Expression, - ) - lazy val Div = Token( - Expression, - Expression, - ) -end CalcAST - -object CalcAST extends CalcAST diff --git a/langs/calc/src/CalcEvaluator.scala b/langs/calc/src/CalcEvaluator.scala deleted file mode 100644 index 49dadf4..0000000 --- a/langs/calc/src/CalcEvaluator.scala +++ /dev/null @@ -1,79 +0,0 @@ -package forja.langs.calc - -import forja.Node.NodeSpan -import forja.syntax.* -import forja.{Node, Pass} - -object CalcEvaluator extends Pass.MultiPass: - def validateInput = CalcAST.Expression.validate - - object evaluateExpr extends Pass.RewritePass: - def evalAdd = on( - CalcAST.Expression( - CalcAST - .Add( - +CalcAST.Expression(+CalcAST.Number(+cc.embed[Int])), - +CalcAST.Expression(+CalcAST.Number(+cc.embed[Int])), - ) - .rewrite: (lhs, rhs) => - CalcAST.Number(lhs + rhs), - ), - ).rewriteInPattern - - def evalSub = on( - CalcAST.Expression( - CalcAST - .Sub( - +CalcAST.Expression(+CalcAST.Number(+cc.embed[Int])), - +CalcAST.Expression(+CalcAST.Number(+cc.embed[Int])), - ) - .rewrite: (lhs, rhs) => - CalcAST.Number(lhs - rhs), - ), - ).rewriteInPattern - - def evalMul = on( - CalcAST.Expression( - CalcAST - .Mul( - +CalcAST.Expression(+CalcAST.Number(+cc.embed[Int])), - +CalcAST.Expression(+CalcAST.Number(+cc.embed[Int])), - ) - .rewrite: (lhs, rhs) => - CalcAST.Number(lhs * rhs), - ), - ).rewriteInPattern - - def evalDivNonZero = on( - CalcAST.Expression( - CalcAST - .Div( - +CalcAST.Expression(+CalcAST.Number(+cc.embed[Int])), - +CalcAST.Expression(+CalcAST.Number(+cc.embed[Int].filter(_ != 0))), - ) - .rewrite: (lhs, rhs) => - CalcAST.Number(lhs / rhs), - ), - ).rewriteInPattern - - def evalDivByZeroErr = on( - CalcAST.Expression( - CalcAST.Div( - CalcAST.Expression(CalcAST.Number(cc.embed[Int])), - NodeSpan( - !CalcAST.Expression(CalcAST.Number(cc.lit(0))), - ).rewrite: rhs => - Node.error("division by 0")(rhs), - ), - ), - ).rewriteInPattern - end evaluateExpr - - trait Evaluated extends CalcAST: - override lazy val Expression: TokenWf = - CalcAST.Expression.replace(CalcAST.Number) - end Evaluated - object Evaluated extends Evaluated - - def validateOutput = Evaluated.Expression.validate -end CalcEvaluator diff --git a/langs/calc/src/CalcParser.scala b/langs/calc/src/CalcParser.scala deleted file mode 100644 index c1c99d3..0000000 --- a/langs/calc/src/CalcParser.scala +++ /dev/null @@ -1,186 +0,0 @@ -package forja.langs.calc - -import forja.Node.NodeSpan -import forja.syntax.* -import forja.{Node, Pass, Token} - -object CalcParser extends Pass.MultiPass: - import CalcReader.Tokenized - lazy val ParseHead = Token() - lazy val ParseLimit = Token() - lazy val ParseOngoing = Token() - def reader = CalcReader - - object addParseHead extends Pass.RewritePass: - def addParseHead = on( - Tokenized.Root( - NodeSpan(cc.not(ParseHead(`...`))).rewrite: _ => - NodeSpan(ParseHead(0)), - `...`, - ), - ).rewriteInPattern - end addParseHead - - object parseAST extends Pass.RewritePass: - def parseNumber = on( - !ParseHead(cc.embed[Int]), - +Tokenized.Number(+cc.embed[Int]), - ).rewrite: (hd, num) => - NodeSpan( - CalcAST.Expression(CalcAST.Number(num)), - hd, - ) - - def parseRootLimit = on( - Tokenized.Root( - `...`, - ParseHead(cc.embed[Int]).rewrite: _ => - ParseLimit(), - ), - ).rewriteInPattern - - def parseGroupIn = on( - +ParseHead(+cc.embed[Int]), - !Tokenized.Group( - NodeSpan().rewrite: _ => - ParseHead(0), - `...`, - ), - ).rewrite: (prec, grp) => - NodeSpan( - ParseOngoing(prec), - grp, - ) - - def parseGroupLimit = on( - ParseOngoing(`...`), - Tokenized.Group( - `...`, - ParseHead(cc.embed[Int]).rewrite: _ => - ParseLimit(), - ), - ).rewriteInPattern - - def parseGroupOut = on( - +ParseOngoing(+cc.embed[Int]), - +Tokenized.Group( - !CalcAST.Expression(`...`), - ParseLimit(), - ), - ).rewrite: (prec, expr) => - NodeSpan(expr, ParseHead(prec)) - - def parseAddSubIn = on( - !CalcAST.Expression(`...`), - +ParseHead(+cc.embed[Int].filter(_ <= 1)), - !(Tokenized.Add() | Tokenized.Sub()), - ).rewrite: (lhs, prec, op) => - NodeSpan( - lhs, - ParseOngoing(prec), - op, - ParseHead(1), - ) - - def parseAddSubOut = on( - !CalcAST.Expression(`...`), - +ParseOngoing(+cc.embed[Int]), - !(Tokenized.Add() | Tokenized.Sub()), - !CalcAST.Expression(`...`), - ParseLimit(), - ).rewrite: (lhs, prec, op, rhs) => - NodeSpan( - CalcAST.Expression( - op.tokenOption.get( - lhs, - rhs, - ), - ), - ParseHead(prec), - ) - - def parseMulDivIn = on( - !CalcAST.Expression(`...`), - +ParseHead(+cc.embed[Int]), - !(Tokenized.Mul() | Tokenized.Div()), - ).rewrite: (lhs, prec, op) => - NodeSpan( - lhs, - ParseOngoing(prec), - op, - ParseHead(2), - ) - - def parseMulDivOut = on( - !CalcAST.Expression(`...`), - +ParseOngoing(+cc.embed[Int]), - !(Tokenized.Mul() | Tokenized.Div()), - !CalcAST.Expression(`...`), - ParseLimit(), - ).rewrite: (lhs, prec, op, rhs) => - NodeSpan( - CalcAST.Expression( - op.tokenOption.get( - lhs, - rhs, - ), - ), - ParseHead(prec), - ) - - def parseMulDivLimit = on( - +ParseHead(+cc.embed[Int].filter(_ >= 2)), - !(Tokenized.Add() | Tokenized.Sub()), - ).rewrite: (prec, op) => - NodeSpan( - ParseLimit(), - op, - ) - end parseAST - - object stripMeta extends Pass.RewritePass: - def successCondition = on( - +Tokenized.Root( - !CalcAST.Expression(`...`), - ParseLimit(), - ), - ).rewrite: expr => - // TODO: why does removing NodeSpan cause infinite loop (???) - NodeSpan(expr) - - def unexpectedEmptyInput = on( - !Tokenized.Root( - ParseLimit(), - ), - ).rewrite: node => - Node.error(s"input is empty")(node) - - def unexpectedOperator = on( - !ParseHead(cc.embed[Int]), - !(Tokenized.Add() | Tokenized.Sub() | Tokenized.Mul() | Tokenized.Div()), - ).rewrite: (hd, op) => - Node.error(s"unexpected operator")(hd, op) - - def missingRhs = on( - !(Tokenized.Add() | Tokenized.Sub() | Tokenized.Mul() | Tokenized.Div()), - !ParseLimit(), - ).rewrite: (op, lm) => - Node.error(s"missing rhs")(op, lm) - - def unexpectedEmptyGroup = on( - !Tokenized.Group( - CalcParser.ParseLimit(), - ), - ).rewrite: grp => - Node.error(s"empty group")(grp) - - def tooManyExpressions = on( - !CalcAST.Expression(`...`), - +cc.rep1(NodeSpan(!CalcAST.Expression(`...`))), - !ParseLimit(), - ).rewrite: (expr, exprs, lm) => - Node.error(s"too many expressions")(((expr +: exprs) :+ lm)*) - end stripMeta - - def validateAST = CalcAST.Expression.validate -end CalcParser diff --git a/langs/calc/src/CalcReader.scala b/langs/calc/src/CalcReader.scala deleted file mode 100644 index 22b5a34..0000000 --- a/langs/calc/src/CalcReader.scala +++ /dev/null @@ -1,138 +0,0 @@ -package forja.langs.calc - -import forja.Wf.{Shape, TokenWf} -import forja.syntax.* -import forja.{Node, NodeSpan, Pass, SourceRange, Token, Wf} - -object CalcReader extends Pass.MultiPass: - trait Input extends Wf: - lazy val Root = Token( - ParseHead, - cc.embed[SourceRange], - ) - lazy val ParseHead = Token() - end Input - - object Input extends Input - def validateInputRoot = Input.Root.validate - - object readTokens extends Pass.RewritePass: - private val tokenBytes = List[(Char, TokenWf)]( - '+' -> Tokenized.Add, - '-' -> Tokenized.Sub, - '*' -> Tokenized.Mul, - '/' -> Tokenized.Div, - ).map((b, wf) => b.toByte -> wf.token) - - private val numberBytes = ('0' to '9').map(_.toByte).toSet - - def popByte = on( - Input.ParseHead(), - cc.embed[SourceRange] - .filter(_.nonEmpty) - .rewrite: rng => - if numberBytes(rng.head) - then - NodeSpan( - rng.head, - rng.tail.iterator.takeWhile(numberBytes).map(cc.lit), - rng.tail.dropWhile(numberBytes), - ) - else NodeSpan(rng.head, rng.tail), - ).rewriteInPattern - end popByte - - def skipWhitespace = on( - !Input.ParseHead(), - cc.lit(' '.toByte, '\n'.toByte, '\t'.toByte, '\r'.toByte), - ).rewrite: hd => - hd - end skipWhitespace - - def openGroup = on( - !Input.ParseHead(), - cc.lit('('.toByte), - +cc.embed[SourceRange], - ).rewrite: (hd, rng) => - Tokenized.Group( - hd, - rng, - ) - end openGroup - - def closeGroup = on( - !Tokenized.Group( - `...`, - +NodeSpan( - !Input.ParseHead(), - cc.lit(')'.toByte), - +cc.embed[SourceRange], - ).rewriteMap: p => - (p, NodeSpan()), - ), - ).rewrite: (g, hd, rng) => - NodeSpan(g, hd, rng) - end closeGroup - - def parseToken = on( - !Input.ParseHead(), - +(tokenBytes.map((b, tok) => cc.lit(b).map((_, tok))).reduce(_ | _)), - ).rewrite: (hd, _, tok) => - NodeSpan(tok(), hd) - end parseToken - - def doneReading = on( - Input.ParseHead(), - cc.embed[SourceRange].filter(_.isEmpty), - ).rewrite: _ => - NodeSpan() - end doneReading - - def readNumber = on( - !Input.ParseHead(), - +cc.rep1(cc.embed[Byte].filter(numberBytes)), - +cc.embed[SourceRange], - ).rewrite: (hd, bytes, last) => - val num: Int = bytes.iterator.map(_.toChar).mkString.toInt - NodeSpan(Tokenized.Number(num), hd, last) - end readNumber - end readTokens - - object errorCases extends Pass.RewritePass: - def unmatchedClosingBrace = on( - !Input.ParseHead(), - +cc.lit(')'.toByte), - ).rewrite: (hd, close) => - Node.error(s"unmatched closing brace")(hd, Node.embed(close)) - - def invalidChar = on( - !Input.ParseHead(), - +cc.embed[Byte].filter(_ != ')'), - ).rewrite: (hd, ch) => - Node.error(s"invalid byte")(hd, Node.embed(ch)) - end errorCases - - trait Tokenized extends Input, CalcAST: - def anyTok: Shape = - Number - | Add - | Sub - | Mul - | Div - | Group - end anyTok - override lazy val Root = Input.Root.replace( - cc.rep(anyTok), - ) - override lazy val Add = CalcAST.Add.replace() - override lazy val Sub = CalcAST.Sub.replace() - override lazy val Mul = CalcAST.Mul.replace() - override lazy val Div = CalcAST.Div.replace() - lazy val Group = Token( - cc.rep(anyTok), - ) - end Tokenized - - object Tokenized extends Tokenized - def validateTokenizedRoot = Tokenized.Root.validate -end CalcReader diff --git a/langs/calc/test/src/CalcEvaluatorTest.scala b/langs/calc/test/src/CalcEvaluatorTest.scala deleted file mode 100644 index 810ed1f..0000000 --- a/langs/calc/test/src/CalcEvaluatorTest.scala +++ /dev/null @@ -1,68 +0,0 @@ -package forja.langs.calc - -import utest.* -import forja.Pass -import forja.Wf.TokenWf -import forja.SourceRange -import forja.Node -import forja.TestUtil.assertEqualDiff - -class CalcEvaluatorTest extends TestSuite: - def evalString(using path: framework.TestPath)(): Node = - val ast = CalcParser.perform( - CalcReader.Input.Root( - CalcReader.Input.ParseHead(), - SourceRange(path.value.last), - ), - ) - Predef.assert(!ast.containsError, s"parsing error in $ast") - CalcEvaluator.perform(ast) - end evalString - - def tests = Tests: - test("2 + 2") { - assertEqualDiff(evalString(), CalcAST.Expression(CalcAST.Number(4))) - } - - test("64 / 0") { - assertEqualDiff( - evalString(), - CalcAST.Expression( - CalcAST.Div( - CalcAST.Expression(CalcAST.Number(64)), - Node.error("division by 0")( - CalcAST.Expression(CalcAST.Number(0)), - ), - ), - ), - ) - } - - test("5 * 4 + 4 / 2 - 6 * 2") { - assertEqualDiff( - evalString(), - CalcAST.Expression(CalcAST.Number(5 * 4 + 4 / 2 - 6 * 2)), - ) - } - - test("ModelChecker") { - CalcEvaluatorTest.CalcEvaluatorModelChecker.assertCheck() - } - end tests -end CalcEvaluatorTest - -object CalcEvaluatorTest: - object CalcEvaluatorModelChecker extends Pass.RewritePass.ModelChecker: - def initRepMax: Int = 0 - def initTreeLevels: Int = 4 - def inputWf: TokenWf = CalcAST.Expression - def outputWf: TokenWf = CalcEvaluator.Evaluated.Expression - - export CalcEvaluator.evaluateExpr - - object embedInt extends Pass.RewritePass.EmbedGenerator[Int]: - def generate: Iterator[Int] = - Iterator(-1, 0, 1, 2, 42) - end embedInt - end CalcEvaluatorModelChecker -end CalcEvaluatorTest diff --git a/langs/calc/test/src/CalcParserTest.scala b/langs/calc/test/src/CalcParserTest.scala deleted file mode 100644 index 5aae3ce..0000000 --- a/langs/calc/test/src/CalcParserTest.scala +++ /dev/null @@ -1,183 +0,0 @@ -package forja.langs.calc - -import utest.* -import forja.SourceRange -import forja.Node - -import forja.TestUtil.assertEqualDiff - -import forja.syntax.* -import forja.Pass - -class CalcParserTest extends TestSuite: - def parseString(using path: framework.TestPath)(): Node = - CalcParser.perform( - CalcReader.Input.Root( - CalcReader.Input.ParseHead(), - SourceRange(path.value.last), - ), - ) - end parseString - - def tests = Tests: - test("42") { - assertEqualDiff( - parseString(), - CalcAST.Expression( - CalcAST.Number(42), - ), - ) - } - - test("2 + 2") { - assertEqualDiff( - parseString(), - CalcAST.Expression( - CalcAST.Add( - CalcAST.Expression(CalcAST.Number(2)), - CalcAST.Expression(CalcAST.Number(2)), - ), - ), - ) - } - - test("1 + 2 + 3") { - assertEqualDiff( - parseString(), - CalcAST.Expression( - CalcAST.Add( - CalcAST.Expression(CalcAST.Number(1)), - CalcAST.Expression( - CalcAST.Add( - CalcAST.Expression(CalcAST.Number(2)), - CalcAST.Expression(CalcAST.Number(3)), - ), - ), - ), - ), - ) - } - - test("1 + (2 + 3)") { - assertEqualDiff( - parseString(), - CalcAST.Expression( - CalcAST.Add( - CalcAST.Expression(CalcAST.Number(1)), - CalcAST.Expression( - CalcAST.Add( - CalcAST.Expression(CalcAST.Number(2)), - CalcAST.Expression(CalcAST.Number(3)), - ), - ), - ), - ), - ) - } - - test("(1 + 2) + 3") { - assertEqualDiff( - parseString(), - CalcAST.Expression( - CalcAST.Add( - CalcAST.Expression( - CalcAST.Add( - CalcAST.Expression(CalcAST.Number(1)), - CalcAST.Expression(CalcAST.Number(2)), - ), - ), - CalcAST.Expression(CalcAST.Number(3)), - ), - ), - ) - } - - test("1 + 2 * 3") { - assertEqualDiff( - parseString(), - CalcAST.Expression( - CalcAST.Add( - CalcAST.Expression(CalcAST.Number(1)), - CalcAST.Expression( - CalcAST.Mul( - CalcAST.Expression(CalcAST.Number(2)), - CalcAST.Expression(CalcAST.Number(3)), - ), - ), - ), - ), - ) - } - - test("1 * 2 + 3") { - assertEqualDiff( - parseString(), - CalcAST.Expression( - CalcAST.Add( - CalcAST.Expression( - CalcAST.Mul( - CalcAST.Expression(CalcAST.Number(1)), - CalcAST.Expression(CalcAST.Number(2)), - ), - ), - CalcAST.Expression(CalcAST.Number(3)), - ), - ), - ) - } - - test("5 * 4 + 4 / 2 - 6 * 2") { - assertEqualDiff( - parseString(), - CalcAST.Expression( - CalcAST.Add( - CalcAST.Expression( - CalcAST.Mul( - CalcAST.Expression(CalcAST.Number(5)), - CalcAST.Expression(CalcAST.Number(4)), - ), - ), - CalcAST.Expression( - CalcAST.Sub( - CalcAST.Expression( - CalcAST.Div( - CalcAST.Expression(CalcAST.Number(4)), - CalcAST.Expression(CalcAST.Number(2)), - ), - ), - CalcAST.Expression( - CalcAST.Mul( - CalcAST.Expression(CalcAST.Number(6)), - CalcAST.Expression(CalcAST.Number(2)), - ), - ), - ), - ), - ), - ), - ) - } - - test("ModelChecker") { - CalcParserTest.modelChecker.assertCheck() - } - end tests -end CalcParserTest - -object CalcParserTest: - object modelChecker extends Pass.RewritePass.ModelChecker: - def initRepMax: Int = 5 - def initTreeLevels: Int = 2 - - export CalcParser.{addParseHead, parseAST, stripMeta} - - def inputWf: TokenWf = CalcParser.reader.Tokenized.Root - def outputWf: TokenWf = CalcAST.Expression - - object embedInt extends Pass.RewritePass.EmbedGenerator[Int]: - def generate: Iterator[Int] = - Iterator(1, 2) - end generate - end embedInt - end modelChecker -end CalcParserTest diff --git a/langs/calc/test/src/CalcReaderTest.scala b/langs/calc/test/src/CalcReaderTest.scala deleted file mode 100644 index ba30071..0000000 --- a/langs/calc/test/src/CalcReaderTest.scala +++ /dev/null @@ -1,164 +0,0 @@ -package forja.langs.calc - -import utest.* -import forja.Node -import forja.SourceRange -import forja.syntax.* -import forja.TestUtil.assertEqualDiff -import forja.Pass - -import CalcReaderTest.* - -class CalcReaderTest extends TestSuite: - def parseString(using path: framework.TestPath)(): Node = - CalcReader.perform( - CalcReader.Input.Root( - CalcReader.Input.ParseHead(), - SourceRange(path.value.last), - ), - ) - end parseString - - val tests = Tests: - test("popByte") { - assertEqualDiff( - CalcReader.readTokens.popByte.pattern - .runPattern: - CalcReader.Input - .Root( - CalcReader.Input.ParseHead(), - SourceRange("42"), - ) - .children - .asEmptyNodeSpan - , - Some( - (), - CalcReader.Input - .Root( - CalcReader.Input.ParseHead(), - '4'.toByte, - '2'.toByte, - SourceRange(""), - ) - .children - .asEmptyNodeSpan - .expandRightMax, - ), - ) - } - - test("2 + 2") { - assertEqualDiff( - parseString(), - CalcReader.Tokenized.Root( - CalcReader.Tokenized.Number(2), - CalcReader.Tokenized.Add(), - CalcReader.Tokenized.Number(2), - ), - ) - } - test("256 +2 - 999* /0") { - assertEqualDiff( - parseString(), - CalcReader.Tokenized.Root( - CalcReader.Tokenized.Number(256), - CalcReader.Tokenized.Add(), - CalcReader.Tokenized.Number(2), - CalcReader.Tokenized.Sub(), - CalcReader.Tokenized.Number(999), - CalcReader.Tokenized.Mul(), - CalcReader.Tokenized.Div(), - CalcReader.Tokenized.Number(0), - ), - ) - } - test("") { - assertEqualDiff(parseString(), CalcReader.Tokenized.Root()) - } - test(" \n\t") { - assertEqualDiff(parseString(), CalcReader.Tokenized.Root()) - } - // TODO: invalid char k - test("5") { - assertEqualDiff( - parseString(), - CalcReader.Tokenized.Root( - CalcReader.Tokenized.Number(5), - ), - ) - } - test("5 + 11") { - assertEqualDiff( - parseString(), - CalcReader.Tokenized.Root( - CalcReader.Tokenized.Number(5), - CalcReader.Tokenized.Add(), - CalcReader.Tokenized.Number(11), - ), - ) - } - test("(2 + 3) * 4") { - assertEqualDiff( - parseString(), - CalcReader.Tokenized.Root( - CalcReader.Tokenized.Group( - CalcReader.Tokenized.Number(2), - CalcReader.Tokenized.Add(), - CalcReader.Tokenized.Number(3), - ), - CalcReader.Tokenized.Mul(), - CalcReader.Tokenized.Number(4), - ), - ) - } - test("((2 + 3) * 4)") { - assertEqualDiff( - parseString(), - CalcReader.Tokenized.Root( - CalcReader.Tokenized.Group( - CalcReader.Tokenized.Group( - CalcReader.Tokenized.Number(2), - CalcReader.Tokenized.Add(), - CalcReader.Tokenized.Number(3), - ), - CalcReader.Tokenized.Mul(), - CalcReader.Tokenized.Number(4), - ), - ), - ) - } - - test("ModelChecker") { - modelChecker.assertCheck() - } - end tests -end CalcReaderTest - -object CalcReaderTest: - object modelChecker extends Pass.RewritePass.ModelChecker: - def initTreeLevels: Int = 1 - def initRepMax: Int = 3 - def inputWf: TokenWf = CalcReader.Input.Root - def outputWf: TokenWf = CalcReader.Tokenized.Root - - export CalcReader.readTokens - export CalcReader.errorCases - - object genSourceRange extends Pass.RewritePass.EmbedGenerator[SourceRange]: - val byteBag = IArray[Byte]( - '\n', ' ', '\r', '\t', '+', '-', '*', '/', '(', ')', 'k', - ) ++ ('0' to '9').map(_.toByte) - def generate: Iterator[SourceRange] = - Iterator - .iterate(List(IArray.empty[Byte])): prefixes => - prefixes.flatMap: prefix => - byteBag.iterator - .map(_ +: prefix) - .takeWhile(_.head.length <= 4) - .flatten - .map(SourceRange(_)) - end generate - end genSourceRange - end modelChecker -end CalcReaderTest diff --git a/langs/package.mill b/langs/package.mill deleted file mode 100644 index 7dd6c0e..0000000 --- a/langs/package.mill +++ /dev/null @@ -1 +0,0 @@ -package build.langs diff --git a/langs/tla/ExprMarker.scala b/langs/tla/ExprMarker.scala deleted file mode 100644 index f8e090e..0000000 --- a/langs/tla/ExprMarker.scala +++ /dev/null @@ -1,371 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.langs.tla - -import cats.syntax.all.given - -import forja.* -import forja.dsl.* -import forja.wf.Wellformed - -import TLAReader.* - -object ExprMarker extends PassSeq: - - lazy val passes = List( - buildExpressions, - // removeNestedExpr - ) - - object ExprTry extends Token - def inputWellformed: Wellformed = TLAParser.outputWellformed - - def parsedChildrenSepBy( - parent: Token, - split: SeqPattern[?], - ): SeqPattern[List[Node]] = - leftSibling(ExprTry) *> - tok(parent) *> - children: - field( - repeatedSepBy(split)( - skip(ExprTry) - ~ field(lang.Expr) - ~ trailing, - ), - ) - ~ eof - end parsedChildrenSepBy - - // TODO: I wonder if this pattern is a bad idea - def firstUnmarkedChildStart( - paren: Token, - ): SeqPattern[Node] = - parent(leftSibling(ExprTry) *> paren) *> - not(leftSibling(anyNode)) *> - not(ExprTry) *> not(lang.Expr) *> anyNode - end firstUnmarkedChildStart - - def unMarkedChildSplit( - paren: Token, - split: Token, - ): SeqPattern[Node] = - parent(leftSibling(ExprTry) *> paren) *> - possibleExprTryToRight(split) - end unMarkedChildSplit - - def rightSiblingNotExprTry(): SeqPattern[Unit] = - rightSibling(not(lang.Expr) *> not(ExprTry)) - end rightSiblingNotExprTry - - def possibleExprTryToRight( - node: SeqPattern[Node], - ): SeqPattern[Node] = - node <* rightSiblingNotExprTry() - end possibleExprTryToRight - - object buildExpressions extends Pass: - val wellformed = prevWellformed.makeDerived: - val removedCases = Seq( - TLAReader.StringLiteral, - TLAReader.NumberLiteral, - TLAReader.TupleGroup, - // TODO: remove cases - ) - TLAReader.groupTokens.foreach: tok => - tok.removeCases(removedCases*) - tok.addCases(lang.Expr) - - lang.Expr.deleteShape() - lang.Expr.importFrom(lang.wf) - lang.Expr.addCases(lang.Expr) - - val rules = - pass(once = false, strategy = pass.bottomUp) - .rules: - // Parse Number and String Literals - on( - leftSibling(ExprTry) *> - field(TLAReader.NumberLiteral) - ~ trailing, - ).rewrite: lit => - splice(lang.Expr(lang.Expr.NumberLiteral().like(lit))) - | on( - leftSibling(ExprTry) *> - field(TLAReader.StringLiteral) - ~ trailing, - ).rewrite: lit => - splice(lang.Expr(lang.Expr.StringLiteral().like(lit))) - // Parse Id - | on( - leftSibling(ExprTry) *> - field(TLAReader.Alpha) - ~ trailing, - ).rewrite: id => - splice( - lang.Expr( - lang.Expr.OpCall( - lang.Id().like(id.unparent()), - lang.Expr.OpCall.Params(), - ), - ), - ) - // TupleLiteral is complete when all the elements are parsed - | on( - parsedChildrenSepBy(TLAReader.TupleGroup, `,`), - ).rewrite: exprs => - splice( - lang.Expr( - lang.Expr.TupleLiteral(exprs.iterator.map(_.unparent())), - ), - ) - // If the TupleLiteral not complete, mark the elements with ExprTry - // Mark the first child in the first pass, and then mark the rest in - // the second - | on( - firstUnmarkedChildStart(TLAReader.TupleGroup), - ).rewrite: first => - splice(ExprTry(), first.unparent()) - | on( - unMarkedChildSplit(TLAReader.TupleGroup, `,`), - ).rewrite: split => - splice(split.unparent(), ExprTry()) - // SetLiteral is complete when all the elements are parsed - | on( - parsedChildrenSepBy(TLAReader.BracesGroup, `,`), - ).rewrite: exprs => - splice( - lang.Expr( - lang.Expr.SetLiteral(exprs.iterator.map(_.unparent())), - ), - ) - // If the SetLiteral is not complete, mark the elements with ExprTry - // Mark the first child in the first pass, and then mark the rest in - // the second - | on( - firstUnmarkedChildStart(TLAReader.BracesGroup), - ).rewrite: first => - splice(ExprTry(), first.unparent()) - | on( - unMarkedChildSplit(TLAReader.BracesGroup, `,`), - ).rewrite: split => - splice(split.unparent(), ExprTry()) - // RecordLiteral is complete when all the elements are parsed - | on( - leftSibling(ExprTry) *> tok(TLAReader.SqBracketsGroup) *> - children: - field( - repeatedSepBy1(`,`)( - field(TLAReader.Alpha) - ~ skip(TLAReader.`|->`) - ~ skip(ExprTry) - ~ field(lang.Expr) - ~ trailing, - ), - ) - ~ eof, - ).rewrite: records => - splice( - lang.Expr( - lang.Expr.RecordLiteral( - records.iterator.map((id, expr) => - lang.Expr.RecordLiteral.Field( - lang.Id().like(id.unparent()), - expr.unparent(), - ), - ), - ), - ), - ) - // If the RecordLiteral is not complete, place ExprTry after each |-> - | on( - unMarkedChildSplit(TLAReader.SqBracketsGroup, `|->`), - ).rewrite: split => - splice(split.unparent(), ExprTry()) - // Parse Projection (Record field access) - | on( - leftSibling(ExprTry) *> - field(lang.Expr) - ~ skip(defns.`.`) - ~ field(TLAReader.Alpha) - ~ trailing, - ).rewrite: (expr, id) => - splice( - lang.Expr( - lang.Expr.Project( - expr.unparent(), - lang.Id().like(id.unparent()), - ), - ), - ) - // IF is complete when every branch is parsed. - | on( - leftSibling(ExprTry) *> - skip(defns.IF) - ~ skip(ExprTry) - ~ field(lang.Expr) - ~ skip(defns.THEN) - ~ skip(ExprTry) - ~ field(lang.Expr) - ~ skip(defns.ELSE) - ~ skip(ExprTry) - ~ field(lang.Expr) - ~ trailing, - ).rewrite: (pred, t, f) => - splice( - lang.Expr( - lang.Expr.If( - pred.unparent(), - t.unparent(), - f.unparent(), - ), - ), - ) - // If IF is not complete, place ExprTry before the pred and branches - | on( - possibleExprTryToRight( - tok(defns.IF) | tok(defns.THEN) | tok(defns.ELSE), - ), - ).rewrite: split => - splice(split.unparent(), ExprTry()) - // CASE is complete when every branch is parsed. - | on( - leftSibling(ExprTry) *> - skip(defns.CASE) - ~ field( - repeatedSepBy1(defns.`[]`)( - skip(ExprTry) - ~ field(lang.Expr) - ~ skip(TLAReader.`->`) - ~ skip(ExprTry) - ~ field(lang.Expr) - ~ trailing, - ), - ) - ~ field( - optional( - skip(defns.OTHER) - ~ skip(TLAReader.`->`) - ~ skip(ExprTry) - ~ field(lang.Expr) - ~ eof, - ), - ) - ~ trailing, - ).rewrite: (cases, other) => - splice( - lang.Expr( - lang.Expr.Case( - lang.Expr.Case.Branches( - cases.iterator.map((pred, branch) => - lang.Expr.Case.Branch(pred.unparent(), branch.unparent()), - ), - ), - lang.Expr.Case.Other( - other match - case None => lang.Expr.Case.None() - case Some(expr) => expr.unparent(), - ), - ), - ), - ) - // If the CASE is not complete, insert ExprTry after [], ->, and OTHER - // as well as before the first case. - | on( - possibleExprTryToRight(leftSibling(ExprTry) *> defns.CASE), - ).rewrite: c => - splice(c.unparent(), ExprTry()) - | on( - possibleExprTryToRight((tok(defns.`[]`) | tok(TLAReader.`->`))), - ).rewrite: split => - splice(split.unparent(), ExprTry()) - | on( - possibleExprTryToRight( - leftSibling(defns.OTHER) *> tok(TLAReader.`->`), - ), - ).rewrite: split => - splice(split.unparent(), ExprTry()) - // LET is complete when the definitions and the body are parsed - // I am assuming TLAParser will have inserted an ExpTry before the let - // body - | on( - leftSibling(ExprTry) *> - field( - tok(TLAReader.LetGroup) *> - children( - repeated: - tok(lang.Operator) - | tok(lang.ModuleDefinition) - | tok(lang.Recursive), - ), - ) - ~ skip(ExprTry) - ~ field(lang.Expr) - ~ trailing, - ).rewrite: (defs, body) => - splice( - lang.Expr( - lang.Expr.Let( - lang.Expr.Let.Defns( - defs.iterator.map(_.unparent()), - ), - body.unparent(), - ), - ), - ) - // Parentheses can be removed when its children are parsed - | on( - leftSibling(ExprTry) *> - tok(TLAReader.ParenthesesGroup) *> - children( - skip(ExprTry) - ~ field(lang.Expr) - ~ eof, - ), - ).rewrite: expr => - splice(expr.unparent()) - // Mark the children of the parentheses - | on( - firstUnmarkedChildStart(TLAReader.ParenthesesGroup), - ).rewrite: node => - splice(ExprTry(), node.unparent()) - *> pass(once = false, strategy = pass.bottomUp) - .rules: - // Expr has been parsed sucessfully, and ExprTry can be removed - // I have to do it this way, otherwise ExprTry might get removed too - // early. - on( - skip(ExprTry) - ~ field(lang.Expr) - ~ eof, - ).rewrite: expr => - splice(expr.unparent()) - end buildExpressions - - object removeNestedExpr extends Pass: - val wellformed = prevWellformed.makeDerived: - lang.Expr.removeCases(lang.Expr) - - val rules = - pass(once = true, strategy = pass.bottomUp) - .rules: - on( - tok(lang.Expr) *> - onlyChild(lang.Expr), - ).rewrite: child => - splice( - child.unparent(), - ) - end removeNestedExpr diff --git a/langs/tla/ExprMarker.test.scala b/langs/tla/ExprMarker.test.scala deleted file mode 100644 index bf4f271..0000000 --- a/langs/tla/ExprMarker.test.scala +++ /dev/null @@ -1,456 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.langs.tla - -import forja.* -import forja.dsl.* - -import ExprMarker.ExprTry - -// Run with: -// scala-cli test . -- '*ExprMarker*' - -class ExprMarkerTests extends munit.FunSuite: - extension (top: Node.Top) - def parseNode: Node.Top = - val freshTop = Node.Top( - lang.Module( - lang.Id("TestMod"), - lang.Module.Extends(), - lang.Module.Defns( - lang.Operator( - lang.Id("test"), - lang.Operator.Params(), - lang.Expr( - top.unparentedChildren, - ), - ), - ), - ), - ) - /* instrumentWithTracer(forja.manip.RewriteDebugTracer(os.pwd / - * "dbg_exprmarker")): */ - ExprMarker(freshTop) - Node.Top( - freshTop(lang.Module)(lang.Module.Defns)(lang.Operator)( - lang.Expr, - ).unparentedChildren, - ) - - test("NumberLiteral"): - assertEquals( - Node.Top(ExprTry(), TLAReader.NumberLiteral("1")).parseNode, - Node.Top(lang.Expr(lang.Expr.NumberLiteral("1"))), - ) - - test("StringLiteral"): - assertEquals( - Node.Top(ExprTry(), TLAReader.StringLiteral("string")).parseNode, - Node.Top(lang.Expr(lang.Expr.StringLiteral("string"))), - ) - - test("Id"): - assertEquals( - Node.Top(ExprTry(), TLAReader.Alpha("X")).parseNode, - Node.Top( - lang.Expr( - lang.Expr.OpCall( - lang.Id("X"), - lang.Expr.OpCall.Params(), - ), - ), - ), - ) - - test("ParenthesesGroup"): - assertEquals( - Node - .Top( - ExprTry(), - TLAReader.ParenthesesGroup( - TLAReader.NumberLiteral("1"), - ), - ) - .parseNode, - Node.Top(lang.Expr(lang.Expr.NumberLiteral("1"))), - ) - // TODO: paranthesis with op - - test("SetLiteral"): - // empty set - assertEquals( - Node.Top(ExprTry(), TLAReader.BracesGroup()).parseNode, - Node.Top(lang.Expr(lang.Expr.SetLiteral())), - ) - // set with three elements - assertEquals( - Node - .Top( - ExprTry(), - TLAReader.BracesGroup( - TLAReader.NumberLiteral("1"), - TLAReader.`,`(","), - TLAReader.NumberLiteral("2"), - TLAReader.`,`(","), - TLAReader.NumberLiteral("3"), - ), - ) - .parseNode, - Node.Top( - lang.Expr( - lang.Expr.SetLiteral( - lang.Expr(lang.Expr.NumberLiteral("1")), - lang.Expr(lang.Expr.NumberLiteral("2")), - lang.Expr(lang.Expr.NumberLiteral("3")), - ), - ), - ), - ) - // nested sets - assertEquals( - Node - .Top( - ExprTry(), - TLAReader.BracesGroup( - TLAReader.NumberLiteral("1"), - TLAReader.`,`(","), - TLAReader.BracesGroup(), - TLAReader.`,`(","), - TLAReader.NumberLiteral("3"), - ), - ) - .parseNode, - Node.Top( - lang.Expr( - lang.Expr.SetLiteral( - lang.Expr(lang.Expr.NumberLiteral("1")), - lang.Expr(lang.Expr.SetLiteral()), - lang.Expr(lang.Expr.NumberLiteral("3")), - ), - ), - ), - ) - - test("TupleLiteral"): - // empty tuple - assertEquals( - Node.Top(ExprTry(), TLAReader.TupleGroup()).parseNode, - Node.Top(lang.Expr(lang.Expr.TupleLiteral())), - ) - // tuple with three elements - assertEquals( - Node - .Top( - ExprTry(), - TLAReader.TupleGroup( - TLAReader.NumberLiteral("1"), - TLAReader.`,`(","), - TLAReader.StringLiteral("two"), - TLAReader.`,`(","), - TLAReader.NumberLiteral("3"), - ), - ) - .parseNode, - Node.Top( - lang.Expr( - lang.Expr.TupleLiteral( - lang.Expr(lang.Expr.NumberLiteral("1")), - lang.Expr(lang.Expr.StringLiteral("two")), - lang.Expr(lang.Expr.NumberLiteral("3")), - ), - ), - ), - ) - - test("RecordLiteral"): - // record with three fields - assertEquals( - Node - .Top( - ExprTry(), - TLAReader.SqBracketsGroup( - TLAReader.Alpha("X"), - TLAReader.`|->`("|->"), - TLAReader.NumberLiteral("1"), - TLAReader.`,`(","), - TLAReader.Alpha("Y"), - TLAReader.`|->`("|->"), - TLAReader.NumberLiteral("2"), - TLAReader.`,`(","), - TLAReader.Alpha("Z"), - TLAReader.`|->`("|->"), - TLAReader.NumberLiteral("3"), - ), - ) - .parseNode, - Node.Top( - lang.Expr( - lang.Expr.RecordLiteral( - lang.Expr.RecordLiteral.Field( - lang.Id("X"), - lang.Expr(lang.Expr.NumberLiteral("1")), - ), - lang.Expr.RecordLiteral.Field( - lang.Id("Y"), - lang.Expr(lang.Expr.NumberLiteral("2")), - ), - lang.Expr.RecordLiteral.Field( - lang.Id("Z"), - lang.Expr(lang.Expr.NumberLiteral("3")), - ), - ), - ), - ), - ) - - test("Projection (Record Field Acess)"): - assertEquals( - Node - .Top( - ExprTry(), - TLAReader.Alpha("X"), - defns.`.`("."), - TLAReader.Alpha("Y"), - ) - .parseNode, - Node.Top( - lang.Expr( - lang.Expr.Project( - lang.Expr( - lang.Expr.OpCall( - lang.Id("X"), - lang.Expr.OpCall.Params(), - ), - ), - lang.Id("Y"), - ), - ), - ), - ) - assertEquals( - Node - .Top( - ExprTry(), - TLAReader.Alpha("X"), - defns.`.`("."), - TLAReader.Alpha("Y"), - defns.`.`("."), - TLAReader.Alpha("Z"), - ) - .parseNode, - Node.Top( - lang.Expr( - lang.Expr.Project( - lang.Expr( - lang.Expr.Project( - lang.Expr( - lang.Expr.OpCall( - lang.Id("X"), - lang.Expr.OpCall.Params(), - ), - ), - lang.Id("Y"), - ), - ), - lang.Id("Z"), - ), - ), - ), - ) - - test("If"): - assertEquals( - Node - .Top( - ExprTry(), - defns.IF(), - TLAReader.Alpha("A"), - defns.THEN(), - TLAReader.NumberLiteral("1"), - defns.ELSE(), - TLAReader.NumberLiteral("2"), - ) - .parseNode, - Node.Top( - lang.Expr( - lang.Expr.If( - lang.Expr( - lang.Expr.OpCall( - lang.Id("A"), - lang.Expr.OpCall.Params(), - ), - ), - lang.Expr(lang.Expr.NumberLiteral("1")), - lang.Expr(lang.Expr.NumberLiteral("2")), - ), - ), - ), - ) - - test("Case"): - assertEquals( - Node - .Top( - ExprTry(), - defns.CASE(), - TLAReader.StringLiteral("A"), - TLAReader.`->`("->"), - TLAReader.NumberLiteral("1"), - ) - .parseNode, - Node.Top( - lang.Expr( - lang.Expr.Case( - lang.Expr.Case.Branches( - lang.Expr.Case.Branch( - lang.Expr(lang.Expr.StringLiteral("A")), - lang.Expr(lang.Expr.NumberLiteral("1")), - ), - ), - lang.Expr.Case.Other(lang.Expr.Case.None()), - ), - ), - ), - ) - assertEquals( - Node - .Top( - ExprTry(), - defns.CASE(), - TLAReader.StringLiteral("A"), - TLAReader.`->`("->"), - TLAReader.NumberLiteral("1"), - defns.`[]`("[]"), - TLAReader.StringLiteral("B"), - TLAReader.`->`("->"), - TLAReader.NumberLiteral("2"), - ) - .parseNode, - Node.Top( - lang.Expr( - lang.Expr.Case( - lang.Expr.Case.Branches( - lang.Expr.Case.Branch( - lang.Expr(lang.Expr.StringLiteral("A")), - lang.Expr(lang.Expr.NumberLiteral("1")), - ), - lang.Expr.Case.Branch( - lang.Expr(lang.Expr.StringLiteral("B")), - lang.Expr(lang.Expr.NumberLiteral("2")), - ), - ), - lang.Expr.Case.Other(lang.Expr.Case.None()), - ), - ), - ), - ) - assertEquals( - Node - .Top( - ExprTry(), - defns.CASE(), - TLAReader.StringLiteral("A"), - TLAReader.`->`("->"), - TLAReader.NumberLiteral("1"), - defns.`[]`("[]"), - TLAReader.StringLiteral("B"), - TLAReader.`->`("->"), - TLAReader.NumberLiteral("2"), - defns.OTHER("OTHER"), - TLAReader.`->`("->"), - TLAReader.NumberLiteral("3"), - ) - .parseNode, - Node.Top( - lang.Expr( - lang.Expr.Case( - lang.Expr.Case.Branches( - lang.Expr.Case.Branch( - lang.Expr(lang.Expr.StringLiteral("A")), - lang.Expr(lang.Expr.NumberLiteral("1")), - ), - lang.Expr.Case.Branch( - lang.Expr(lang.Expr.StringLiteral("B")), - lang.Expr(lang.Expr.NumberLiteral("2")), - ), - ), - lang.Expr.Case.Other(lang.Expr(lang.Expr.NumberLiteral("3"))), - ), - ), - ), - ) - - /* This test assumes that TLAParser has parsed the definitions and has - * inserted the ExprTry before the let body. */ - /* TODO: I am not sure what kind of node I should pass to lang.Operator. It - * complains if it is an Expr. */ - // test("LET"): - // assertEquals( - // Node.Top( - // ExprTry(), - // TLAReader.LetGroup( - // lang.Operator( - // lang.Id("X"), - // lang.Operator.Params(), - // lang.Expr(lang.Expr.NumberLiteral("1")), - // ), - // lang.Operator( - // lang.Id("Y"), - // lang.Operator.Params(), - // lang.Expr(lang.Expr.NumberLiteral("2")), - // ), - // ), - // ExprTry(), - // TLAReader.BracesGroup( - // TLAReader.Alpha("X"), - // TLAReader.`,`(","), - // TLAReader.Alpha("Y"), - // ) - // ).parseNode, - // Node.Top( - // lang.Expr( - // lang.Expr.Let( - // lang.Expr.Let.Defns( - // lang.Operator( - // lang.Id("X"), - // lang.Operator.Params(), - // lang.Expr(lang.Expr.NumberLiteral("1")), - // ), - // lang.Operator( - // lang.Id("Y"), - // lang.Operator.Params(), - // lang.Expr(lang.Expr.NumberLiteral("2")), - // ), - // ), - // lang.Expr( - // lang.Expr.SetLiteral( - // lang.Expr( - // lang.Expr.OpCall( - // lang.Id("Y"), - // lang.Expr.OpCall.Params(), - // ), - // ), - // lang.Expr( - // lang.Expr.OpCall( - // lang.Id("X"), - // lang.Expr.OpCall.Params(), - // ), - // ), - // ), - // ), - // ), - // ), - // ) - // ) diff --git a/langs/tla/TLAParser.scala b/langs/tla/TLAParser.scala deleted file mode 100644 index 7bfbb69..0000000 --- a/langs/tla/TLAParser.scala +++ /dev/null @@ -1,729 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.langs.tla - -import cats.syntax.all.given - -import forja.* -import forja.dsl.* -import forja.source.{Source, SourceRange} -import forja.wf.Wellformed - -import scala.collection.IndexedSeqView - -object TLAParser extends PassSeq: - import TLAReader.* - - def inputWellformed: Wellformed = TLAReader.wellformed - - lazy val passes = List( - ReservedWordsAndComments, - ModuleGroups, - UnitDefns, - AddLocals, - ) - - val opDeclPattern: SeqPattern[(Node, Int)] = - val alphaCase = - field(Alpha) - ~ field: - tok(ParenthesesGroup).withChildren: - field(repeatedSepBy(`,`)(Alpha.src("_")).map(_.size)) - ~ eof - ~ trailing - val prefixCase = - locally: - field(tok(defns.PrefixOperator.instances*)) - ~ skip(Alpha.src("_")) - ~ trailing - .map((_, 1)) - val infixCase = - locally: - skip(Alpha.src("_")) - ~ field(tok(defns.InfixOperator.instances*)) - ~ skip(Alpha.src("_")) - ~ trailing - .map((_, 2)) - val postfixCase = - locally: - skip(Alpha.src("_")) - ~ field(tok(defns.PostfixOperator.instances*)) - ~ trailing - .map((_, 1)) - - alphaCase - | prefixCase - | infixCase - | postfixCase - - val expressionDelimiters = Seq( - defns.VARIABLE, - defns.VARIABLES, - defns.CONSTANT, - defns.CONSTANTS, - defns.ASSUME, - defns.AXIOM, - defns.ASSUMPTION, - defns.THEOREM, - defns.PROPOSITION, - defns.LEMMA, - defns.COROLLARY, - defns.INSTANCE, - defns.LOCAL, - // For use in parsing opdecls and WITH, so we stop at the right places. - // Because we call these delimiters, we have to - // skip over them on purpose in nested expressions - // that have exposed commas or colons. - // We special-case: \A, \E, CHOOSE, LAMBDA. - // All the others are in () etc so don't count. - `<-`, - `,`, - `:`, - // what the beginning of a proof may look like - TLAReader.StepMarker, - defns.PROOF, - defns.BY, - defns.OBVIOUS, - defns.OMITTED, - // UseOrHide is its own unit - defns.USE, - defns.HIDE, - // may appear between top-level units - DashSeq, - // nested modules look like this (we will have parsed them earlier) - lang.Module, - // can occur in nested ASSUME ... ASSUME ... PROVE PROVE - defns.PROVE, - ) - - final case class RawExpression(nodes: IndexedSeqView[Node.Child]): - def mkNode: Node = - lang.Expr(nodes.map(_.unparent())) - - private lazy val operatorDefnBeginnings: SeqPattern[EmptyTuple] = - (skip(Alpha) ~ skip(optional(ParenthesesGroup)) ~ skip(`_==_`) ~ trailing) - | (skip(Alpha) ~ skip(SqBracketsGroup) ~ skip(`_==_`) ~ trailing) - | (skip(Alpha) ~ skip(tok(defns.InfixOperator.instances*)) ~ skip( - Alpha, - ) ~ skip(`_==_`) ~ trailing) - | (skip(Alpha) ~ skip(tok(defns.PostfixOperator.instances*)) ~ skip( - `_==_`, - ) ~ trailing) - | (skip(tok(defns.PrefixOperator.instances*)) ~ skip(Alpha) ~ skip( - `_==_`, - ) ~ trailing) - - lazy val rawExpression: SeqPattern[RawExpression] = - val simpleCases: SeqPattern[Unit] = - anyChild.void <* not( - tok(expressionDelimiters*) - // stop at operator definitions: all valid patterns leading to == here - | operatorDefnBeginnings, - ) - - lazy val quantifierBound: SeqPattern[EmptyTuple] = - skip( - tok(TupleGroup).as(EmptyTuple) - | repeatedSepBy1(`,`)(Alpha), - ) - ~ skip(defns.`\\in`) - ~ skip(defer(impl)) - ~ trailing - - lazy val quantifierBounds: SeqPattern[Unit] = - repeatedSepBy1(`,`)(quantifierBound).void - - lazy val forallExists: SeqPattern[EmptyTuple] = - skip( - tok(LaTexLike).src("\\A") | tok(LaTexLike).src("\\AA") | tok(LaTexLike) - .src("\\E") | tok(LaTexLike).src("\\EE"), - ) - ~ skip(quantifierBounds | repeatedSepBy1(`,`)(Alpha)) - ~ skip(`:`) - ~ trailing - - lazy val choose: SeqPattern[EmptyTuple] = - skip(defns.CHOOSE) - ~ skip(quantifierBound | repeatedSepBy1(`,`)(Alpha)) - ~ skip(`:`) - ~ trailing - - lazy val lambda: SeqPattern[EmptyTuple] = - skip(defns.LAMBDA) - ~ skip(repeatedSepBy1(`,`)(Alpha)) - ~ skip(`:`) - ~ trailing - - // this is an over-approximation of what can show up - // in an identifier prefix - // TODO: why does the grammar make it look like it goes the other way round? - lazy val idFrag: SeqPattern[EmptyTuple] = - skip(`!`) - ~ skip(`:`) - ~ trailing - - lazy val impl: SeqPattern[Unit] = - repeated1( - forallExists - | choose - | lambda - | idFrag - | simpleCases, // last, otherwise it eats parts of the above - ).void - - nodeSpanMatchedBy(impl).map(RawExpression.apply) - - val proofDelimiters: Seq[Token] = Seq( - defns.ASSUME, - defns.VARIABLE, - defns.VARIABLES, - defns.CONSTANT, - defns.CONSTANTS, - defns.AXIOM, - defns.ASSUMPTION, - defns.THEOREM, - defns.PROPOSITION, - defns.LEMMA, - defns.COROLLARY, - defns.INSTANCE, - defns.LOCAL, - ) - - lazy val assumeProve: SeqPattern[EmptyTuple] = - skip(defns.ASSUME) - ~ skip( - repeated( - anyChild <* not(tok(defns.PROVE, defns.ASSUME)) - | defer(assumeProve), - ), - ) - ~ skip(defns.PROVE) - ~ skip(rawExpression) - ~ trailing - - lazy val rawProofs: SeqPattern[IndexedSeqView[Node.Child]] = - val simpleCases: SeqPattern[Unit] = - anyChild.void <* not( - tok(proofDelimiters*) - | operatorDefnBeginnings, - ) - - val innerDefn: SeqPattern[EmptyTuple] = - skip(StepMarker) - ~ skip( - ( - skip(optional(defns.DEFINE)) - // you can have a chain of operator defs after DEFINE - ~ skip(repeatedSepBy1(rawExpression)(operatorDefnBeginnings)) - ~ trailing - ) - | tok(defns.INSTANCE), - ) - ~ trailing - - val impl: SeqPattern[Unit] = - repeated( - assumeProve - | innerDefn - | simpleCases, - ).void - - nodeSpanMatchedBy(impl) - - object ReservedWordsAndComments extends Pass: - val wellformed = prevWellformed.makeDerived: - TLAReader.groupTokens.foreach: tok => - tok.addCases(defns.ReservedWord.instances*) - defns.ReservedWord.instances.iterator - .filter(!_.isInstanceOf[defns.Operator]) - .foreach(_ ::= Atom) - - val reservedWordMap = - defns.ReservedWord.instances.view - .map: word => - SourceRange.entire(Source.fromString(word.spelling)) -> word - .toMap - - val laTexLikeOperatorMap = - defns.Operator.instances.view - .filter(_.spelling.startsWith("\\")) - .map: op => - SourceRange.entire(Source.fromString(op.spelling)) -> op - .toMap - - val rules = pass(once = true, strategy = pass.bottomUp) - .rules: - on( - tok(Alpha).filter(node => reservedWordMap.contains(node.sourceRange)), - ).rewrite: word => - splice(reservedWordMap(word.sourceRange)().like(word)) - | on( - tok(LaTexLike).filter(node => - laTexLikeOperatorMap.contains(node.sourceRange), - ), - ).rewrite: op => - splice(laTexLikeOperatorMap(op.sourceRange)().like(op)) - | on( - TLAReader.Comment, - ).rewrite: _ => - splice() // delete it. TODO: maybe try to gather comments? - end ReservedWordsAndComments - - object ModuleGroups extends Pass: - val wellformed = prevWellformed.makeDerived: - Node.Top ::=! repeated(lang.Module) - TLAReader.groupTokens.foreach: tok => - tok.removeCases(defns.MODULE, DashSeq, EqSeq) - - lang.Module ::= fields( - lang.Id, - lang.Module.Extends, - lang.Module.Defns, - ) - lang.Module.Extends.importFrom(lang.wf) - lang.Module.Defns ::= repeated( - choice(ModuleGroup.existingCases + DashSeq + lang.Module), - ) - - val rules = pass(once = false, strategy = pass.topDown) - .rules: - // remove top-level modules from ModuleGroup - on( - tok(ModuleGroup) *> onlyChild(lang.Module), - ).rewrite: mod => - splice(mod.unparent()) - /* Any module that doesn't have what looks like an unparsed nested - * module in it. */ - | on( - skip(DashSeq) - ~ skip(defns.MODULE) - ~ field(Alpha) - ~ skip(DashSeq) - ~ field( - optional( - skip(defns.EXTENDS) - ~ field(repeatedSepBy1(`,`)(Alpha)) - ~ trailing, - ).map(_.getOrElse(Nil)), - ) - ~ field( - repeated( - anyChild <* not( - tok(EqSeq) - | (skip(DashSeq) ~ skip(defns.MODULE) ~ skip(Alpha) ~ skip( - DashSeq, - ) ~ trailing), - ), - ), - ) - ~ skip(EqSeq) - ~ trailing, // we might be inside another module - ).rewrite: (name, exts, unitSoup) => - splice( - lang.Module( - lang.Id().like(name), - lang.Module.Extends(exts.map(ext => lang.Id().like(ext))), - lang.Module.Defns(unitSoup.map(_.unparent())), - ), - ) - end ModuleGroups - - object UnitDefns extends Pass: - val wellformed = prevWellformed.makeDerived: - lang.OpSym.importFrom(lang.wf) - - lang.Module.Defns ::=! repeated( - choice( - lang.Operator, - lang.Variable, - lang.Constant, - lang.Assumption, - lang.Theorem, - lang.Recursive, - lang.Instance, - lang.ModuleDefinition, - lang.Module, - lang.UseOrHide, - defns.LOCAL, // goes away on next pass! - ), - ) - - TLAReader.groupTokens.foreach: tok => - tok.removeCases( - `_==_`, - defns.VARIABLE, - defns.VARIABLES, - defns.CONSTANT, - defns.CONSTANTS, - defns.ASSUME, - defns.AXIOM, - defns.ASSUMPTION, - defns.THEOREM, - defns.INSTANCE, - ) - - lang.Expr ::= repeated( - choice(TLAReader.ParenthesesGroup.existingCases), - minCount = 1, - ) - - TLAReader.LetGroup ::=! repeated( - choice( - lang.Operator, - lang.ModuleDefinition, - lang.Recursive, - ), - ) - - lang.Operator.importFrom(lang.wf) - lang.Variable.importFrom(lang.wf) - lang.Constant.importFrom(lang.wf) - lang.OpSym.importFrom(lang.wf) - lang.Assumption.importFrom(lang.wf) - lang.Theorem.importFrom(lang.wf) - lang.Recursive.importFrom(lang.wf) - lang.Instance.importFrom(lang.wf) - lang.ModuleDefinition.importFrom(lang.wf) - lang.UseOrHide.importFrom(lang.wf) - - final case class Instance( - name: Node, - substitutions: List[(Node, RawExpression)], - ): - def mkNode: Node = - lang.Instance( - lang.Id().like(name), - lang.Instance.Substitutions( - substitutions.iterator - .map: (name, expr) => - lang.Instance.Substitution( - if name.token == Alpha - then lang.Id().like(name) - else name.unparent(), - expr.mkNode, - ), - ), - ) - - object Instance: - val pattern: SeqPattern[Instance] = - ( - skip(defns.INSTANCE) - ~ field(TLAReader.Alpha) - ~ field( - optional( - skip(defns.WITH) - ~ field(repeatedSepBy1(`,`): - field(tok(Alpha) | tok(defns.Operator.instances*)) - ~ skip(`<-`) - ~ field(rawExpression) - ~ trailing) - ~ trailing, - ), - ) - ~ trailing - ).map: (name, subsOpt) => - Instance(name, subsOpt.getOrElse(Nil)) - - val rules = pass(once = true, strategy = pass.bottomUp) - .rules: - // operator defn variations - on( - field(Alpha) - ~ field( - optional( - tok(ParenthesesGroup) *> children( - repeatedSepBy(`,`)(TLAReader.Alpha), - ), - ), - ) - ~ skip(`_==_`) - ~ field(rawExpression) - ~ trailing, - ).rewrite: (name, paramsOpt, body) => - splice( - lang.Operator( - lang.Id().like(name), - paramsOpt match - case None => lang.Operator.Params() - case Some(params) => - lang.Operator.Params( - params.iterator.map(p => lang.Id().like(p)), - ), - body.mkNode, - ), - ) - | on( - field(Alpha) - ~ field(tok(defns.InfixOperator.instances*)) - ~ field(Alpha) - ~ skip(`_==_`) - ~ field(rawExpression) - ~ trailing, - ).rewrite: (param1, op, param2, body) => - splice( - lang.Operator( - lang.OpSym(op.unparent()), - lang.Operator - .Params(lang.Id().like(param1), lang.Id().like(param2)), - body.mkNode, - ), - ) - | on( - field(Alpha) - ~ field(tok(defns.PostfixOperator.instances*)) - ~ skip(`_==_`) - ~ field(rawExpression) - ~ trailing, - ).rewrite: (param, op, body) => - splice( - lang.Operator( - lang.OpSym(op.unparent()), - lang.Operator.Params(lang.Id().like(param)), - body.mkNode, - ), - ) - | on( - field(tok(defns.PrefixOperator.instances*)) - ~ field(Alpha) - ~ skip(`_==_`) - ~ field(rawExpression) - ~ trailing, - ).rewrite: (op, param, body) => - splice( - lang.Operator( - lang.OpSym(op.unparent()), - lang.Operator.Params(lang.Id().like(param)), - body.mkNode, - ), - ) - | on( - field(Alpha) - ~ field(SqBracketsGroup) - ~ skip(`_==_`) - ~ field(rawExpression) - ~ trailing, - ).rewrite: (name, params, body) => - splice( - lang.Operator( - lang.Id().like(name), - lang.Operator.Params(), - lang.Expr( - SqBracketsGroup( - params.children.view.map(_.unparent()) - ++ List(`|->`()) - ++ body.nodes.map(_.unparent()), - ), - ), - ), - ) - // variable decls - | on( - skip(tok(defns.VARIABLE, defns.VARIABLES)) - ~ field(repeatedSepBy1(`,`)(Alpha)) - ~ trailing, - ).rewrite: vars => - splice( - vars.map: v => - lang.Variable(lang.Id().like(v)), - ) - // constant decls - | on( - skip(tok(defns.CONSTANT, defns.CONSTANTS)) - ~ field( - repeatedSepBy1(`,`): - opDeclPattern - | tok(Alpha), - ) ~ trailing, - ).rewrite: decls => - splice( - decls.iterator - .map: - case (alpha, arity) if alpha.token == Alpha => - lang.Constant( - lang.Order2( - lang.Id().like(alpha), - Node.Embed(arity), - ), - ) - case (op, arity) => - lang.Constant( - lang.Order2( - op.unparent(), - Node.Embed(arity), - ), - ) - case alpha: Node => - lang.Constant(lang.Id().like(alpha)), - ) - // assume - | on( - skip(tok(defns.ASSUME, defns.ASSUMPTION, defns.AXIOM)) - ~ field( - optional( - field(Alpha) - ~ skip(`_==_`) - ~ trailing, - ), - ) - ~ field(rawExpression) - ~ trailing, - ).rewrite: (nameOpt, rawExpr) => - splice( - lang.Assumption( - nameOpt match - case None => lang.Anonymous() - case Some(name) => lang.Id().like(name), - rawExpr.mkNode, - ), - ) - // theorem - | on( - skip( - tok(defns.THEOREM, defns.PROPOSITION, defns.LEMMA, defns.COROLLARY), - ) - ~ field( - optional( - field(Alpha) - ~ skip(`_==_`) - ~ trailing, - ), - ) - ~ field(nodeSpanMatchedBy(assumeProve.void) | rawExpression) - ~ field( - optional( - // without ~, this works like a lookahead - tok( - TLAReader.StepMarker, - defns.PROOF, - defns.BY, - defns.OBVIOUS, - defns.OMITTED, - ) - *> rawProofs, - ), - ) - ~ trailing, - ).rewrite: (nameOpt, body, proofsOpt) => - splice( - lang.Theorem( - nameOpt match - case None => lang.Anonymous() - case Some(name) => lang.Id().like(name), - body match - case rawExpr: RawExpression => rawExpr.mkNode - case assumeProveNodes: IndexedSeqView[Node.Child] => - lang.Theorem.AssumeProve( - assumeProveNodes.map(_.unparent()), - ), - proofsOpt match - case None => lang.Theorem.Proofs() - case Some(proofs) => - lang.Theorem.Proofs(proofs.map(_.unparent())), - ), - ) - // recursive - | on( - skip(tok(defns.RECURSIVE)) - ~ field( - repeatedSepBy1(`,`): - opDeclPattern - | tok(Alpha), - ) - ~ trailing, - ).rewrite: decls => - splice( - decls.iterator - .map: - case (alpha, arity) if alpha.token == Alpha => - lang.Recursive( - lang.Order2( - lang.Id().like(alpha), - Node.Embed(arity), - ), - ) - case (op, arity) => - lang.Recursive( - lang.Order2( - op.unparent(), - Node.Embed(arity), - ), - ) - case alpha: Node => - lang.Recursive(lang.Id().like(alpha)), - ) - // instance - | on( - Instance.pattern, - ).rewrite: inst => - splice(inst.mkNode) - // module definition - | on( - field(Alpha) - ~ field( - optional( - tok(ParenthesesGroup) *> children( - repeated1(Alpha), - ), - ), - ) - ~ skip(`_==_`) - ~ field(Instance.pattern) - ~ trailing, - ).rewrite: (name, paramsOpt, instance) => - splice( - lang.ModuleDefinition( - lang.Id().like(name), - paramsOpt match - case None => lang.Operator.Params() - case Some(params) => - lang.Operator.Params( - params.map(param => lang.Id().like(param)), - ), - instance.mkNode, - ), - ) - // dashseq - | on( - tok(DashSeq), - ).rewrite: _ => - splice() - // useOrHide - | on( - tok(defns.USE, defns.HIDE) - *> rawProofs, - ).rewrite: contents => - splice(lang.UseOrHide(contents.map(_.unparent()))) - end UnitDefns - - object AddLocals extends Pass: - val wellformed = prevWellformed.makeDerived: - lang.Module.Defns.removeCases(defns.LOCAL) - lang.Module.Defns.addCases(lang.Local) - lang.Local.importFrom(lang.wf) - - val rules = pass(once = true, strategy = pass.bottomUp) - .rules: - // LOCAL - on( - field(tok(defns.LOCAL)) - ~ field( - tok(lang.Operator, lang.Instance, lang.ModuleDefinition), - ) - ~ trailing, - ).rewrite: (local, op) => - splice(lang.Local(op.unparent()).like(local)) - end AddLocals -end TLAParser diff --git a/langs/tla/TLAParser.test.scala b/langs/tla/TLAParser.test.scala deleted file mode 100644 index 3100564..0000000 --- a/langs/tla/TLAParser.test.scala +++ /dev/null @@ -1,49 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.langs.tla - -import forja.* -import forja.source.{Source, SourceRange} -// import forja.dsl.* -// import forja.manip.DebugAdapter - -class TLAParserTests extends munit.FunSuite, test.WithTLACorpus: - self => - - /* TODO: skip the TLAPS files; parsing that seems like a waste of time for - * now... or maybe not? */ - - testWithCorpusFile: file => - val src = Source.mapFromFile(file) - val top = TLAReader(SourceRange.entire(src)) - - assume(!top.hasErrors, top) - - // instrumentWithTracer(DebugAdapter("localhost", 4711)): - // TLAParser(top) - - // re-enable if interesting: - // val folder = os.SubPath(file.subRelativeTo(clonesDir).segments.init) - // os.write.over( - // os.pwd / "dbg_tla_parser" / folder / s"${file.last}.dbg", - // top.toPrettyWritable(TLAReader.wellformed), - // createFolders = true - // ) - - if top.hasErrors - then fail(top.presentErrors(debug = true)) - - if top.children.isEmpty - then fail("no data extracted") diff --git a/langs/tla/TLAReader.scala b/langs/tla/TLAReader.scala deleted file mode 100644 index 8affd1e..0000000 --- a/langs/tla/TLAReader.scala +++ /dev/null @@ -1,578 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.langs.tla - -import cats.syntax.all.given - -import forja.* -import forja.dsl.* -import forja.langs.tla.ExprMarker.ExprTry -import forja.source.{Reader, SourceRange} -import forja.wf.Wellformed - -object TLAReader extends Reader: - lazy val groupTokens: List[Token] = List( - ModuleGroup, - ParenthesesGroup, - SqBracketsGroup, - BracesGroup, - TupleGroup, - LetGroup, - ) - - lazy val wellformed = Wellformed: - Node.Top ::= repeated(ModuleGroup) - - val allToks = - choice( - StringLiteral, - NumberLiteral, - // --- - StepMarker, - // --- - ModuleGroup, - ParenthesesGroup, - SqBracketsGroup, - BracesGroup, - TupleGroup, - LetGroup, - // --- - Alpha, - LaTexLike, - // --- - Comment, - DashSeq, - EqSeq, - // - ExprTry, - ) - | choice(NonAlpha.instances.toSet) - | choice(allOperators.toSet) - - ModuleGroup ::= repeated(allToks) - - ParenthesesGroup ::= repeated(allToks) - SqBracketsGroup ::= repeated(allToks) - BracesGroup ::= repeated(allToks) - TupleGroup ::= repeated(allToks) - LetGroup ::= repeated(allToks) - - StringLiteral ::= Atom - NumberLiteral ::= Atom - Alpha ::= Atom - LaTexLike ::= Atom - ExprTry ::= Atom - - StepMarker ::= fields( - choice(StepMarker.Num, StepMarker.Plus, StepMarker.Star), - StepMarker.Ident, - embedded[Int], - ) - StepMarker.Num ::= Atom - StepMarker.Plus ::= Atom - StepMarker.Star ::= Atom - StepMarker.Ident ::= Atom - - Comment ::= Atom - DashSeq ::= Atom - EqSeq ::= Atom - - allOperators.foreach(_ ::= Atom) - NonAlpha.instances.foreach(_ ::= Atom) - - import Reader.* - - object StringLiteral extends Token.ShowSource - object NumberLiteral extends Token.ShowSource - - object StepMarker extends Token: - object Num extends Token.ShowSource - object Plus extends Token - object Star extends Token - object Ident extends Token.ShowSource - - object ModuleGroup extends Token - object ParenthesesGroup extends Token - object SqBracketsGroup extends Token - object BracesGroup extends Token - object TupleGroup extends Token - object LetGroup extends Token - - object Alpha extends Token.ShowSource - object LaTexLike extends Token.ShowSource - - object Comment extends Token.ShowSource - object DashSeq extends Token - object EqSeq extends Token - - sealed trait NonAlpha extends Product, Token: - def spelling: String = productPrefix - object NonAlpha extends util.HasInstanceArray[NonAlpha] - - case object `:` extends NonAlpha - case object `::` extends NonAlpha - case object `<-` extends NonAlpha - case object `->` extends NonAlpha - case object `|->` extends NonAlpha - case object `,` extends NonAlpha - case object `.` extends NonAlpha - case object `!` extends NonAlpha - case object `@` extends NonAlpha - case object `_==_` extends NonAlpha: - override def spelling: String = "==" - - case object `WF_` extends NonAlpha - case object `SF_` extends NonAlpha - - val digits: Set[Char] = - ('0' to '9').toSet - val letters: Set[Char] = - ('a' to 'z').toSet ++ ('A' to 'Z').toSet - val spaces: Set[Char] = - Set(' ', '\t', '\n', '\r') - - val allOperators = - defns.PrefixOperator.instances - ++ defns.InfixOperator.instances - ++ defns.PostfixOperator.instances - - val nonAlphaNonLaTexOperators: IArray[defns.Operator] = - allOperators.filter: op => - (!op.spelling.startsWith( - "\\", - ) || op == defns.`\\/` || op == defns.`\\`) - && !letters(op.spelling.head) - - extension (sel: bytes.selecting[SourceRange]) - private def installNonAlphaNonLatexOperators: bytes.selecting[SourceRange] = - nonAlphaNonLaTexOperators.foldLeft(sel): (sel, op) => - sel.onSeq(op.spelling): - consumeMatch: m => - addChild(op(m)) - *> tokens - - private def installNonAlphas: bytes.selecting[SourceRange] = - NonAlpha.instances.foldLeft(sel): (sel, nonAlpha) => - sel.onSeq(nonAlpha.spelling): - consumeMatch: m => - addChild(nonAlpha(m)) - *> tokens - - private def installLatexLikes: bytes.selecting[SourceRange] = - lazy val impl = - bytes.selectManyLike(letters): - consumeMatch: m => - addChild(LaTexLike(m)) - *> tokens - letters.foldLeft(sel): (sel, l) => - sel.onSeq(s"\\$l")(impl) - - private def installAlphas( - next: => Manip[SourceRange], - ): bytes.selecting[SourceRange] = - val nxt = defer(next) - sel.onOneOf(letters + '_'): - bytes.selectManyLike(letters ++ digits + '_'): - consumeMatch: m => - addChild(Alpha(m)) - *> nxt - - private def installDotNumberLiterals: bytes.selecting[SourceRange] = - digits.foldLeft(sel): (sel, d) => - sel.onSeq(s".$d")(numberLiteralAfterPoint) - - private def installStepMarkers: bytes.selecting[SourceRange] = - digits - .foldLeft(sel): (sel, d) => - sel.onSeq(s"<$d")(maybeNumStepMarker) - .onSeq("<*>"): - consumeMatch: m1 => - finishStepMarkerWithIdx( - m1, - StepMarker.Star(m1.drop(1).dropRight(1)), - ) - .onSeq("<+>"): - consumeMatch: m1 => - finishStepMarkerWithIdx( - m1, - StepMarker.Plus(m1.drop(1).dropRight(1)), - ) - - override protected lazy val rules: Manip[SourceRange] = - moduleSearch - - lazy val moduleSearchNeverMind: Manip[SourceRange] = - on( - tok(ModuleGroup) *> refine(atParent(on(theTop).value)), - ).value.flatMap: top => - dropMatch: - effect(top.children.dropRightInPlace(1)) - *> atNode(top)(moduleSearch) - - private lazy val moduleSearch: Manip[SourceRange] = - lazy val onAlpha: Manip[SourceRange] = - val validCases = - on( - tok(ModuleGroup).withChildren: - skip(DashSeq) - ~ skip(Alpha.src("MODULE")) - ~ skip(optional(Alpha)) - ~ eof, - ).check - - (validCases *> moduleSearch) - | on(theTop).value.flatMap: top => - // Saw an alpha at top level. Delete it. - effect(top.children.dropRightInPlace(1)) - *> moduleSearch - | moduleSearchNeverMind // otherwise we failed to parse a module start - - commit: - bytes - .selecting[SourceRange] - .onOneOf(spaces)(dropMatch(moduleSearch)) - .onSeq("----"): - bytes.selectManyLike(Set('-')): - commit: - (on(theTop).check - *> addChild(ModuleGroup()) - .here: - consumeMatch: m => - addChild(DashSeq(m)) - *> moduleSearch - ) - | (on( - tok(ModuleGroup).withChildren: - skip(DashSeq) - ~ skip(Alpha.src("MODULE")) - ~ skip(Alpha) - ~ eof, - ).check *> consumeMatch: m => - addChild(DashSeq(m)) - *> tokens) - | moduleSearchNeverMind - .installAlphas(onAlpha) - .onOneOf(digits): - /* This case handles enough of the number -> alpha promotion that we - * can parse the module name if it starts with one or more digits. */ - bytes.selectManyLike(digits): - bytes - .selecting[SourceRange] - .installAlphas(onAlpha) - .fallback: - on(theTop).check - /* Saw digits (?) at top level. We didn't even make a node, so - * just drop the match and go back to business as usual. */ - *> dropMatch(moduleSearch) - | moduleSearchNeverMind // Or we saw an integer in the middle of module pattern, in which case drop this match. - .fallback: - bytes.selectOne: - commit: - (on(theTop).check *> moduleSearch) - | moduleSearchNeverMind - | consumeMatch: m => - on(theTop).check *> Manip.pure(m) - | moduleSearchNeverMind - - private def openGroup(tkn: Token): Manip[SourceRange] = - consumeMatch: m => - addChild(tkn(m)) - .here(tokens) - - private def closeGroup( - tkn: Token, - reinterpretTok: Token, - ): Manip[SourceRange] = - val rest: Manip[SourceRange] = - if tkn == reinterpretTok - then on(tok(tkn)).check *> extendThisNodeWithMatch(atParent(tokens)) - else - on(tok(tkn)).value.flatMap: node => - effect( - node.replaceThis(reinterpretTok(node.unparentedChildren).like(node)), - ) - *> tokens - - on(tok(tkn)).check *> rest - - private def closeGroup(tkn: Token): Manip[SourceRange] = - closeGroup(tkn, tkn) - - private def invalidGroupClose(tkn: Token, name: String): Manip[SourceRange] = - // TODO: close a parent group if you can - consumeMatch: m => - addChild( - Builtin.Error( - s"unexpected end of $name group", - Builtin.SourceMarker(m), - ), - ) - *> tokens - - private lazy val unexpectedEOF: Manip[SourceRange] = - consumeMatch: m => - addChild( - Builtin.Error( - "unexpected EOF", - Builtin.SourceMarker(m), - ), - ) - *> Manip.pure(m) - - private lazy val letGroupSemantics: Manip[SourceRange] = - commit: - on( - lastChild(Alpha.src("LET")), - ).value.flatMap: node => - effect(node.replaceThis(LetGroup(node.sourceRange))) - .here(tokens) - | on( - tok(LetGroup) *> lastChild(Alpha.src("IN")), - ).value.flatMap: node => - effect(node.removeThis().sourceRange) - .flatMap: m => - extendThisNode(m) - atParent(tokens) - | on( - lastChild(Alpha.src("IN")), - ).value.flatMap: node => - effect(node.removeThis().sourceRange) - .flatMap: m => - addChild( - Builtin.Error( - s"unexpected end of LET group", - Builtin.SourceMarker(m), - ), - ) - *> tokens - | on(not(lastChild(Alpha.src("IN")))).check *> tokens - - private lazy val tokens: Manip[SourceRange] = - commit: - bytes - .selecting[SourceRange] - .onOneOf(spaces)(dropMatch(tokens)) - .onSeq("===="): - bytes.selectManyLike(Set('=')): - (on(tok(ModuleGroup)).check - *> consumeMatch: m => - addChild(EqSeq(m)) - *> locally: - atParent(on(theTop).check *> moduleSearch) - ) - | atParent(tokens) - | consumeMatch: m => - addChild( - Builtin.Error( - "unexpected end of module", - Builtin.SourceMarker(m), - ), - ) - *> on(ancestor(theTop)).value.here(moduleSearch) - .onSeq("----"): - bytes.selectManyLike(Set('-')): - consumeMatch: m => - addChild(DashSeq(m)) - *> locally: - val handleNestedModule = - atFirstChild( - atIdxFromRight(3): - on( - field(DashSeq) - ~ field(tok(Alpha).src("MODULE")) - ~ field(Alpha) - ~ field(DashSeq) - ~ eof, - ).value.tapEffect: (initDashes, mod, name, endDashes) => - val parent = initDashes.parent.get - parent.children.patchInPlace( - initDashes.idxInParent, - Iterator.single( - ModuleGroup( - initDashes.unparent(), - mod.unparent(), - name.unparent(), - endDashes.unparent(), - ), - ), - replaced = 4, - ), - ) *> atLastChild(tokens) - - handleNestedModule | tokens - .on('"')(stringLiteral) - .onSeq("(*")(multiComment) - .onSeq("\\*")(lineComment) - .on('(')(openGroup(ParenthesesGroup)) - .on(')'): - closeGroup(ParenthesesGroup) - | invalidGroupClose(ParenthesesGroup, "parentheses") - .on('[')(openGroup(SqBracketsGroup)) - .on(']'): - closeGroup(SqBracketsGroup) - | invalidGroupClose(SqBracketsGroup, "square brackets") - .on('{')(openGroup(BracesGroup)) - .on('}'): - closeGroup(BracesGroup) - | invalidGroupClose(BracesGroup, "braces") - .installStepMarkers - .onSeq("<<")(openGroup(TupleGroup)) - .onSeq(">>"): - closeGroup(TupleGroup) - | invalidGroupClose(TupleGroup, "tuple") - .onOneOf(digits)(numberLiteral) - .installDotNumberLiterals - .installAlphas(letGroupSemantics) - .installLatexLikes - .installNonAlphaNonLatexOperators - .installNonAlphas - .fallback: - bytes.selectOne: - consumeMatch: m => - addChild( - Builtin.Error( - "invalid character", - Builtin.SourceMarker(m), - ), - ) - *> tokens - | unexpectedEOF - - private lazy val lineComment: Manip[SourceRange] = - val endComment: Manip[SourceRange] = - consumeMatch: m => - addChild(Comment(m)) - *> tokens - - commit: - bytes - .selecting[SourceRange] - .on('\n')(endComment) - .onSeq("\r\n")(endComment) - .fallback: - bytes.selectOne(lineComment) - - private lazy val multiComment: Manip[SourceRange] = - multiCommentRec: - consumeMatch: m => - addChild(Comment(m)) - *> tokens - - private def multiCommentRec(outer: Manip[SourceRange]): Manip[SourceRange] = - lazy val impl: Manip[SourceRange] = - commit: - bytes - .selecting[SourceRange] - .onSeq("*)")(outer) - .onSeq("(*")(multiCommentRec(impl)) - .fallback: - bytes.selectOne: - impl - - impl - - private lazy val stringLiteral: Manip[SourceRange] = - object builderRef extends Manip.Ref[SourceRange.Builder] - - def addByte(b: Byte): Manip[SourceRange] = - dropMatch: - builderRef.get - .tapEffect(_.addOne(b)) - *> stringLiteralLoop - - def finishStringLiteral(rest: Manip[SourceRange]): Manip[SourceRange] = - dropMatch: - builderRef.get.flatMap: builder => - addChild(StringLiteral(builder.result())) - *> rest - - lazy val stringLiteralLoop: Manip[SourceRange] = - commit: - bytes - .selecting[SourceRange] - .on('"')(finishStringLiteral(tokens)) - .onSeq("\\\"")(addByte('"')) - .onSeq("\\\\")(addByte('\\')) - .onSeq("\\t")(addByte('\t')) - .onSeq("\\n")(addByte('\n')) - .onSeq("\\f")(addByte('\f')) - .onSeq("\\r")(addByte('\r')) - .fallback: - bytes.selectOne: - consumeMatch: m => - assert(m.length == 1) - addByte(m.head) - | finishStringLiteral(unexpectedEOF) - - builderRef.reset: - builderRef.init(SourceRange.newBuilder)(dropMatch(stringLiteralLoop)) - - lazy val endNumberLiteral: Manip[SourceRange] = - consumeMatch: m => - addChild(NumberLiteral(m)) - *> tokens - - private lazy val numberLiteralAfterPoint: Manip[SourceRange] = - commit: - bytes - .selecting[SourceRange] - .onOneOf(digits)(numberLiteralAfterPoint) - .fallback(endNumberLiteral) - - private lazy val numberLiteral: Manip[SourceRange] = - commit: - bytes - .selecting[SourceRange] - .onOneOf(digits)(numberLiteral) - // If we find a letter, then we're not in a num literal. - // Act like we were processing an Alpha all along. - .installAlphas(letGroupSemantics) - .installDotNumberLiterals - .fallback(endNumberLiteral) - - private lazy val maybeNumStepMarker: Manip[SourceRange] = - commit: - bytes - .selecting[SourceRange] - .onOneOf(digits)(maybeNumStepMarker) - .on('>'): - consumeMatch: m1 => - finishStepMarkerWithIdx(m1, StepMarker.Num(m1.drop(1).dropRight(1))) - .fallback: - Reader.matchedRef.get.flatMap: m => - /* Throw away the leading <, then pretend it was a num all along. - * Note: this may in fact be `<5x__` which should lex as op `<`, id - * `5x__` (num lexer already handles this) */ - addChild(defns.`<`(m.take(1))) - *> Reader.matchedRef.updated(_ => m.drop(1)): - numberLiteral - - private def finishStepMarkerWithIdx( - m1: SourceRange, - idx: Node, - ): Manip[SourceRange] = - bytes.selectManyLike(digits ++ letters + '_'): - consumeMatch: m2 => - bytes.selectManyLike(Set('.')): - consumeMatch: m3 => - addChild( - StepMarker( - idx, - StepMarker.Ident(m2), - Node.Embed(m3.size), - ) - .at(m1 <+> m2 <+> m3), - ) - *> tokens diff --git a/langs/tla/TLAReader.test.scala b/langs/tla/TLAReader.test.scala deleted file mode 100644 index 32149d0..0000000 --- a/langs/tla/TLAReader.test.scala +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.langs.tla - -import forja.* -import forja.source.{Source, SourceRange} - -class TLAReaderTests extends munit.FunSuite, test.WithTLACorpus: - self => // funny parser ambiguity: try deleting this line - - testWithCorpusFile: file => - val src = Source.mapFromFile(file) - val top = TLAReader(SourceRange.entire(src)) - - // re-enable if interesting: - // os.write.over( - // os.pwd / "dbg_tla_reader" / s"${file.last}.dbg", - // top.toPrettyWritable(TLAReader.wellformed), - // createFolders = true - // ) - - if top.hasErrors - then fail(top.presentErrors(debug = true)) - - if top.children.isEmpty - then fail("no data extracted") diff --git a/langs/tla/defns.scala b/langs/tla/defns.scala deleted file mode 100644 index fb8dbd1..0000000 --- a/langs/tla/defns.scala +++ /dev/null @@ -1,222 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.langs.tla - -import forja.* - -object defns: - transparent sealed trait HasSpelling extends Product: - def spelling: String = productPrefix - - sealed trait ReservedWord extends Token, HasSpelling - object ReservedWord extends util.HasInstanceArray[ReservedWord] - - case object IN extends ReservedWord - case object WITH extends ReservedWord - case object THEN extends ReservedWord - case object CONSTANTS extends ReservedWord - case object ASSUMPTION extends ReservedWord - case object VARIABLE extends ReservedWord - case object WF_ extends ReservedWord - case object ASSUME extends ReservedWord - case object AXIOM extends ReservedWord - case object CHOOSE extends ReservedWord - case object OTHER extends ReservedWord - case object EXTENDS extends ReservedWord - case object VARIABLES extends ReservedWord - case object EXCEPT extends ReservedWord - case object INSTANCE extends ReservedWord - case object THEOREM extends ReservedWord - case object SF_ extends ReservedWord - case object MODULE extends ReservedWord - case object LET extends ReservedWord - case object IF extends ReservedWord - case object LOCAL extends ReservedWord - case object ELSE extends ReservedWord - case object CONSTANT extends ReservedWord - case object CASE extends ReservedWord - case object COROLLARY extends ReservedWord - case object BY extends ReservedWord - case object HAVE extends ReservedWord - case object QED extends ReservedWord - case object TAKE extends ReservedWord - case object DEF extends ReservedWord - case object HIDE extends ReservedWord - case object RECURSIVE extends ReservedWord - case object USE extends ReservedWord - case object DEFINE extends ReservedWord - case object PROOF extends ReservedWord - case object WITNESS extends ReservedWord - case object PICK extends ReservedWord - case object DEFS extends ReservedWord - case object PROVE extends ReservedWord - case object SUFFICES extends ReservedWord - case object NEW extends ReservedWord - case object LAMBDA extends ReservedWord - case object STATE extends ReservedWord - case object ACTION extends ReservedWord - case object TEMPORAL extends ReservedWord - case object OBVIOUS extends ReservedWord - case object OMITTED extends ReservedWord - case object LEMMA extends ReservedWord - case object PROPOSITION extends ReservedWord - case object ONLY extends ReservedWord - - sealed trait Operator extends Token, HasSpelling: - def highPrecedence: Int - def lowPrecedence: Int - - object Operator: - lazy val instances: IArray[Operator] = - PrefixOperator.instances - ++ InfixOperator.instances - ++ PostfixOperator.instances - - sealed trait PrefixOperator(val lowPrecedence: Int, val highPrecedence: Int) - extends Operator - object PrefixOperator extends util.HasInstanceArray[PrefixOperator] - - case object `ENABLED` extends PrefixOperator(4, 15), ReservedWord - case object `SUBSET` extends PrefixOperator(8, 8), ReservedWord - case object `DOMAIN` extends PrefixOperator(9, 9), ReservedWord - case object `[]` extends PrefixOperator(4, 15) - case object `\\neg` extends PrefixOperator(4, 4) - case object `~` extends PrefixOperator(4, 4) - case object `UNION` extends PrefixOperator(8, 8), ReservedWord - case object `<>` extends PrefixOperator(4, 15) - case object `\\lnot` extends PrefixOperator(4, 4) - case object `-_` extends PrefixOperator(12, 12) - case object `UNCHANGED` extends PrefixOperator(4, 15), ReservedWord - - sealed trait InfixOperator( - val lowPrecedence: Int, - val highPrecedence: Int, - val isAssociative: Boolean = false, - ) extends Operator - object InfixOperator extends util.HasInstanceArray[InfixOperator] - - case object `\\cong` extends InfixOperator(5, 5) - case object `\\cdot` extends InfixOperator(5, 14, true) - case object `\\sqsubseteq` extends InfixOperator(5, 5) - case object `\\bullet` extends InfixOperator(13, 13, true) - case object `**` extends InfixOperator(13, 13, true) - case object `^^` extends InfixOperator(14, 14) - case object `/\\` extends InfixOperator(3, 3, true) - case object `|=` extends InfixOperator(5, 5) - case object `\\succeq` extends InfixOperator(5, 5) - case object `\\oslash` extends InfixOperator(13, 13) - case object `\\sqcap` extends InfixOperator(9, 13, true) - case object `*` extends InfixOperator(13, 13, true) - case object `<=` extends InfixOperator(5, 5) - case object `\\approx` extends InfixOperator(5, 5) - case object `\\equiv` extends InfixOperator(2, 2) - case object `%` extends InfixOperator(10, 11) - case object `/=` extends InfixOperator(5, 5) - case object `\\lor` extends InfixOperator(3, 3) - case object `\\in` extends InfixOperator(5, 5) - case object `\\div` extends InfixOperator(13, 13) - case object `:>` extends InfixOperator(7, 7) - case object `.` extends InfixOperator(17, 17, true) - case object `\\asymp` extends InfixOperator(5, 5) - case object `=` extends InfixOperator(5, 5) - case object `\\prec` extends InfixOperator(5, 5) - case object `\\circ` extends InfixOperator(13, 13, true) - case object `\\succ` extends InfixOperator(5, 5) - case object `\\simeq` extends InfixOperator(5, 5) - case object `<` extends InfixOperator(5, 5) - case object `\\notin` extends InfixOperator(5, 5) - case object `::=` extends InfixOperator(5, 5) - case object `\\cap` extends InfixOperator(8, 8, true) - case object `\\ominus` extends InfixOperator(11, 11, true) - case object `-|` extends InfixOperator(5, 5) - case object `&` extends InfixOperator(13, 13, true) - case object `=|` extends InfixOperator(5, 5) - case object `|-` extends InfixOperator(5, 5) - case object `\\` extends InfixOperator(8, 8) - case object `=<` extends InfixOperator(5, 5) - case object `(-)` extends InfixOperator(11, 11) - case object `\\union` extends InfixOperator(8, 8, true) - case object `>=` extends InfixOperator(5, 5) - case object `=>` extends InfixOperator(1, 1) - case object `\\leq` extends InfixOperator(5, 5) - case object `\\propto` extends InfixOperator(5, 5) - case object `\\sqcup` extends InfixOperator(9, 13, true) - case object `||` extends InfixOperator(10, 11, true) - case object `~>` extends InfixOperator(2, 2) - case object `|` extends InfixOperator(10, 11, true) - case object `\\odot` extends InfixOperator(13, 13, true) - case object `\\sim` extends InfixOperator(5, 5) - case object `\\o` extends InfixOperator(13, 13, true) - case object `\\sqsupseteq` extends InfixOperator(5, 5) - case object `-` extends InfixOperator(11, 11, true) - case object `<=>` extends InfixOperator(5, 5) - case object `@@` extends InfixOperator(6, 6, true) - case object `??` extends InfixOperator(9, 13, true) - case object `\\oplus` extends InfixOperator(10, 10, true) - case object `\\land` extends InfixOperator(3, 3) - case object `\\bigcirc` extends InfixOperator(13, 13) - case object `++` extends InfixOperator(10, 10, true) - case object `\\subset` extends InfixOperator(5, 5) - case object `#` extends InfixOperator(5, 5) - case object `\\subseteq` extends InfixOperator(5, 5) - case object `..` extends InfixOperator(9, 9) - case object `\\/` extends InfixOperator(3, 3, true) - case object `\\supseteq` extends InfixOperator(5, 5) - case object `\\uplus` extends InfixOperator(9, 13, true) - case object `?` extends InfixOperator(5, 5) - case object `(/)` extends InfixOperator(13, 13) - case object `\\geq` extends InfixOperator(5, 5) - case object `(.)` extends InfixOperator(13, 13) - case object `(\\X)` extends InfixOperator(13, 13) - case object `//` extends InfixOperator(13, 13) - case object `+` extends InfixOperator(10, 10, true) - case object `<:` extends InfixOperator(7, 7) - case object `\\doteq` extends InfixOperator(5, 5) - case object `...` extends InfixOperator(9, 9) - case object `&&` extends InfixOperator(13, 13, true) - case object `\\otimes` extends InfixOperator(13, 13, true) - case object `\\preceq` extends InfixOperator(5, 5) - case object `\\wr` extends InfixOperator(9, 14) - case object `\\gg` extends InfixOperator(5, 5) - case object `--` extends InfixOperator(11, 11, true) - case object `\\ll` extends InfixOperator(5, 5) - case object `\\intersect` extends InfixOperator(8, 8) - case object `\\sqsupset` extends InfixOperator(5, 5) - case object `$` extends InfixOperator(9, 13, true) - case object `\\cup` extends InfixOperator(8, 8, true) - case object `(+)` extends InfixOperator(10, 10) - case object `:=` extends InfixOperator(5, 5) - case object `!!` extends InfixOperator(9, 13) - case object `^` extends InfixOperator(14, 14) - case object `\\star` extends InfixOperator(13, 13, true) - case object `$$` extends InfixOperator(9, 13, true) - case object `>` extends InfixOperator(5, 5) - case object `_##_` extends InfixOperator(9, 13, true): - override def spelling: String = "##" - case object `-+->` extends InfixOperator(2, 2) - case object `/` extends InfixOperator(13, 13) - case object `\\sqsubset` extends InfixOperator(5, 5) - case object `\\supset` extends InfixOperator(5, 5) - case object `%%` extends InfixOperator(10, 11, true) - - sealed trait PostfixOperator(val precedence: Int) extends Operator: - def highPrecedence: Int = precedence - def lowPrecedence: Int = precedence - object PostfixOperator extends util.HasInstanceArray[PostfixOperator] - - case object `^+` extends PostfixOperator(15) - case object `^*` extends PostfixOperator(15) - case object `^#` extends PostfixOperator(15) - case object `'` extends PostfixOperator(15) diff --git a/langs/tla/package.scala b/langs/tla/package.scala deleted file mode 100644 index 232cfea..0000000 --- a/langs/tla/package.scala +++ /dev/null @@ -1,378 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.langs.tla - -import cats.syntax.all.given - -import forja.* -import forja.dsl.* -import forja.wf.WellformedDef - -object lang extends WellformedDef: - lazy val topShape: Shape = repeated(Module) - - object Module - extends t( - fields( - Id, - Module.Extends, - Module.Defns, - ), - ): - object Extends extends t(repeated(Id)) - object Defns - extends t( - repeated( - choice( - Local, - Recursive, - Operator, - Variable, - Constant, - Assumption, - Theorem, - Instance, - Module, - ModuleDefinition, - ), - ), - ) - end Module - - object Local - extends t( - choice( - Operator, - Instance, - ModuleDefinition, - ), - ) - - object Recursive - extends t( - choice( - Id, - Order2, - ), - ) - - object ModuleDefinition - extends t( - fields( - Id, - Operator.Params, - Instance, - ), - ) - - object Id extends t(Atom) - - object Ids extends t(repeated(Id, minCount = 1)) - - object OpSym extends t(choice(defns.Operator.instances.toSet)) - defns.Operator.instances.foreach(_ ::= Atom) - - object Order2 - extends t( - fields( - Id, - embedded[Int], - ), - ) - - object Expr - extends t( - choice( - Expr.NumberLiteral, - Expr.StringLiteral, - Expr.SetLiteral, - Expr.TupleLiteral, - Expr.RecordLiteral, - Expr.RecordSetLiteral, - Expr.Project, - Expr.OpCall, - Expr.FnCall, - Expr.If, - Expr.Case, - Expr.Let, - Expr.Exists, - Expr.Forall, - Expr.Function, - Expr.SetComprehension, - Expr.SetRefinement, - Expr.Choose, - Expr.Except, - Expr.Except.Anchor, - Expr.Lambda, - ), - ): - object NumberLiteral extends t(Atom) - - object StringLiteral extends t(Atom) - - object SetLiteral extends t(repeated(Expr)) - - object TupleLiteral extends t(repeated(Expr)) - - object RecordLiteral extends t(repeated(RecordLiteral.Field, minCount = 1)): - object Field - extends t( - fields( - Id, - Expr, - ), - ) - end RecordLiteral - - object Project - extends t( - fields( - Expr, - Id, - ), - ) - - object RecordSetLiteral - extends t( - repeated( - RecordSetLiteral.Field, - minCount = 1, - ), - ): - object Field - extends t( - fields( - Id, - Expr, - ), - ) - end RecordSetLiteral - - object OpCall - extends t( - fields( - choice(Id, OpSym), - OpCall.Params, - ), - ): - object Params extends t(repeated(Expr)) - end OpCall - - object FnCall extends t(fields(Expr, Expr)) - - object If - extends t( - fields( - Expr, - Expr, - Expr, - ), - ) - - object Case extends t(fields(Case.Branches, Case.Other)): - object Branches extends t(repeated(Branch, minCount = 1)) - object Branch - extends t( - fields( - Expr, - Expr, - ), - ) - object Other extends t(choice(Expr, None)) - object None extends t(Atom) - end Case - - object Let - extends t( - fields( - Let.Defns, - Expr, - ), - ): - object Defns - extends t( - repeated( - choice( - Operator, - ModuleDefinition, - Recursive, - ), - minCount = 1, - ), - ) - end Let - - object Exists - extends t( - fields( - QuantifierBounds, - Expr, - ), - ) - - object Forall - extends t( - fields( - QuantifierBounds, - Expr, - ), - ) - - object Function - extends t( - fields( - QuantifierBounds, - Expr, - ), - ) - - object SetComprehension - extends t( - fields( - Expr, - QuantifierBounds, - ), - ) - - object SetRefinement - extends t( - fields( - QuantifierBound, - Expr, - ), - ) - - object Choose - extends t( - fields( - QuantifierBound, - Expr, - ), - ) - - object Except - extends t( - fields( - Expr, - Except.Substitutions, - ), - ): - object Substitutions - extends t( - repeated( - Substitution, - minCount = 1, - ), - ) - - object Substitution - extends t( - fields( - Path, - Expr, - ), - ) - - object Path extends t(repeated(Expr, minCount = 1)) - - object Anchor extends t(Atom) - end Except - - object Lambda - extends t( - fields( - Lambda.Params, - Expr, - ), - ): - object Params extends t(repeated(Id, minCount = 1)) - end Lambda - end Expr - - object Operator - extends t( - fields( - choice(Id, OpSym), - Operator.Params, - Expr, - ), - ): - object Params - extends t( - repeated( - choice( - Id, - Order2, - ), - ), - ) - end Operator - - object Variable extends t(Id) - - object Constant extends t(choice(Id, Order2)) - - object Anonymous extends t(Atom) - - object Assumption - extends t( - fields( - choice(Id, Anonymous), - Expr, - ), - ) - - object Theorem - extends t( - fields( - choice(Id, Anonymous), - choice(Expr, Theorem.AssumeProve), - Theorem.Proofs, - ), - ): - object AssumeProve extends t(AnyShape) - object Proofs extends t(AnyShape) - end Theorem - - object UseOrHide extends t(AnyShape) - forceDef(UseOrHide) - - object Instance - extends t( - fields( - Id, - Instance.Substitutions, - ), - ): - object Substitutions extends t(repeated(Substitution)) - object Substitution - extends t( - fields( - choice(Id, OpSym), - Expr, - ), - ) - end Instance - - object QuantifierBound - extends t( - fields( - choice(Id, Ids), - Expr, - ), - ) - - object QuantifierBounds extends t(repeated(QuantifierBound, minCount = 1)) -end lang diff --git a/scripts/rewrite_src.scala b/scripts/rewrite_src.scala deleted file mode 100644 index 44ad053..0000000 --- a/scripts/rewrite_src.scala +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package scripts - -@main -def rewrite_src_sc(): Unit = - def cmd(parts: os.Shellable*): Unit = - println(s"$$ ${parts.flatten(using _._1).mkString(" ")}") - os.proc(parts*).call(cwd = os.pwd, stdout = os.Inherit, stderr = os.Inherit) - - cmd( - "scala-cli", - "run", - ".", - "--main-class", - "scripts.update_license_sc", - "--", - "rewrite", - ) - cmd("scala-cli", "fix", "--power", ".") - cmd("scala-cli", "format", ".") diff --git a/scripts/update_license.scala b/scripts/update_license.scala deleted file mode 100755 index 0d0ed01..0000000 --- a/scripts/update_license.scala +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package scripts - -import java.time.LocalDate -import java.time.format.DateTimeFormatter - -import scala.util.matching.Regex - -@main -def update_license_sc(args: String*): Unit = - var shouldRewrite = false - - val parser = new scopt.OptionParser[Unit]("update_license"): - cmd("rewrite") - .optional() - .foreach(_ => shouldRewrite = true) - .text("rewrite the source files") - cmd("dry-run") - .optional() - .foreach(_ => shouldRewrite = false) - .text("don't touch any of the files") - - if !parser.parse(args, ()).isDefined - then sys.exit(1) - - val licenseTemplate = - """ Copyright 2024-____ Forja Team - | - | Licensed under the Apache License, Version 2.0 (the "License"); - | you may not use this file except in compliance with the License. - | You may obtain a copy of the License at - | - | http://www.apache.org/licenses/LICENSE-2.0 - | - | Unless required by applicable law or agreed to in writing, software - | distributed under the License is distributed on an "AS IS" BASIS, - | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - | See the License for the specific language governing permissions and - | limitations under the License.""".stripMargin.linesIterator - .map(line => s"//$line") - .mkString(System.lineSeparator()) - - val yearString = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy")) - val licenseText = licenseTemplate.replace("____", yearString) - val licenseRegex = Regex( - raw"""(?s)// Copyright \d\d\d\d.*// limitations under the License\.""", - ) - val licenseReplacement = Regex.quoteReplacement(licenseText) - - var checkFailed = false - - os.walk(os.pwd) - .iterator - .filterNot(_.segments.exists(_.startsWith("."))) - .filterNot(_ == os.pwd / "project.scala") // conflict with fix rule - .filter(p => p.last.endsWith(".scala") || p.last.endsWith(".sc")) - .foreach: sourceFile => - val contents = os.read(sourceFile) - - val modifiedContents = - licenseRegex.findFirstIn(contents) match - case None => - licenseText - ++ System.lineSeparator() - ++ System.lineSeparator() - ++ contents - case Some(str) => - licenseRegex.replaceFirstIn(contents, licenseReplacement) - - if shouldRewrite - then os.write.over(sourceFile, modifiedContents) - else if contents != modifiedContents - then - checkFailed = true - println(s"license needs updating in $sourceFile") - - if shouldRewrite - then println("all changes made.") - else if checkFailed - then - locally { - /* Mystery: without locally { ... }, compiler rejects this. - * You can make it work with more indentation, but format changes it back - * to the inexplicably broken version. */ - println("check failed. TODO: update license info") - System.exit(1) - } - else println("check ok, all licenses up to date.") -end update_license_sc diff --git a/src/Builtin.scala b/src/Builtin.scala deleted file mode 100644 index 7772d1f..0000000 --- a/src/Builtin.scala +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja - -import cats.syntax.all.given - -object Builtin: - object Error extends Token: - def apply(msg: String, ast: Node.Child): Node = - Error( - Error.Message().at(msg), - Error.AST(ast), - ) - - object Message extends Token: - override val showSource = true - object AST extends Token - end Error - - object SourceMarker extends Token.ShowSource -end Builtin diff --git a/src/EmbedMeta.scala b/src/EmbedMeta.scala deleted file mode 100644 index 9e3e371..0000000 --- a/src/EmbedMeta.scala +++ /dev/null @@ -1,102 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja - -import java.io.OutputStream - -import forja.source.{Source, SourceRange} - -import geny.Writable -import izumi.reflect.Tag - -trait EmbedMeta[T](using val tag: Tag[T]): - def doClone(self: T): T = self - - def serialize(self: T): Writable - def deserialize(src: SourceRange): T - - final def canonicalName: String = - tag.tag.scalaStyledName - - override def equals(that: Any): Boolean = - that match - case that: EmbedMeta[?] => tag == that.tag - case _ => false - - override def hashCode(): Int = tag.hashCode() - - override def toString(): String = - s"EmbedMeta($canonicalName)" - -object EmbedMeta: - inline def apply[T: EmbedMeta]: EmbedMeta[T] = summon[EmbedMeta[T]] - - given embedString: EmbedMeta[String] with - def serialize(self: String): Writable = self - def deserialize(src: SourceRange): String = - src.decodeString() - - // all primitive types - given embedDouble: EmbedMeta[Double] with - def serialize(self: Double): Writable = self.toString() - def deserialize(src: SourceRange): Double = - src.decodeString().toDouble - given embedFloat: EmbedMeta[Float] with - def serialize(self: Float): Writable = self.toString() - def deserialize(src: SourceRange): Float = - src.decodeString().toFloat - given embedLong: EmbedMeta[Long] with - def serialize(self: Long): Writable = self.toString() - def deserialize(src: SourceRange): Long = - src.decodeString().toLong - given embedInt: EmbedMeta[Int] with - def serialize(self: Int): Writable = self.toString() - def deserialize(src: SourceRange): Int = - src.decodeString().toInt - given embedChar: EmbedMeta[Char] with - def serialize(self: Char): Writable = self.toString() - def deserialize(src: SourceRange): Char = - val str = src.decodeString() - assert(str.size == 1) - str.head - given embedShort: EmbedMeta[Short] with - def serialize(self: Short): Writable = self.toString() - def deserialize(src: SourceRange): Short = - src.decodeString().toShort - given embedByte: EmbedMeta[Byte] with - def serialize(self: Byte): Writable = new Writable: - def writeBytesTo(out: OutputStream): Unit = - out.write(self) - def deserialize(src: SourceRange): Byte = - assert(src.size == 1) - src.head - given embedBoolean: EmbedMeta[Boolean] with - private val t = SourceRange.entire(Source.fromString("true")) - private val f = SourceRange.entire(Source.fromString("false")) - - def serialize(self: Boolean): Writable = - if self then t else f - def deserialize(src: SourceRange): Boolean = - if src == t - then true - else if src == f - then false - else throw IllegalArgumentException(s"$src must be $t or $f") - - given embedSingleton[T <: Singleton: Tag](using ValueOf[T]): EmbedMeta[T] with - def serialize(self: T): Writable = "" - def deserialize(src: SourceRange): T = - assert(src.isEmpty) - summon[ValueOf[T]].value diff --git a/src/EmbedMeta.test.scala b/src/EmbedMeta.test.scala deleted file mode 100644 index 615e019..0000000 --- a/src/EmbedMeta.test.scala +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja - -import forja.dsl.* - -class EmbedMetaTests extends munit.FunSuite: - test("int == int"): - assert(EmbedMeta[Int] == EmbedMeta[Int]) - - test("int != long"): - assert(EmbedMeta[Int] != EmbedMeta[Long]) - - val n42 = Node.Embed(42) - - test("node with int"): - assert(n42.meta == EmbedMeta[Int]) - - test("pattern match"): - // for parent purposes - val top = Node.Top(n42) - - val manip = - initNode(n42): - on(embed[Int]).value - - assertEquals(manip.perform(), 42) - - // try rewriting, to be sure - val manip2 = - initNode(n42): - pass(strategy = pass.bottomUp, once = true) - .rules: - on(embed[Int]).rewrite: i => - splice(Node.Embed(43)) - - manip2.perform() - - assertEquals(top, Node.Top(Node.Embed(43))) - - // test("serialization"): - /* val serialized = - * Source.fromWritable(n42.toCompactWritable(Wellformed.empty)) */ - // val tree = sexpr.parse.fromSourceRange(SourceRange.entire(serialized)) - // val desern42 = Wellformed.empty.deserializeTree(tree) - // assertEquals(desern42, n42) - - // test("embed toString"): - // assertEquals(Node.Embed(42).toString(), "") diff --git a/src/Node.scala b/src/Node.scala deleted file mode 100644 index 5ab620c..0000000 --- a/src/Node.scala +++ /dev/null @@ -1,688 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja - -import cats.Eval -import cats.data.Chain -import cats.syntax.all.given - -import forja.dsl.* -import forja.source.{Source, SourceRange} -import forja.util.toShortString -import forja.wf.Wellformed - -import scala.annotation.constructorOnly -import scala.collection.mutable - -final case class NodeError(msg: String) extends RuntimeException(msg) - -final class Node(val token: Token)( - childrenInit: IterableOnce[Node.Child] @constructorOnly = Nil, -) extends Node.Child, - Node.Parent, - Node.Traversable: - thisNode => - - def this(token: Token)(childrenInit: Node.Child*) = - this(token)(childrenInit) - - // !! important !! - // these vars must init _before_ we make Children, otherwise - // Children's constructor will see the default 0 value even if it - // was supposed to start at 1 - - // maybe refactor this logic into some kind of Node.Counted interface? - private var _scopeRelevance: Int = - if token.canBeLookedUp then 1 else 0 - - private var _errorRefCount: Int = - if token == Builtin.Error - then 1 - else 0 - - val children: Node.Children = Node.Children(this, childrenInit) - - assertErrorRefCounts() - - override def asNode: Node = this - - override type This = Node - override def cloneEval(): Eval[Node] = - Chain - .traverseViaChain(children.toIndexedSeq)(_.cloneEval()) - .map(clonedChildren => Node(token)(clonedChildren.toIterable).like(this)) - - private var _sourceRange: SourceRange | Null = null - - def extendLocation(sourceRange: SourceRange): this.type = - if _sourceRange eq null - then _sourceRange = sourceRange - else _sourceRange = _sourceRange.nn <+> sourceRange - this - - def at(string: String): this.type = - at(Source.fromString(string)) - - def at(source: Source): this.type = - at(SourceRange.entire(source)) - - def at(sourceRange: SourceRange): this.type = - if _sourceRange eq null - then - _sourceRange = sourceRange - this - else throw NodeError("node source range already set") - - def like(other: Node): this.type = - if other._sourceRange eq null - then this - else at(other._sourceRange.nn) - - def sourceRange: SourceRange = - var rangeAcc: SourceRange | Null = null - traverse: - case node: Node => - if node._sourceRange ne null - then - if rangeAcc ne null - then rangeAcc = rangeAcc.nn <+> node._sourceRange.nn - else rangeAcc = node._sourceRange - Node.TraversalAction.SkipChildren - else Node.TraversalAction.Continue - case _: Node.Embed[?] => - Node.TraversalAction.SkipChildren - - if rangeAcc ne null - then rangeAcc.nn - else SourceRange.entire(Source.empty) - - override def unparent(unsafe: Boolean = false): this.type = - require(parent.nonEmpty) - if _scopeRelevance > 0 - then parent.get.decScopeRelevance() - if _errorRefCount > 0 - then parent.get.decErrorRefCount() - - val prevParent = parent.get - - super.unparent(unsafe) - this - - override def ensureParent(parent: Node.Parent, idxInParent: Int): this.type = - val prevParent = this.parent - - // We can ensureParent without unparent() if only the idx changes. - // In that case, don't inc twice. - val prevParentPtr = prevParent.getOrElse(null) - if _scopeRelevance > 0 && (prevParentPtr ne parent) - then parent.incScopeRelevance() - if _errorRefCount > 0 && (prevParentPtr ne parent) - then parent.incErrorRefCount() - - super.ensureParent(parent, idxInParent) - - prevParent.foreach(_.assertErrorRefCounts()) - parent.assertErrorRefCounts() - - this - - override def assertErrorRefCounts(): Unit = - // check only if configured; can be very very slow otherwise - if Node.assertErrorRefCorrectness - then - // may be called during object construction - if children ne null - then - val countErrors = - children.iterator.filter(_.hasErrors).size - + (if token == Builtin.Error then 1 else 0) - assert( - countErrors == _errorRefCount, - s"mismatched error counts, $countErrors != $_errorRefCount", - ) - - override def hasErrors: Boolean = - assertErrorRefCounts() - _errorRefCount > 0 - - override def errors: List[Node] = - val errorsAcc = mutable.ListBuffer.empty[Node] - traverse: - case thisNode: Node if thisNode.token == Builtin.Error => - errorsAcc += thisNode - Node.TraversalAction.SkipChildren - - case _: (Node | Node.Embed[?]) => Node.TraversalAction.Continue - - errorsAcc.result() - - def lookup: List[Node] = - assert(token.canBeLookedUp) - parent.map(_.findNodeByKey(thisNode)).getOrElse(Nil) - - def lookupRelativeTo(referencePoint: Node): List[Node] = - assert(token.canBeLookedUp) - referencePoint.findNodeByKey(thisNode) - - override def lookupKeys: Set[Node] = - this.inspect(token.lookedUpBy).getOrElse(Set.empty) -end Node - -object Node: - def unapplySeq(node: Node): Some[(Token, childrenAccessor)] = - Some((node.token, childrenAccessor((node.children)))) - - final class childrenAccessor(val children: Node.Children) extends AnyVal: - def length: Int = children.length - def apply(idx: Int): Node.Child = children(idx) - def drop(n: Int): Seq[Node.Child] = toSeq.drop(n) - def toSeq: Seq[Node.Child] = children.toSeq - - private val assertErrorRefCorrectness: Boolean = - val prop = System.getProperty("distcompiler.Node.assertErrorRefCorrectness") - (prop ne null) && prop.nn.toLowerCase() == "yes" - - enum TraversalAction: - case SkipChildren, Continue - end TraversalAction - - sealed trait Traversable: - thisTraversable => - - final def traverse(fn: Node.Child => Node.TraversalAction): Unit = - extension (self: Node.Child) - def findNextChild: Option[Node.Child] = - var curr = self - def extraConditions: Boolean = - curr.parent.nonEmpty - /* .rightSibling looks at the parent node. If we're looking at - * thisTraversable, then that means we should stop or we'll be - * traversing our parent's siblings. */ - && (curr ne thisTraversable) - && !curr.parent.get.isInstanceOf[Node.Top] - - while curr.rightSibling.isEmpty - && extraConditions - do curr = curr.parent.get.asNode - - if !extraConditions - then None - else curr.rightSibling - - @scala.annotation.tailrec - def impl(traversable: Node.Child): Unit = - traversable match - case node: Node => - import TraversalAction.* - fn(node) match - case SkipChildren => - node.findNextChild match - case None => - case Some(child) => impl(child) - case Continue => - node.firstChild match - case None => - node.findNextChild match - case None => - case Some(child) => impl(child) - case Some(child) => impl(child) - case embed: Embed[?] => - fn(embed) // result doesn't matter as it has no children - embed.findNextChild match - case None => - case Some(child) => impl(child) - - this match - case thisChild: Node.Child => impl(thisChild) - case thisTop: Node.Top => - thisTop.children.iterator - .foreach(impl) - end Traversable - - object Hole extends Token - - sealed trait All extends Cloneable: - type This <: All - def cloneEval(): Eval[This] - - def assertErrorRefCounts(): Unit = () - - final override def clone(): This = - cloneEval().value - - final def inspect[T](manip: Manip[T]): Option[T] = - import dsl.* - initNode(this)(manip.map(Some(_)) | Manip.pure(None)) - .perform() - - def asNode: Node = - throw NodeError("not a node") - - def asTop: Top = - throw NodeError("not a top") - - def asParent: Parent = - throw NodeError("not a parent") - - def asChild: Child = - throw NodeError("not a child") - - def isChild: Boolean = false - - def hasErrors: Boolean - - def errors: List[Node] - - def parent: Option[Node.Parent] = None - - def idxInParent: Int = - throw NodeError("tried to get index in node that cannot have a parent") - - final override def hashCode(): Int = - this match - case node: Node => - (Node, node.token, node.children).hashCode() - case top: Top => - (Top, top.children).hashCode() - case Embed(value) => - (Embed, value).hashCode() - - final override def equals(that: Any): Boolean = - if this eq that.asInstanceOf[AnyRef] - then return true - (this, that) match - case (thisNode: Node, thatNode: Node) => - thisNode.token == thatNode.token - && (if thisNode.token.showSource - then thisNode.sourceRange == thatNode.sourceRange - else true) - && thisNode.children == thatNode.children - case (thisTop: Top, thatTop: Top) => - thisTop.children == thatTop.children - case (thisEmbed: Embed[?], thatEmbed: Embed[?]) => - thisEmbed.value == thatEmbed.value - case _ => false - - final override def toString(): String = - val str = - sexpr.serialize.toPrettyString(Wellformed.empty.serializeTree(this)) - - if this.isInstanceOf[Top] - then s"[top] $str" - else str - - final def presentErrors(debug: Boolean = false): String = - require(hasErrors) - errors - .map: err => - val msg = err(Builtin.Error.Message) - val ast = err(Builtin.Error.AST) - - s"${msg.sourceRange.decodeString()} at ${ast.sourceRange.presentationStringLong}\n${ - if debug then ast.toShortString() else "" - }" - .mkString("\n") - - final def serializedBy(wf: Wellformed): This = - wf.serializeTree(this).asInstanceOf[This] - - final def toPrettyWritable(wf: Wellformed): geny.Writable = - sexpr.serialize.toPrettyWritable(serializedBy(wf)) - - final def toCompactWritable(wf: Wellformed): geny.Writable = - sexpr.serialize.toCompactWritable(serializedBy(wf)) - - final def toPrettyString(wf: Wellformed): String = - sexpr.serialize.toPrettyString(serializedBy(wf)) - - final class Top(childrenInit: IterableOnce[Node.Child] @constructorOnly) - extends Parent, - Traversable: - def this(childrenInit: Node.Child*) = this(childrenInit) - - val children: Children = Node.Children(this, childrenInit) - - override def asTop: Top = this - - override type This = Top - override def cloneEval(): Eval[Top] = - Chain - .traverseViaChain(children.toIndexedSeq)(_.cloneEval()) - .map(_.toIterable) - .map(Top(_)) - - override def hasErrors: Boolean = children.exists(_.hasErrors) - override def errors: List[Node] = - children.iterator - .filter(_.hasErrors) - .flatMap(_.errors) - .toList - end Top - - object Top - - sealed trait Parent extends All: - thisParent => - - override type This <: Parent - - override def asParent: Parent = this - - val children: Node.Children - - final def apply(tok: Token, toks: Token*): Node = - val results = children.iterator - .collect: - case node: Node if node.token == tok || toks.contains(node.token) => - node - .toList - - require( - results.size == 1, - s"token(s) not found ${(tok +: toks).map(_.name).mkString(", ")}, in ${this.toShortString()}", - ) - results.head - - final def firstChild: Option[Node.Child] = - children.headOption - - @scala.annotation.tailrec - private[Node] final def incScopeRelevance(): Unit = - this match - case root: Node.Top => // nothing to do here - case thisNode: Node => - thisNode._scopeRelevance += 1 - if thisNode._scopeRelevance == 1 - then - thisNode.parent match - case None => - case Some(parent) => parent.incScopeRelevance() - - @scala.annotation.tailrec - private[Node] final def decScopeRelevance(): Unit = - this match - case root: Node.Top => // nothing to do here - case thisNode: Node => - assert(thisNode._scopeRelevance > 0) - thisNode._scopeRelevance -= 1 - if thisNode._scopeRelevance == 0 - then - thisNode.parent match - case None => - case Some(parent) => parent.decScopeRelevance() - - @scala.annotation.tailrec - private[Node] final def incErrorRefCount(): Unit = - this match - case root: Node.Top => // nothing to do here - case thisNode: Node => - thisNode._errorRefCount += 1 - if thisNode._errorRefCount == 1 - then - thisNode.parent match - case None => - case Some(parent) => parent.incErrorRefCount() - - @scala.annotation.tailrec - private[Node] final def decErrorRefCount(): Unit = - this match - case root: Node.Top => // nothing to do here - case thisNode: Node => - thisNode._errorRefCount -= 1 - if thisNode._errorRefCount == 0 - then - thisNode.parent match - case None => - case Some(parent) => parent.decErrorRefCount() - - @scala.annotation.tailrec - private[Node] final def findNodeByKey(key: Node): List[Node] = - this match - case root: Node.Top => Nil - case thisNode: Node - if thisNode.token.symbolTableFor.contains(key.token) => - import Node.TraversalAction.* - val resultsList = mutable.ListBuffer.empty[Node] - thisNode.traverseChildren: - case irrelevantNode: Node if irrelevantNode._scopeRelevance == 0 => - SkipChildren - case descendantNode: Node => - if !descendantNode.token.canBeLookedUp - then TraversalAction.Continue - else - descendantNode.inspect(descendantNode.token.lookedUpBy) match - case None => TraversalAction.Continue - case Some(descendantKey) => - if key == descendantKey - then resultsList.addOne(descendantNode) - TraversalAction.Continue - case _: Node.Embed[?] => SkipChildren - - resultsList.result() - case thisNode: Node => - thisNode.parent match - case None => Nil - case Some(parent) => parent.findNodeByKey(key) - - final def traverseChildren(fn: Node.Child => TraversalAction): Unit = - children.iterator.foreach(_.traverse(fn)) - - final def children_=(childrenInit: IterableOnce[Node.Child]): Unit = - children.clear() - children.addAll(childrenInit) - - final def unparentedChildren - : scala.collection.mutable.ArraySeq[Node.Child] = - // .unparent() is done by Children .clear - val result = children.toArray - children.clear() - result - end Parent - - sealed trait Child extends Traversable, All: - thisChild => - - override type This <: Child - - override def isChild: Boolean = true - - override def asChild: Child = this - - private var _parent: Parent | Null = null - private var _idxInParent: Int = -1 - - def ensureParent(parent: Parent, idxInParent: Int): this.type = - val oldParent = _parent - if _parent eq parent - then - /* Reparenting within the same parent shouldn't really do anything, so - * don't make a fuss if it happens. The seq ops on Children might do - * this. */ - _idxInParent = idxInParent - this - else if _parent eq null - then - _parent = parent - _idxInParent = idxInParent - this - else throw NodeError("node already has a parent") - - if oldParent ne null - then oldParent.assertErrorRefCounts() - if parent ne null - then parent.assertErrorRefCounts() - - this - - def unparent(unsafe: Boolean = false): this.type = - assert(_parent ne null, "tried to unparent floating node") - val safe = !unsafe - val oldParent = _parent - - if safe - then _parent.nn.children.makeHole(_idxInParent) - - _parent = null - _idxInParent = -1 - - if safe && (oldParent ne null) - then oldParent.assertErrorRefCounts() - - this - - override def parent: Option[Parent] = - _parent match - case null => None - case _parent: Parent => Some(_parent) - - override def idxInParent: Int = - if _parent eq null - then throw NodeError("tried to get index in parent of floating node") - assert(_idxInParent >= 0) - _idxInParent - - def lookupKeys: Set[Node] = Set.empty - - final def replaceThis[R <: Node.Child](replacement: => R): R = - require(parent.nonEmpty) - val parentTmp = parent.get - val idxInParentTmp = idxInParent - val computedReplacement = replacement - parentTmp.children(idxInParentTmp) = computedReplacement - computedReplacement - - final def removeThis(): this.type = - require(parent.nonEmpty) - val parentTmp = parent.get - val idxInParentTmp = idxInParent - parentTmp.children.remove(idxInParentTmp).asInstanceOf[this.type] - - final def rightSibling: Option[Node.Child] = - parent.flatMap: parent => - parent.children.lift(idxInParent + 1) - end Child - - final case class Embed[T](value: T)(using val meta: EmbedMeta[T]) - extends Child: - override type This = Embed[T] - override def cloneEval(): Eval[Embed[T]] = - Eval.now(Embed(meta.doClone(value))) - - override def hasErrors: Boolean = false - override def errors: List[Node] = Nil - end Embed - - object EmbedT extends Token - - final class Children private[Node] ( - val parent: Node.Parent, - childrenInit: IterableOnce[Node.Child] @constructorOnly, - ) extends mutable.IndexedBuffer[Node.Child]: - private val _children = mutable.ArrayBuffer.from(childrenInit) - _children.iterator.zipWithIndex.foreach: (child, idx) => - child.ensureParent(parent, idx) - export _children.{apply, length} - - private def reIdxFromIdx(idx: Int): this.type = - var curr = idx - while curr < length - do - _children(curr).ensureParent(parent, curr) - curr += 1 - this - - private[Node] def makeHole(idx: Int): Unit = - val hole = Hole() - _children(idx) match - case node: Node => hole.like(node) - case _: Node.Embed[?] => - _children(idx) = hole - hole.ensureParent(parent, idx) - - // can't export due to redef rules - override def knownSize: Int = _children.knownSize - - def ensureWithKey(elem: Node): this.type = - val keys = elem.lookupKeys - if !_children.exists(_.lookupKeys.intersect(keys).nonEmpty) - then addOne(elem) - this - - override def prepend(elem: Node.Child): this.type = - _children.prepend(elem.ensureParent(parent, 0)) - reIdxFromIdx(1) - parent.assertErrorRefCounts() - this - - override def insert(idx: Int, elem: Node.Child): Unit = - _children.insert(idx, elem) - elem.ensureParent(parent, idx) - reIdxFromIdx(idx + 1) - parent.assertErrorRefCounts() - - @scala.annotation.tailrec - override def insertAll(idx: Int, elems: IterableOnce[Node.Child]): Unit = - elems match - case elems: Iterable[Node.Child] => - /* Keeping this separate allows ensureParent to fail without - * corrupting the structure. */ - elems.iterator.zipWithIndex - .foreach: (child, childIdx) => - child.ensureParent(parent, idx + childIdx) - - _children.insertAll(idx, elems) - reIdxFromIdx(idx + elems.size) - parent.assertErrorRefCounts() - case elems => insertAll(idx, mutable.ArrayBuffer.from(elems)) - - override def remove(idx: Int): Node.Child = - _children(idx).unparent(unsafe = true) - val child = _children.remove(idx) - reIdxFromIdx(idx) - parent.assertErrorRefCounts() - child - - override def remove(idx: Int, count: Int): Unit = - (idx until idx + count).foreach: childIdx => - _children(childIdx).unparent(unsafe = true) - _children.remove(idx, count) - reIdxFromIdx(idx) - parent.assertErrorRefCounts() - - override def clear(): Unit = - _children.foreach(_.unparent(unsafe = true)) - _children.clear() - parent.assertErrorRefCounts() - - override def addOne(elem: Child): this.type = - val idx = length - _children.addOne(elem) - elem.ensureParent(parent, idx) - parent.assertErrorRefCounts() - this - - override def update(idx: Int, child: Node.Child): Unit = - val existingChild = _children(idx) - if existingChild ne child - then - existingChild.unparent(unsafe = true) - _children(idx) = child - child.ensureParent(parent, idx) - parent.assertErrorRefCounts() - - override def iterator: Iterator[Node.Child] = - (0 until length).iterator.map(this) - end Children -end Node diff --git a/src/PassSeq.scala b/src/PassSeq.scala deleted file mode 100644 index 77b3171..0000000 --- a/src/PassSeq.scala +++ /dev/null @@ -1,116 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja - -import cats.syntax.all.given - -import forja.dsl.* -import forja.wf.Wellformed - -/** A sequence of passes that iteratively transform a tree of [[forja.Node]]. - * - * While technically everything this does can be replicated with relatively - * simple [[forja.manip.Manip]] compositions, the goal here is to provide a - * default structure for specifying any sort of transformation workflow. See - * [[forja.langs.calc]] for an example of this trait in action. - */ -transparent trait PassSeq: - export PassSeq.Pass - - /** Returns the passes that make up this sequence, in the order they should be - * applied. - */ - def passes: List[Pass] - - /** Returns the intended input structure for the first pass in the sequence, - * which is also the starting [[forja.PassSeq.Pass#prevWellformed]] that each - * pass may transform. - */ - def inputWellformed: Wellformed - - /** Returns the expected final Wellformed after running all passes. - */ - final def outputWellformed: Wellformed = outputWellformedImpl - - /** Returns a Manip representing all passes in aggregate. This is what - * [[forja.PassSeq#apply]] uses internally. - */ - final def allPasses: Manip[Unit] = allPassesImpl - - private final lazy val ( - outputWellformedImpl: Wellformed, - allPassesImpl: Manip[Unit], - ) = - def whenNoErrors(manip: Manip[Unit]): Manip[Unit] = - commit: - (getNode.filter(!_.hasErrors) *> commit(manip)) - | Manip.unit - - val initWf = inputWellformed - passes.foldLeft((initWf, initWf.markErrorsPass)): (acc, pass) => - val (prevWf, accRules) = acc - val wf = pass.wellformed(using PassSeq.BuildCtx(prevWf)) - ( - wf, - accRules *> whenNoErrors: - pass.rules - *> wf.markErrorsPass, - ) - - /** Run all [[forja.PassSeq#passes]] in sequence on a given - * [[forja.Node.Top]]. - * - * Addionally, all wellformed assertions will be performed, and the passes - * will stop on error. The transformation is destructive. Use - * [[forja.Node#clone]] to copy the tree if you want to keep the original. - * - * @param top - * root of the [[forja.Node]] tree to transform - */ - final def apply(top: Node.Top): Unit = - initNode(top)(allPasses).perform() -end PassSeq - -object PassSeq: - /** Contextual information about the sequence where this Pass is being used. - * If the Pass is used in multiple places, relevant parts will be recomputed - * with a different context. - * - * @param inputWellformed - * the previous pass's output, or the intial Wellformed. - */ - final class BuildCtx private[PassSeq] (val inputWellformed: Wellformed) - - abstract class Pass: - /** Returns the previous pass's [[forja.wf.Wellformed]]. - */ - protected def prevWellformed(using ctx: BuildCtx): Wellformed = - ctx.inputWellformed - - /** Returns the [[forja.wf.Wellformed]] that outputs of - * [[forja.PassSeq.Pass#rules]] should satisfy. - * @note - * For maximum reusability, define this in terms of - * [[forja.PassSeq.Pass#prevWellformed]] - */ - def wellformed: BuildCtx ?=> Wellformed - - /** Returns a [[forja.manip.Manip]] defining what this pass does. - * @note - * Normally defined using a variation of [[forja.manip.ManipOps#pass]] - */ - def rules: Manip[Unit] - end Pass -end PassSeq diff --git a/src/Token.scala b/src/Token.scala deleted file mode 100644 index d5f2cfe..0000000 --- a/src/Token.scala +++ /dev/null @@ -1,158 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja - -import java.lang.ref.{ReferenceQueue, WeakReference} - -import forja.dsl.* -import forja.source.SourceRange -import forja.util.{Named, TokenMapFactory} - -/** Defines a "token", a unique identifier used to label and distinguish - * [[forja.Node]] instances. - * - * The name is a list of string segments, following an `x.y.z` pattern which - * maps conceptually to the token object's own name and its package / enclosing - * object prefix. - * - * If this trait is directly inherited by an object or defined using - * [[forja.wf.WellformedDef#t]], then it will guess its own name using macros - * (see [[forja.util.Named]]). If doing something more esoteric, pass the - * correct name as an implicit parameter [[forja.util.Named.OwnName]] to the - * [[forja.util.Named]] supertrait. - * - * @note - * The name is the unique identifier, not object identity, so while Scala's - * type system makes it hard to get name collisions, they are possible. - * @note - * Name inference is currently known not to work with enum syntax. - */ -trait Token extends Named, TokenMapFactory.Mapped: - private val sym = Token.TokenSym(nameSegments) - - /** Checks whether this and that have the same token name. - * - * Given that two tokens with the same name must also have the same identity, - * this is done efficiently using an identity comparison. - */ - final override def equals(that: Any): Boolean = - that match - case tok: Token => sym == tok.sym - case _ => false - - /** Returns a hash code representing the token's name. - */ - final override def hashCode(): Int = sym.hashCode() - - /** Forwards to the constructor of [[forja.Node]]. - */ - final def mkNode(childrenInit: IterableOnce[Node.Child] = Nil): Node = - Node(this)(childrenInit) - - /** Forwards to the constructor of [[forja.Node]]. - */ - final def mkNode(childrenInit: Node.Child*): Node = - Node(this)(childrenInit) - - override def toString(): String = - s"Token($name)" - - final def canBeLookedUp: Boolean = !lookedUpBy.isBacktrack - - def symbolTableFor: Set[Token] = Set.empty - def lookedUpBy: Manip[Set[Node]] = backtrack - def showSource: Boolean = false -end Token - -object Token: - private final class TokenSym private (val nameSegments: List[String]): - override def equals(that: Any): Boolean = - this `eq` that.asInstanceOf[AnyRef] - override def hashCode(): Int = nameSegments.hashCode() - end TokenSym - - private object TokenSym: - private final class TokenSymRef( - val nameSegments: List[String], - sym: TokenSym, - ) extends WeakReference[TokenSym](sym, canonicalRefQueue) - - private val canonicalRefQueue = ReferenceQueue[TokenSym]() - private val canonicalMap = - scala.collection.concurrent.TrieMap[List[String], TokenSymRef]() - - private def cleanQueue(): Unit = - var ref: TokenSymRef | Null = null - while - ref = canonicalRefQueue.poll().asInstanceOf[TokenSymRef | Null] - ref ne null - do canonicalMap.remove(ref.nameSegments) - end while - - def apply(nameSegments: List[String]): TokenSym = - var sym: TokenSym | Null = null - /* If invoked, forms a GC root for our new sym so it won't be reclaimed - * immediately after construction. */ - lazy val freshSym = new TokenSym(nameSegments) - while sym eq null do - cleanQueue() - val ref = canonicalMap.getOrElseUpdate( - nameSegments, - TokenSymRef(nameSegments, freshSym), - ) - /* We might have just barely sniped a ref that cleanQueue missed. If sym - * is null, go around again. */ - sym = ref.get() - end while - sym - end TokenSym - - /** Helper trait that overrides [[forja.Token#showSource]] to return true. - */ - trait ShowSource extends Token: - override def showSource: Boolean = true - - extension (token: Token) - /** `Token(x, y, z)` constructor syntax. - */ - def apply(children: Node.Child*): Node = - Node(token)(children) - - /** `Token(iter)` constructor syntax. - */ - def apply(children: IterableOnce[Node.Child]): Node = - Node(token)(children) - - /** `Token("foo")` constructor syntax, setting the source location to "foo" - * (implies no children). - */ - def apply(sourceRange: String): Node = - Node(token)().at(sourceRange) - - /** `Token(sourceRange)` constructor syntax (implies no children). - */ - def apply(sourceRange: SourceRange): Node = - Node(token)().at(sourceRange) - - /** Allow patten matching using Token. - * - * @example - * {{{??? match case Tok(child1, child2) => ???}}} - */ - def unapplySeq(node: Node): Option[Node.childrenAccessor] = - if node.token == token - then Some(Node.childrenAccessor(node.children)) - else None -end Token diff --git a/src/Token.test.scala b/src/Token.test.scala deleted file mode 100644 index b0119ce..0000000 --- a/src/Token.test.scala +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja - -import forja.util.Named - -final class TokenTests extends munit.FunSuite: - import TokenTests.* - - test("tokens =="): - assert(t1 == t1) - assert(t2 == t2) - - test("tokens !="): - assert(t1 != t2) - assert(t2 != t1) - - class Tok(name: String) extends Token, Named(using Named.OwnName(List(name))) - - test("GC churn"): - /* I manually tested that this actually triggers GC quite often on my - * machine. Not sure how to enforce this, though. */ - (0 until 100).foreach: _ => - (0 until 100).foreach: _ => - assertEquals(Tok("foo"), Tok("foo")) - System.gc() - Thread.sleep(100) -end TokenTests - -object TokenTests: - object t1 extends Token - object t2 extends Token -end TokenTests diff --git a/src/dsl.scala b/src/dsl.scala deleted file mode 100644 index 391ccd9..0000000 --- a/src/dsl.scala +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja - -import forja.manip.* -import forja.wf.Shape - -object dsl extends ManipOps, SeqPatternOps, Shape.Ops: - export forja.{Node, Token} - export forja.manip.{Manip, SeqPattern} - export forja.source.SourceRange.src - export Manip.Rules - export forja.wf.Shape - export Shape.{AnyShape, Atom} diff --git a/src/manip/DebugAdapter.scala b/src/manip/DebugAdapter.scala deleted file mode 100644 index 57a1f4e..0000000 --- a/src/manip/DebugAdapter.scala +++ /dev/null @@ -1,713 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.manip - -import java.io.Closeable -import java.net.InetSocketAddress -import java.nio.channels.{ - AsynchronousCloseException, - Channels, - ClosedChannelException, - ServerSocketChannel, - SocketChannel, -} -import java.nio.charset.StandardCharsets - -import forja.* -import forja.util.ById - -import scala.collection.mutable - -final class DebugAdapter(host: String, port: Int) extends Tracer: - import DebugAdapter.{EvalState, EvalTag, SourceRecord, StackEntry} - private val io = DebugAdapter.IO(host, port) - - @scala.annotation.tailrec - private def handlePointOfInterest( - debugInfo: DebugInfo, - evalTag: DebugAdapter.EvalTag, - ): Unit = - io.witnessSources(debugInfo) - val shouldWait = - io.withState: state => - evalTag match - case EvalTag.Backtrack => - case EvalTag.FatalError => - case EvalTag.Commit => - state.frameIdCounter += state.stackTrace.size - state.stackTrace.clear() - case EvalTag.BeforeRewrite => - case EvalTag.AfterRewrite => - case EvalTag.BeforePass => - case EvalTag.AfterPass => - - val stackEntry = StackEntry( - source = SourceRecord( - fileName = debugInfo.file, - ), - tag = evalTag, - line = debugInfo.line, - ) - - if state.stackTrace.headOption != Some(stackEntry) - then state.stackTrace += stackEntry - - def enforceRunningState(): Unit = - state.debugInfo = null - state.currHandle = null - state.clearNodeRefs() - - def beginPause(): Unit = - println(s"pausing...") - state.debugInfo = debugInfo - state.currHandle = currHandle - state.evalState = EvalState.Paused - - state.evalState match - case EvalState.Running => - enforceRunningState() - false - case EvalState.Pausing => - enforceRunningState() - beginPause() - io.witnessPause(evalTag) - true - case EvalState.Paused => - // println(s"paused...") - state.debugInfo = debugInfo - state.currHandle = currHandle - true - case EvalState.Stepping => - // println(s"stepping...") - enforceRunningState() - state.evalState = EvalState.Pausing - false - case EvalState.ContinueNextRewrite => - enforceRunningState() - state.evalState = EvalState.NextRewriteSearch - false - case EvalState.NextRewriteSearch => - enforceRunningState() - evalTag match - case EvalTag.BeforeRewrite => - beginPause() - io.witnessPause(evalTag) - true - case _ => false - - if shouldWait - then - io.awaitStateUpdate() - // println("wake up...") - handlePointOfInterest(debugInfo, evalTag) - - def close(): Unit = - io.close() - - def beforePass(debugInfo: DebugInfo): Unit = - handlePointOfInterest(debugInfo, EvalTag.BeforePass) - - def afterPass(debugInfo: DebugInfo): Unit = - handlePointOfInterest(debugInfo, EvalTag.AfterPass) - - def onRead( - manip: Manip[?], - ref: Manip.Ref[?], - value: Any, - debugInfo: DebugInfo, - ): Unit = - () // TODO: implement - - private var currHandle: Option[Handle] = None - - def onAssign(manip: Manip[?], ref: Manip.Ref[?], value: Any): Unit = - if ref == Handle.ref - then currHandle = Some(value.asInstanceOf[Handle]) - // TODO: implement - - def onDel(manip: Manip[?], ref: Manip.Ref[?], debugInfo: DebugInfo): Unit = - if ref == Handle.ref - then currHandle = None - () // TODO: implement - - def onRewriteMatch( - debugInfo: DebugInfo, - parent: Node.Parent, - idx: Int, - matchedCount: Int, - ): Unit = - handlePointOfInterest( - debugInfo, - EvalTag.BeforeRewrite, - ) - - def onRewriteComplete( - debugInfo: DebugInfo, - parent: Node.Parent, - idx: Int, - resultCount: Int, - ): Unit = - handlePointOfInterest(debugInfo, EvalTag.AfterRewrite) - - def onBranch(manip: Manip[?], debugInfo: DebugInfo): Unit = - // TODO: implement - () - - def onCommit(manip: Manip[?], debugInfo: DebugInfo): Unit = - handlePointOfInterest(debugInfo, EvalTag.Commit) - - def onBacktrack(manip: Manip[?], debugInfo: DebugInfo): Unit = - handlePointOfInterest(debugInfo, EvalTag.Backtrack) - - def onFatal(manip: Manip[?], debugInfo: DebugInfo, from: DebugInfo): Unit = - handlePointOfInterest(debugInfo, EvalTag.FatalError) - -object DebugAdapter: - final case class SourceRecord(fileName: String): - def asJSON: ujson.Obj = - ujson.Obj( - "name" -> fileName, - "path" -> fileName, - ) - - enum EvalState: - case Paused, Pausing - case Running - case Stepping - case ContinueNextRewrite, NextRewriteSearch - - enum EvalTag: - case Backtrack - case FatalError - case Commit - case BeforeRewrite - case AfterRewrite - case BeforePass - case AfterPass - - def desc: String = - this match - case Backtrack => "on backtrack" - case FatalError => "on fatal error" - case Commit => "on commit" - case BeforeRewrite => "before rewrite" - case AfterRewrite => "after rewrite" - case BeforePass => "before pass" - case AfterPass => "after pass" - - final case class StackEntry(source: SourceRecord, tag: EvalTag, line: Int) - - final class State: - var evalState = EvalState.Paused - - var evalTag: EvalTag | Null = null - var debugInfo: DebugInfo | Null = null - var currHandle: Option[Handle] | Null = null - var frameIdCounter = 0 - - val knownSources = mutable.HashSet[SourceRecord]() - val stackTrace = mutable.ArrayBuffer[StackEntry]() - - def clearNodeRefs(): Unit = - nodeIdx = 1 - nodeById.clear() - idByNode.clear() - - private var nodeIdx = 1 - private val nodeById = mutable.HashMap[Int, Node.All]() - private val idByNode = mutable.HashMap[ById[Node.All], Int]() - - def refOf(node: Node.All): Int = - idByNode.get(ById(node)) match - case Some(idx) => idx - case None => - val idx = nodeIdx - nodeIdx += 1 - nodeById(idx) = node - idByNode(ById(node)) = idx - idx - - def refOfHandle(handle: Handle): Int = - handle match - case Handle.AtChild(_, _, child) => refOf(child) - case Handle.AtTop(top) => refOf(top) - case Handle.Sentinel(parent, _) => refOf(parent) - - def getNodeByRef(ref: Int): Option[Node.All] = - nodeById.get(ref) - - object DAPProtocolMessage: - def unapply(js: ujson.Value): Option[(Int, String, ujson.Obj)] = - js match - case obj: ujson.Obj - if obj("seq").numOpt - .exists(_.isValidInt) && obj("type").strOpt.nonEmpty => - Some((obj("seq").num.toInt, obj("type").str, obj)) - case _ => None - - object DAPRequest: - def unapply(js: ujson.Value): Option[(Int, String, ujson.Value)] = - js match - case DAPProtocolMessage(seq, "request", obj) - if obj("command").strOpt.nonEmpty => - Some( - ( - seq, - obj("command").str, - obj.obj.getOrElse("arguments", ujson.Obj()), - ), - ) - case _ => None - - final class IO(host: String, port: Int) extends java.io.Closeable: - io => - - def witnessSources(debugInfo: DebugInfo): Unit = - this.connectionOpt match - case None => - withState: state => - state.knownSources += SourceRecord(debugInfo.file) - case Some(connection) => - withState: state => - val record = SourceRecord(debugInfo.file) - if !state.knownSources(record) - then - state.knownSources += record - connection.sendSource(record) - - def witnessPause(evalTag: EvalTag): Unit = - this.connectionOpt match - case None => - case Some(connection) => - connection.sendEvent( - event = "stopped", - body = ujson.Obj( - "reason" -> "step", - "description" -> s"Paused ${evalTag.desc}", - "threadId" -> 1, - ), - ) - - private final class Connection(sock: SocketChannel) - extends Runnable, - Closeable: - conn => - private val input = Channels.newInputStream(sock) - private val output = Channels.newOutputStream(sock) - private var responseIdx = 0 - private val thread = Thread(this, "debug-adapter-connection") - thread.start() - - private object state: - private val headerParts = mutable.ListBuffer.empty[String] - private val headerPattern = raw"""Content-Length: (\d+)""".r - private val builder = StringBuilder() - - def onChar(char: Char): Unit = - (builder.lastOption, char) match - case (Some('\r'), '\n') => - builder.deleteCharAt(builder.size - 1) - val part = builder.result() - builder.clear() - if part == "" - then onHeader() - else headerParts += part - case (_, ch) => builder += ch - - def onHeader(): Unit = - var contentLength = -1 - headerParts.foreach: - case headerPattern(contentLengthStr) => - contentLength = contentLengthStr.toInt - case part => - throw AssertionError(s"unrecognized DAP header: $part") - - assert(contentLength != -1, "content length missing from DAP header") - - val bytes = input.readNBytes(contentLength) - handleMsg(ujson.read(bytes)) - - def run(): Unit = - def closedConnection(): Unit = - connectionOpt = None - println(s"closed connection") - while true - do - try - val result = input.read() - if result == -1 - then - sock.close() - closedConnection() - return - - state.onChar(result.toChar) - catch - case _: (ClosedChannelException | AsynchronousCloseException) => - // we are closing, goodbye - closedConnection() - return - - def sendMsg(payload: ujson.Obj): Unit = - // synchronized, because close() might call us to send a terminate event - conn.synchronized: - payload("seq") = responseIdx - responseIdx += 1 - // println(s"send $payload") - val payloadBytes = ujson.writeToByteArray(payload) - output.write( - s"Content-Length: ${payloadBytes.length}\r\n\r\n" - .getBytes(StandardCharsets.UTF_8), - ) - output.write(payloadBytes) - - def sendResp( - requestSeq: Int, - command: String, - body: ujson.Value = ujson.Null, - ): Unit = - sendMsg( - ujson.Obj( - "request_seq" -> requestSeq, - "success" -> true, - "command" -> command, - "type" -> "response", - "body" -> body, - ), - ) - - def sendEvent( - event: String, - body: ujson.Value = ujson.Null, - ): Unit = - sendMsg( - ujson.Obj( - "type" -> "event", - "event" -> event, - "body" -> body, - ), - ) - - def sendSource(record: SourceRecord): Unit = - sendEvent( - event = "loadedSource", - body = ujson.Obj( - "reason" -> "new", - "source" -> record.asJSON, - ), - ) - - private def handleMsg(msg: ujson.Value): Unit = - msg match - case DAPRequest(seq, "initialize", req) => - // println(s"initialize req: $req") - sendResp( - requestSeq = seq, - command = "initialize", - ) - case DAPRequest(seq, "attach", req) => - // println(s"attach req: $req") - sendResp( - requestSeq = seq, - command = "attach", - ) - sendEvent( - event = "stopped", - body = ujson.Obj( - "reason" -> "pause", - "description" -> "Ready to start", - "threadId" -> 1, - ), - ) - withState: state => - state.knownSources.foreach(sendSource) - case DAPRequest(seq, "disconnect", req) => - // println(s"disconnect req: $req") - sendResp( - requestSeq = seq, - command = "disconnect", - ) - sock.close() - case DAPRequest(seq, "threads", req) => - // println(s"threads req: $req") - sendResp( - requestSeq = seq, - command = "threads", - body = ujson.Obj( - "threads" -> ujson.Arr( - ujson.Obj( - "id" -> 1, - "name" -> "singleton", - "threadId" -> 1, - ), - ), - ), - ) - case DAPRequest(seq, "pause", req) => - // println(s"pause req: $req") - val wasPaused = - io.withState: state => - val wasPaused = state.evalState match - case EvalState.Paused => true - case _ => - state.evalState = EvalState.Pausing - false - - wasPaused - sendResp( - requestSeq = seq, - command = "pause", - ) - if wasPaused - then - sendEvent( - event = "stopped", - body = ujson.Obj( - "reason" -> "pause", - "description" -> "Paused", - "threadId" -> 1, - ), - ) - case DAPRequest(seq, "stackTrace", req) => - // println(s"stackTrace req: $req") - withState: state => - state.evalState match - case EvalState.Paused => - val frames = state.stackTrace.zipWithIndex - .map: (rec, idx) => - ujson.Obj( - "id" -> (state.frameIdCounter + idx), - "name" -> rec.source.fileName, - "path" -> rec.source.fileName, - "line" -> rec.line, - "column" -> 0, - "origin" -> "manip", - "source" -> rec.source.asJSON, - ) - - sendResp( - requestSeq = seq, - command = "stackTrace", - body = ujson.Obj( - "stackFrames" -> frames.reverseIterator, - "totalFrames" -> frames.size, - ), - ) - case _ => // TODO: error or something - case DAPRequest(seq, "continue", req) => - // println(s"continue req: $req") - io.withState: state => - state.evalState = EvalState.Running - sendResp( - requestSeq = seq, - command = "continue", - ) - case DAPRequest(seq, "next", req) => - // println(s"next req: $req") - io.withState: state => - state.evalState = EvalState.Stepping - sendResp( - requestSeq = seq, - command = "next", - ) - case DAPRequest(seq, "stepIn", req) => - io.withState: state => - state.evalState = EvalState.ContinueNextRewrite - sendResp( - requestSeq = seq, - command = "stepIn", - ) - case DAPRequest(seq, "scopes", req) => - // println(s"scopes req: $req") - val frameId = req("frameId").num.toInt - io.withState: state => - // val node = state.getNodeByRef(frameId) - state.currHandle.nn match - case None => - case Some(Handle.AtTop(top)) => - sendResp( - requestSeq = seq, - command = "scopes", - body = ujson.Obj( - "scopes" -> ujson.Arr( - ujson.Obj( - "name" -> "Tree (at top)", - "variablesReference" -> state.refOf(top), - "namedVariables" -> top.children.size, - ), - ), - ), - ) - case Some(Handle.AtChild(parent, idx, child)) => - val childName = child match - case node: Node => node.token.name - case embed: Node.Embed[?] => s"embed ${embed.meta.tag}" - sendResp( - requestSeq = seq, - command = "scopes", - body = ujson.Obj( - "scopes" -> ujson.Arr( - ujson.Obj( - "name" -> s"Tree (at $childName)", - "variablesReference" -> state.refOf(child), - "namedVariables" -> (child match - case parent: Node.Parent => parent.children.size + 1 - case _ => 1), - ), - ), - ), - ) - case Some(Handle.Sentinel(parent, idx)) => - val parentName = parent match - case top: Node.Top => "top" - case node: Node => node.token.name - sendResp( - requestSeq = seq, - command = "scopes", - body = ujson.Obj( - "scopes" -> ujson.Arr( - ujson.Obj( - "name" -> s"Tree (past-the-end $idx of $parentName)", - "variablesReference" -> state.refOf(parent), - "namedVariables" -> 1, - ), - ), - ), - ) - case DAPRequest(seq, "variables", req) => - // println(s"variables req: $req") - val ref = req("variablesReference").num.toInt - io.withState: state => - def variableOf(node: Node.All, idxInParent: Int): ujson.Obj = - node match - case node: Node => - val pfx = - if idxInParent == -1 - then s"[${node.idxInParent}]" - else s"parent [$idxInParent]" - ujson.Obj( - "name" -> s"$pfx ${node.token.name}", - "value" -> node.sourceRange.decodeString(), - "variablesReference" -> state.refOf(node), - "namedVariables" -> (node.children.size + 1), - ) - case embed: Node.Embed[?] => - val pfx = - if idxInParent == -1 - then s"[${node.idxInParent}]" - else s"parent [$idxInParent]" - ujson.Obj( - "name" -> s"$pfx ${embed.meta.tag}", - "value" -> embed.value.toString(), - "variablesReference" -> state.refOf(node), - "namedVariables" -> 1, - ) - case top: Node.Top => - ujson.Obj( - "name" -> (if idxInParent == -1 then "top" - else s"parent [$idxInParent] top"), - "value" -> "", - "variablesReference" -> state.refOf(top), - "namedVariables" -> top.children.size, - ) - - def variablesOf(node: Node.All): ujson.Arr = - node match - case node: Node => - ujson.Arr.from( - node.parent.map(variableOf(_, node.idxInParent)).iterator - ++ node.children.iterator.map(variableOf(_, -1)), - ) - case embed: Node.Embed[?] => - embed.parent.map(variableOf(_, embed.idxInParent)) - case top: Node.Top => - top.children.iterator.map(variableOf(_, -1)) - - state.getNodeByRef(ref) match - case None => - case Some(node: Node.All) => - sendResp( - requestSeq = seq, - command = "variables", - body = ujson.Obj( - "variables" -> variablesOf(node), - ), - ) - case _ => - println(s"unsupported DAP request ${msg.render()}") - - def close(): Unit = - sendEvent( - event = "terminated", - ) - sock.close() - thread.join() - - private var connectionOpt: Option[Connection] = None - - private val _state = State() - - def withState[T](fn: State => T): T = - io.synchronized: - val result = fn(_state) - io.notifyAll() - result - - def awaitStateUpdate(): Unit = - io.synchronized(io.wait()) - - private final class ConnectionAccepter extends Runnable, Closeable: - private val serverChannel = - ServerSocketChannel - .open() - .setOption(java.net.StandardSocketOptions.SO_REUSEADDR, true) - .bind(InetSocketAddress(host, port)) - private val thread = Thread(this, "debug-adapter-listener") - thread.start() - - def run(): Unit = - println(s"listening for DAP on ${serverChannel.getLocalAddress()}") - while true - do - try - val sock = serverChannel.accept() - println(s"received connection from ${sock.getRemoteAddress()}") - io.synchronized: - connectionOpt match - case None => - connectionOpt = Some(Connection(sock)) - case Some(connection) => - connectionOpt = None - connection.close() - sock.close() - catch - case _: (ClosedChannelException | AsynchronousCloseException) => - // means we are closing - return - - def close(): Unit = - serverChannel.close() - thread.join() - - private val connectionAccepter = ConnectionAccepter() - - def close(): Unit = - io.synchronized(connectionAccepter.close()) - io.synchronized: - connectionOpt match - case None => - case Some(connection) => - connection.close() - connectionOpt = None diff --git a/src/manip/Handle.scala b/src/manip/Handle.scala deleted file mode 100644 index cd7835c..0000000 --- a/src/manip/Handle.scala +++ /dev/null @@ -1,166 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.manip - -import forja.* -import forja.dsl.* -import forja.util.toShortString - -enum Handle: - assertCoherence() - - case AtTop(top: Node.Top) - case AtChild(parent: Node.Parent, idx: Int, child: Node.Child) - case Sentinel(parent: Node.Parent, idx: Int) - - def treeDescr: String = - this match - case Handle.AtTop(top) => s"top $top" - case Handle.AtChild(parent, idx, child) => - if parent.children.lift(idx).exists(_ eq child) - then s"child $idx of $parent" - else s"!!mismatch child $idx of $parent\n!!and $child" - case Handle.Sentinel(parent, idx) => - s"end [idx=$idx] of $parent" - - def assertCoherence(): Unit = - this match - case AtTop(_) => - case AtChild(parent, idx, child) => - if parent.children.isDefinedAt(idx) && (parent.children(idx) eq child) - then () // ok - else - throw NodeError( - s"mismatch: idx $idx in ${parent.toShortString()} and ptr -> ${child.toShortString()}", - ) - case Sentinel(parent, idx) => - if parent.children.length != idx - then - throw NodeError( - s"mismatch: sentinel at $idx does not point to end ${parent.children.length} of ${parent.toShortString()}", - ) - - def rightSibling: Option[Handle] = - assertCoherence() - this match - case AtTop(_) => None - case AtChild(parent, idx, _) => - Handle.idxIntoParent(parent, idx + 1) - case Sentinel(_, _) => None - - def leftSibling: Option[Handle] = - assertCoherence() - this match - case AtTop(_) => None - case AtChild(parent, idx, _) => - Handle.idxIntoParent(parent, idx - 1) - case Sentinel(_, _) => None - - def findFirstChild: Option[Handle] = - assertCoherence() - this match - case AtTop(top) => Handle.idxIntoParent(top, 0) - case AtChild(_, _, child) => - child match - case child: Node.Parent => Handle.idxIntoParent(child, 0) - case _: Node.Embed[?] => None - case Sentinel(_, _) => None - - def findLastChild: Option[Handle] = - assertCoherence() - def forParent(parent: Node.Parent): Option[Handle] = - parent.children.indices.lastOption - .flatMap(Handle.idxIntoParent(parent, _)) - - this match - case AtTop(top) => forParent(top) - case AtChild(_, _, child) => - child match - case child: Node.Parent => forParent(child) - case _: Node.Embed[?] => None - case Sentinel(_, _) => None - - def findParent: Option[Handle] = - assertCoherence() - this match - case AtTop(_) => None - case AtChild(parent, _, _) => Some(Handle.fromNode(parent)) - case Sentinel(parent, _) => Some(Handle.fromNode(parent)) - - def atIdx(idx: Int): Option[Handle] = - assertCoherence() - this match - case AtTop(_) => None - case AtChild(parent, _, _) => Handle.idxIntoParent(parent, idx) - case Sentinel(parent, _) => Handle.idxIntoParent(parent, idx) - - def atIdxFromRight(idx: Int): Option[Handle] = - assertCoherence() - this match - case AtTop(_) => None - case handle: (Sentinel | AtChild) => - val parent = handle.parent - Handle.idxIntoParent(parent, parent.children.size - 1 - idx) - - def keepIdx: Option[Handle] = - this match - case AtTop(_) => Some(this) - case AtChild(parent, idx, _) => Handle.idxIntoParent(parent, idx) - case Sentinel(parent, idx) => Handle.idxIntoParent(parent, idx) - - def keepPtr: Handle = - this match - case AtTop(_) => this - case AtChild(_, _, child) => Handle.fromNode(child) - case Sentinel(parent, _) => - Handle.idxIntoParent(parent, parent.children.length).get - - def findTop: Option[Node.Top] = - def forParent(parent: Node.Parent): Option[Node.Top] = - val p = parent - inline given DebugInfo = DebugInfo.notPoison - p.inspect: - on(theTop).value - | atAncestor(on(theTop).value) - this match - case AtTop(top) => Some(top) - case AtChild(parent, _, _) => forParent(parent) - case Sentinel(parent, _) => forParent(parent) - -object Handle: - object ref extends Manip.Ref[Handle] - - def fromNode(node: Node.All): Handle = - node match - case top: Node.Top => Handle.AtTop(top) - case child: Node.Child => - require(child.parent.nonEmpty, "node must have parent") - Handle.AtChild(child.parent.get, child.idxInParent, child) - - private def idxIntoParent(parent: Node.Parent, idx: Int): Option[Handle] = - if parent.children.isDefinedAt(idx) - then Some(AtChild(parent, idx, parent.children(idx))) - else if idx == parent.children.length - then Some(Sentinel(parent, idx)) - else None - - extension (handle: Handle.Sentinel | Handle.AtChild) - def idx: Int = handle match - case Sentinel(_, idx) => idx - case AtChild(_, idx, _) => idx - - def parent: Node.Parent = handle match - case Sentinel(parent, _) => parent - case AtChild(parent, _, _) => parent diff --git a/src/manip/Manip.FuzzCompare.test.scala b/src/manip/Manip.FuzzCompare.test.scala deleted file mode 100644 index 47be49b..0000000 --- a/src/manip/Manip.FuzzCompare.test.scala +++ /dev/null @@ -1,323 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.manip - -import forja.* -import forja.dsl.* -import forja.util.FuzzTestSuite - -import com.pholser.junit.quickcheck.From -import com.pholser.junit.quickcheck.generator.{ - GenerationStatus, - Generator, - InRange, -} -import com.pholser.junit.quickcheck.random.SourceOfRandomness -import edu.berkeley.cs.jqf.fuzz.{Fuzz, JQF} -import org.junit.runner.RunWith -import scala.util.Using - -@RunWith(classOf[JQF]) -final class ManipFuzzCompareTests extends FuzzTestSuite: - import ManipFuzzCompareTests.* - // fuzzTestMethod(fuzzManipPerform) - - import org.junit.Assume - @Fuzz - def fuzzManipPerform(@From(classOf[ManipGenerator]) manip: Manip[?]): Unit = - Using.resource(ReferenceTracer(manip)): tracer => - instrumentWithTracer(tracer): - try manip.perform() - catch - case bt: Manip.UnrecoveredBacktrackException => - // these are fine but kinda trivial - Assume.assumeTrue(false) - -object ManipFuzzCompareTests: - final class ManipGenerator extends Generator[Manip[Any]](classOf[Manip[Any]]): - private var min = 1 - private var max = 20 - - def configure(range: InRange): Unit = - if range.min().nonEmpty - then min = range.min().toInt - if range.max().nonEmpty - then min = range.max().toInt - - def generate( - random: SourceOfRandomness, - status: GenerationStatus, - ): Manip[Any] = - require(min >= 0 && max >= min, s"failed 0 <= $min <= $max") - val treeSize = random.nextInt(min, max) - ManipGenerator.generateAny(treeSize)(using ManipGenerator.Ctx(random)) - - object ManipGenerator: - import scala.compiletime.summonAll - import scala.deriving.Mirror - import Manip.* - - object SimRefs: - object ref1 extends Ref[Any] - object ref2 extends Ref[Any] - object ref3 extends Ref[Any] - val refs = Array(ref1, ref2, ref3) - - def pickOne(using ctx: Ctx): Ref[Any] = - refs(ctx.random.nextInt(refs.length)) - - final class Ctx( - val random: SourceOfRandomness, - val requireFn: Boolean = false, - ): - def withRequireFn: Ctx = - Ctx(random, requireFn = true) - - def withoutRequireFn: Ctx = - Ctx(random, requireFn = false) - - def getValue(): Any = - val num = random.nextInt() - def fn: Any => Any = { - case n: Int => num + n - case fn: (? => ?) => fn - case _: Tracer => num - case _: RefMap => num - } - if requireFn then fn - else - random.nextBoolean() match - case true => num - case false => fn - - sealed abstract class ManipGenImpl[T]: - def shouldSkip = false - def fixedResultType = false - val minTreeSize: Int - def generate(treeSize: Int)(using ctx: Ctx): T - - def generateAny(treeSize: Int)(using ctx: Ctx): Manip[Any] = - require(treeSize >= 0, s"tree size $treeSize became negative") - var generators = - if treeSize >= generatorsByMinTreeSize.length - then generatorsByMinTreeSize.last - else generatorsByMinTreeSize(treeSize) - if ctx.requireFn - then generators = generators.filterNot(_.fixedResultType) - - val genIdx = ctx.random.nextInt(generators.length) - generators(genIdx).generate(treeSize) - - private def splitSubTreeSize(subTreeSize: Int)(using ctx: Ctx): (Int, Int) = - val lSize = ctx.random.nextInt(1, subTreeSize - 1) - val rSize = subTreeSize - lSize - assert(lSize >= 1) - assert(rSize >= 1) - (lSize, rSize) - - given ManipGenImpl[Backtrack] with - val minTreeSize = 1 - def generate(treeSize: Int)(using ctx: Ctx): Backtrack = - Backtrack(DebugInfo()) - - given ManipGenImpl[Pure[Any]] with - val minTreeSize = 1 - def generate(treeSize: Int)(using ctx: Ctx): Pure[Any] = - val v = ctx.getValue() - Pure(v) - - given ManipGenImpl[Ap[Any, Any]] with - val minTreeSize = 3 - def generate(treeSize: Int)(using ctx: Ctx): Ap[Any, Any] = - val (subTreeSizeL, subTreeSizeR) = splitSubTreeSize(treeSize - 1) - val left = generateAny(subTreeSizeL)(using ctx.withRequireFn) - Ap(left.asInstanceOf[Manip[Any => Any]], generateAny(subTreeSizeR)) - - given ManipGenImpl[MapOpt[Any, Any]] with - val minTreeSize = 2 - def generate(treeSize: Int)(using ctx: Ctx): MapOpt[Any, Any] = - val v = ctx.getValue() - MapOpt(generateAny(treeSize - 1), _ => v) - - given ManipGenImpl[FlatMap[Any, Any]] with - val minTreeSize = 3 - def generate(treeSize: Int)(using ctx: Ctx): FlatMap[Any, Any] = - val (subTreeSizeL, subTreeSizeR) = splitSubTreeSize(treeSize - 1) - val result1 = generateAny(subTreeSizeR) - val result2 = generateAny(subTreeSizeR) - FlatMap( - generateAny(subTreeSizeL)(using ctx.withoutRequireFn), - { - case _: Int => result1 - case _: (? => ?) => result2 - case _: RefMap => result1 - case _: Tracer => result2 - }, - ) - - given ManipGenImpl[Restrict[Any, Any]] with - val minTreeSize = 2 - def generate(treeSize: Int)(using ctx: Ctx): Restrict[Any, Any] = - val max = ctx.random.nextInt() - val keepFn = ctx.random.nextBoolean() - Restrict( - generateAny(treeSize - 1), - { - case num: Int if num <= max => num - case fn: (? => ?) if keepFn => fn - case tracer: Tracer if keepFn => tracer - case refMap: RefMap if keepFn => refMap - }, - DebugInfo(), - ) - - given ManipGenImpl[Effect[Any]] with - val minTreeSize = 1 - def generate(treeSize: Int)(using ctx: Ctx): Effect[Any] = - val v1 = ctx.getValue() - if ctx.random.nextBoolean() - then Effect(() => v1) - else - val v2 = ctx.getValue() - var alt = false - Effect: () => - alt = !alt - if alt then v1 else v2 - - given ManipGenImpl[Finally[Any]] with - val minTreeSize = 2 - def generate(treeSize: Int)(using ctx: Ctx): Finally[Any] = - Finally( - generateAny(treeSize - 1), - () => (), - ) - - given ManipGenImpl[KeepLeft[Any]] with - val minTreeSize = 3 - def generate(treeSize: Int)(using ctx: Ctx): KeepLeft[Any] = - val (subTreeSizeL, subTreeSizeR) = splitSubTreeSize(treeSize - 1) - KeepLeft( - generateAny(subTreeSizeL), - generateAny(subTreeSizeR)(using ctx.withoutRequireFn), - ) - - given ManipGenImpl[KeepRight[Any]] with - val minTreeSize = 3 - def generate(treeSize: Int)(using ctx: Ctx): KeepRight[Any] = - val (subTreeSizeL, subTreeSizeR) = splitSubTreeSize(treeSize - 1) - KeepRight( - generateAny(subTreeSizeL)(using ctx.withoutRequireFn), - generateAny(subTreeSizeR), - ) - - given ManipGenImpl[Commit[Any]] with - val minTreeSize = 2 - def generate(treeSize: Int)(using ctx: Ctx): Commit[Any] = - Commit(generateAny(treeSize - 1), DebugInfo()) - - given ManipGenImpl[RefInit[Any, Any]] with - val minTreeSize = 2 - def generate(treeSize: Int)(using ctx: Ctx): Manip.RefInit[Any, Any] = - val v = ctx.getValue() - RefInit( - SimRefs.pickOne, - () => v, - generateAny(treeSize - 1), - DebugInfo(), - ) - - given ManipGenImpl[RefGet[Any]] with - // not technically true, but because we can't prove we stored a fn, - // assume we don't properly control getting it back - override val fixedResultType = true - val minTreeSize = 1 - def generate(treeSize: Int)(using ctx: Ctx): RefGet[Any] = - require(!ctx.requireFn) - RefGet(SimRefs.pickOne, DebugInfo()) - - given ManipGenImpl[RefReset[Any, Any]] with - val minTreeSize = 2 - def generate(treeSize: Int)(using ctx: Ctx): RefReset[Any, Any] = - RefReset(SimRefs.pickOne, generateAny(treeSize - 1), DebugInfo()) - - given ManipGenImpl[RefUpdated[Any, Any]] with - val minTreeSize = 2 - def generate(treeSize: Int)(using ctx: Ctx): RefUpdated[Any, Any] = - val v = ctx.getValue() - RefUpdated( - SimRefs.pickOne, - (_ => v), - generateAny(treeSize - 1), - DebugInfo(), - ) - - given ManipGenImpl[GetTracer.type] with - override val fixedResultType = true - val minTreeSize = 1 - def generate(treeSize: Int)(using ctx: Ctx): GetTracer.type = - require(!ctx.requireFn) - GetTracer - - given ManipGenImpl[Disjunction[Any]] with - val minTreeSize = 3 - def generate(treeSize: Int)(using ctx: Ctx): Disjunction[Any] = - val (subTreeSizeL, subTreeSizeR) = splitSubTreeSize(treeSize - 1) - Disjunction( - generateAny(subTreeSizeL), - generateAny(subTreeSizeR), - DebugInfo(), - ) - - given ManipGenImpl[Deferred[Any]] with - val minTreeSize = 2 - def generate(treeSize: Int)(using ctx: Ctx): Deferred[Any] = - val deferred = generateAny(treeSize - 1) - Deferred(() => deferred) - - given ManipGenImpl[TapEffect[Any]] with - val minTreeSize = 2 - def generate(treeSize: Int)(using ctx: Ctx): TapEffect[Any] = ??? - - given ManipGenImpl[RestrictHandle[Any]] with - // explicit handle manipulation, simulating that is TODO - /* (idea: keep a couple of token handles, allow RefInit to set it, and - * then update it here) */ - override val shouldSkip = true - val minTreeSize = 2 - def generate(treeSize: Int)(using ctx: Ctx): RestrictHandle[Any] = ??? - - private inline def findGenOptions(using - mirror: Mirror.Of[Manip[Any]], - ): Array[ManipGenImpl[Manip[Any]]] = - summonAll[Tuple.Map[mirror.MirroredElemTypes, ManipGenImpl]].toArray - .map(_.asInstanceOf[ManipGenImpl[Manip[Any]]]) - - val generatorsByMinTreeSize: Array[Array[ManipGenImpl[Manip[Any]]]] = - locally: - val generators = findGenOptions - val builder = Array.newBuilder[Array[ManipGenImpl[Manip[Any]]]] - var lastAddition = generators.filter(_.minTreeSize <= 1) - builder += lastAddition - var minTreeSize = 1 - var proposedAddition = generators.filter(_.minTreeSize <= minTreeSize) - while proposedAddition.length > lastAddition.length do - builder += proposedAddition - minTreeSize += 1 - lastAddition = proposedAddition - proposedAddition = generators.filter(_.minTreeSize <= minTreeSize) - builder.result() - end generatorsByMinTreeSize - end ManipGenerator -end ManipFuzzCompareTests diff --git a/src/manip/Manip.scala b/src/manip/Manip.scala deleted file mode 100644 index 183bbb7..0000000 --- a/src/manip/Manip.scala +++ /dev/null @@ -1,606 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.manip - -import cats.syntax.all.given - -import forja.* -import forja.dsl.* -import forja.util.SymbolicMapFactory - -import scala.reflect.ClassTag -import scala.util.NotGiven - -/** The core DSL class, whose [[forja.manip.Manip.perform]] method causes the - * represented instructions to run. It has instances of [[cats]] typeclasses, - * and therefore inherits some subset of syntax from that library: - * - [[forja.manip.Manip.applicative]] allows applicative functor - * composition, such as - * - `Manip.pure(41).map(_ + 1)` mapping the result of one op - * - Keep right `Manip.unit *> Manip.pure("keep this side")`` - * - Keep left `Manip.pure("keep this side") <* Manip.unit` - * - Tuple of ops into one op that gives tuple of results - * `(Manip.pure("elem 1"), Manip.pure("elem 2")).tupled` - * - `(Manip.pure(2), Manip.pure(2)).mapN(_ + _)` similar to above, but - * directly applying a function to results - * - [[forja.manip.Manip.monoidK]] enables cats utilities that rely on - * "choice" and "empty" operations - * - * The specific subclasses are an implementation detail. See [[forja.dsl]] for - * instructions on how to construct instances of this class. - */ -sealed abstract class Manip[+T]: - private inline given DebugInfo = DebugInfo.poison - - private[forja] def isBacktrack: Boolean = false - - private[manip] def performImpl(state: Manip.PerformState): Manip.PerformResult - - final def perform(using DebugInfo)(): T = - def impl: T = - val state = Manip.PerformState(summon[DebugInfo]) - - var performResult: Manip.PerformResult = this - while performResult ne null do - assert( - state.result == null, - s"result ${state.result} should have been null", - ) - performResult = performResult.performImpl(state) - if performResult eq null - then performResult = state.popNextResult() - end while - - state.result.asInstanceOf[T] - end impl - - if Manip.useReferenceTracer - then instrumentWithTracerReentrant(ReferenceTracer(this))(impl) - else impl -end Manip - -object Manip: - private inline given poison(using NotGiven[DebugInfo.Ctx]): DebugInfo = - DebugInfo.poison - - final case class UnrecoveredBacktrackException( - rootInfo: DebugInfo, - debugInfo: DebugInfo, - ) extends RuntimeException( - s"unrecovered backtrack caught in ($rootInfo), initiated at $debugInfo", - ) - - private val useReferenceTracer = - val propName = "distcompiler.Manip.useReferenceTracer" - System.getProperty(propName) match - case null => false - case "yes" => true - case "no" => false - case idk => - throw RuntimeException(s"invalid property value \"$idk\" for $propName") - private[manip] var tracerVar: ThreadLocal[Tracer] = ThreadLocal() - - private[manip] final class PerformState(debugInfo: DebugInfo): - var result: Any | Null = null - val refMap = RefMap.empty - refMap(PerformState.RootDebugInfoRef) = debugInfo - val committedStack = SegmentedStack[PerformState.StackRec]() - val speculateStack = SegmentedStack[PerformState.StackRec]() - - def rootDebugInfo: DebugInfo = - refMap(PerformState.RootDebugInfoRef) - - private val safeMap = RefMapFactory.Map.empty[Int](-1) - private var leftDepth = 0 - private var committedDepth = 0 - def startCommit(debugInfo: DebugInfo): Unit = - pushSave(PerformState.RootDebugInfoRef, rootDebugInfo) - refMap(PerformState.RootDebugInfoRef) = debugInfo - if committedDepth <= leftDepth - then leftDepth = committedDepth - def finishCommit(): Unit = - committedDepth = leftDepth - def leftInc(): Unit = - leftDepth += 1 - def leftDec(): Unit = - leftDepth -= 1 - assert(leftDepth >= 0) - def pushSave[T]( - ref: Ref[T], - value: T | Null, - oldSavedDepth: Int = -2, - isCommit: Boolean = false, - ): Unit = - /* If we have an old depth that "predates" what's in safeMap, restore to - * that. This will catch the first RestoreRef during commit, if the - * safeMap entry has advanced since then. - * There's a theorem here about how we cannot speculate with leftDepth - * less than what we've committed. */ - if oldSavedDepth != -2 && oldSavedDepth < safeMap(ref) - then safeMap(ref) = oldSavedDepth - - val savedDepth = safeMap(ref) - assert(savedDepth <= leftDepth) - if savedDepth < leftDepth - then - val rec = PerformState.RestoreRef(ref, value, savedDepth) - if isCommit - then committedStack.push(rec) - else speculateStack.push(rec) - safeMap(ref) = leftDepth - def pushSaveDel[T](ref: Ref[T]): Unit = - pushSave(ref, null) - - val tracer: Tracer = tracerVar.get() match - case null => NopTracer - case tracer => tracer - - def backtrack(self: Manip[?], posInfo: DebugInfo): PerformResult = - tracer.onBacktrack(self, posInfo) - result = null.asInstanceOf - var backtrackResult: PerformResult = null - while backtrackResult eq null do - speculateStack.pop() match - case null => - throw UnrecoveredBacktrackException(rootDebugInfo, posInfo) - case rec => backtrackResult = rec.performBacktrack(this, posInfo) - backtrackResult - - def popNextResult(): PerformResult = - var popResult: PerformResult = null - while popResult eq null do - var popped = speculateStack.pop() - if popped `eq` null - then popped = committedStack.pop() - - popped match - case null => return null - case elem => - popResult = elem.performPop(this) - popResult - end PerformState - - private[manip] object PerformState: - private object RootDebugInfoRef extends Manip.Ref[DebugInfo] - - sealed trait StackRec: - private[manip] def performPop( - state: Manip.PerformState, - ): Manip.PerformResult - private[manip] def performBacktrack( - state: Manip.PerformState, - posInfo: DebugInfo, - ): Manip.PerformResult - private[manip] def performCommit(state: Manip.PerformState): Unit - - sealed trait NopBacktrack extends StackRec: - def performBacktrack( - state: PerformState, - posInfo: DebugInfo, - ): PerformResult = null - - sealed trait PopPop extends StackRec: - def performPop( - state: Manip.PerformState, - ): Manip.PerformResult = null - - sealed trait PushCommit extends StackRec: - private[manip] def performCommit(state: PerformState): Unit = - state.committedStack.push(this) - - final case class RestoreRef[T](ref: Ref[T], value: T | Null, leftDepth: Int) - extends StackRec: - private def restore(state: PerformState): Unit = - state.safeMap(ref) = leftDepth - if value == null - then state.refMap.remove(ref) - else state.refMap(ref) = value.nn - def performBacktrack( - state: PerformState, - posInfo: DebugInfo, - ): PerformResult = - restore(state) - null - def performPop(state: PerformState): PerformResult = - restore(state) - null - override def performCommit(state: PerformState): Unit = - state.pushSave(ref, value, oldSavedDepth = leftDepth, isCommit = true) - - final class MapFn[T, U](fn: T => U) extends NopBacktrack, PushCommit: - def performPop(state: PerformState): PerformResult = - state.result = fn(state.result.asInstanceOf[T]) - null - - import PerformState.* - - private[manip] type PerformResult = Manip[?] | Null - - final case class Backtrack(debugInfo: DebugInfo) extends Manip[Nothing]: - private[forja] override def isBacktrack: Boolean = true - private[manip] def performImpl(state: PerformState): PerformResult = - state.backtrack(this, debugInfo) - - final case class Pure[T](value: T) extends Manip[T]: - def performImpl(state: PerformState): PerformResult = - state.result = value - null - - final case class Ap[T, U](ff: Manip[T => U], fa: Manip[T]) - extends Manip[U], - StackRec: - def performImpl(state: PerformState): PerformResult = - state.leftInc() - state.speculateStack.push(this) - ff - def performPop(state: PerformState): PerformResult = - state.leftDec() - state.speculateStack.push( - PerformState.MapFn(state.result.asInstanceOf[T => U]), - ) - state.result = null - fa - def performBacktrack( - state: PerformState, - posInfo: DebugInfo, - ): PerformResult = - state.leftDec() - null - def performCommit(state: PerformState): Unit = - state.leftInc() - state.committedStack.push(this) - - final case class MapOpt[T, U](manip: Manip[T], fn: T => U) - extends Manip[U], - NopBacktrack, - PushCommit: - def performImpl(state: PerformState): PerformResult = - state.speculateStack.push(this) - manip - def performPop(state: PerformState): PerformResult = - state.result = fn(state.result.asInstanceOf) - null - - final case class FlatMap[T, U](manip: Manip[T], fn: T => Manip[U]) - extends Manip[U], - StackRec: - def performImpl(state: PerformState): PerformResult = - state.leftInc() - state.speculateStack.push(this) - manip - def performPop(state: PerformState): PerformResult = - state.leftDec() - val next = fn(state.result.asInstanceOf[T]) - state.result = null - next - def performBacktrack( - state: PerformState, - posInfo: DebugInfo, - ): PerformResult = - state.leftDec() - null - def performCommit(state: PerformState): Unit = - state.leftInc() - state.committedStack.push(this) - - final case class Restrict[T, U]( - manip: Manip[T], - restriction: PartialFunction[T, U], - debugInfo: DebugInfo, - ) extends Manip[U], - NopBacktrack, - PushCommit: - def performImpl(state: PerformState): PerformResult = - state.speculateStack.push(this) - manip - def performPop(state: PerformState): PerformResult = - val value = state.result.asInstanceOf[T] - value match - case restriction(result) => - state.result = result - null - case _ => - state.backtrack(this, debugInfo) - - final case class Effect[T](fn: () => T) extends Manip[T]: - def performImpl(state: PerformState): PerformResult = - state.result = fn() - null - - final case class Finally[T](manip: Manip[T], fn: () => Unit) - extends Manip[T], - StackRec, - PushCommit: - def performImpl(state: PerformState): PerformResult = - state.speculateStack.push(this) - manip - def performPop(state: PerformState): PerformResult = - fn() - null - def performBacktrack( - state: PerformState, - posInfo: DebugInfo, - ): PerformResult = - fn() - null - - final case class KeepLeft[T](left: Manip[T], right: Manip[?]) - extends Manip[T], - StackRec: - def performImpl(state: PerformState): PerformResult = - state.leftInc() - state.speculateStack.push(this) - left - def performPop(state: PerformState): PerformResult = - state.leftDec() - val storedResult = state.result - state.speculateStack.push(PerformState.MapFn(_ => storedResult)) - state.result = null - right - def performBacktrack( - state: PerformState, - posInfo: DebugInfo, - ): PerformResult = - state.leftDec() - null - def performCommit(state: PerformState): Unit = - state.leftInc() - state.committedStack.push(this) - - final case class KeepRight[T](left: Manip[?], right: Manip[T]) - extends Manip[T], - StackRec: - def performImpl(state: PerformState): PerformResult = - state.leftInc() - state.speculateStack.push(this) - left - def performPop(state: PerformState): PerformResult = - state.leftDec() - state.result = null - right - def performBacktrack( - state: PerformState, - posInfo: DebugInfo, - ): PerformResult = - state.leftDec() - null - def performCommit(state: PerformState): Unit = - state.leftInc() - state.committedStack.push(this) - - final case class Commit[T](manip: Manip[T], debugInfo: DebugInfo) - extends Manip[T]: - def performImpl(state: PerformState): PerformResult = - state.tracer.onCommit(this, debugInfo) - state.startCommit(debugInfo) - state.speculateStack.consumeInReverse(_.performCommit(state)) - state.finishCommit() - manip - - final case class RefInit[T, U]( - ref: Manip.Ref[T], - initFn: () => T, - manip: Manip[U], - debugInfo: DebugInfo, - ) extends Manip[U]: - def performImpl(state: PerformState): PerformResult = - state.refMap.get(ref) match - case Some(_) => - state.backtrack(this, debugInfo) - case None => - val initValue = initFn() - state.tracer.onAssign(this, ref, initValue) - state.refMap(ref) = initValue - state.pushSaveDel(ref) - manip - - final case class RefReset[T, U]( - ref: Manip.Ref[T], - manip: Manip[U], - debugInfo: DebugInfo, - ) extends Manip[U]: - def performImpl(state: PerformState): PerformResult = - state.refMap.get(ref) match - case None => - case Some(value) => - state.pushSave(ref, value) - state.tracer.onDel(this, ref, debugInfo) - state.refMap.remove(ref) - - manip - - final case class RefGet[T](ref: Manip.Ref[T], debugInfo: DebugInfo) - extends Manip[T]: - def performImpl(state: PerformState): PerformResult = - state.refMap.get(ref) match - case None => - state.backtrack(this, debugInfo) - case Some(value) => - state.tracer.onRead(this, ref, value, debugInfo) - state.result = value - null - - final case class RefUpdated[T, U]( - ref: Manip.Ref[T], - fn: T => T, - manip: Manip[U], - debugInfo: DebugInfo, - ) extends Manip[U]: - def performImpl(state: PerformState): PerformResult = - state.refMap.get(ref) match - case None => state.backtrack(this, debugInfo) - case Some(value) => - state.tracer.onRead(this, ref, value, debugInfo) - state.pushSave(ref, value) - val newValue = fn(value) - state.tracer.onAssign(this, ref, newValue) - state.refMap(ref) = newValue - manip - - case object GetTracer extends Manip[Tracer]: - def performImpl(state: PerformState): PerformResult = - state.result = state.tracer - null - - final case class Disjunction[T]( - first: Manip[T], - second: Manip[T], - debugInfo: DebugInfo, - ) extends Manip[T], - StackRec: - def performImpl(state: PerformState): PerformResult = - state.leftInc() - state.tracer.onBranch(this, debugInfo) - state.speculateStack.push(this) - first - def performPop(state: PerformState): PerformResult = - state.leftDec() - null - def performBacktrack( - state: PerformState, - posInfo: DebugInfo, - ): PerformResult = - state.leftDec() - second - def performCommit(state: PerformState): Unit = - () // drop this, do not leftInc - - final case class Deferred[T](fn: () => Manip[T]) extends Manip[T]: - def performImpl(state: PerformState): PerformResult = fn() - - final case class TapEffect[T](manip: Manip[T], fn: T => Unit) - extends Manip[T], - NopBacktrack, - PushCommit: - def performImpl(state: PerformState): PerformResult = - state.speculateStack.push(this) - manip - def performPop(state: PerformState): PerformResult = - fn(state.result.asInstanceOf[T]) - null - - final case class RestrictHandle[T]( - fn: PartialFunction[Handle, Handle], - manip: Manip[T], - debugInfo: DebugInfo, - ) extends Manip[T]: - def performImpl(state: PerformState): PerformResult = - state.refMap.get(Handle.ref) match - case None => state.backtrack(this, debugInfo) - case Some(oldHandle) => - state.tracer.onRead(this, Handle.ref, oldHandle, debugInfo) - oldHandle match - case fn(handle) => - state.pushSave(Handle.ref, oldHandle) - state.tracer.onAssign(this, Handle.ref, handle) - state.refMap(Handle.ref) = handle - manip - case _ => - state.backtrack(this, debugInfo) - - abstract class Ref[T] extends RefMapFactory.Mapped: - final def init[U](using DebugInfo)(init: => T)(manip: Manip[U]): Manip[U] = - Manip.RefInit(this, () => init, manip, summon[DebugInfo]) - final def reset[U](using DebugInfo)(manip: Manip[U]): Manip[U] = - Manip.RefReset(this, manip, summon[DebugInfo]) - final def get(using DebugInfo): Manip[T] = - Manip.RefGet(this, summon[DebugInfo]) - final def updated[U](using - DebugInfo, - )(fn: T => T)( - manip: Manip[U], - ): Manip[U] = Manip.RefUpdated(this, fn, manip, summon[DebugInfo]) - final def doEffect[U](using DebugInfo)(fn: T => U): Manip[U] = - get.lookahead.flatMap: v => - effect(fn(v)) - - object RefMapFactory extends SymbolicMapFactory - - final class RefMap(private val map: RefMapFactory.Map[Any]) extends AnyVal: - def apply[T](ref: Ref[T]): T = - map(ref).nn.asInstanceOf[T] - - def get[T](ref: Ref[T]): Option[T] = - map(ref) match - case null => None - case value => Some(value.asInstanceOf[T]) - - def updated[T](ref: Ref[T], value: T): RefMap = - RefMap(map.updated(ref, value)) - - def removed[T](ref: Ref[T]): RefMap = - RefMap(map.removed(ref)) - - def update[T](ref: Ref[T], value: T): Unit = - map(ref) = value - - def remove[T](ref: Ref[T]): Unit = - map.remove(ref) - - object RefMap: - def empty: RefMap = RefMap(RefMapFactory.Map.empty(null)) - - type Rules = Manip[RulesResult] - - enum RulesResult: - case Progress, NoProgress - - val unit: Manip[Unit] = ().pure - // export dsl.defer - export applicative.pure - - given applicative: cats.Applicative[Manip] with - override def unit: Manip[Unit] = Manip.unit - override def map[A, B](fa: Manip[A])(f: A => B): Manip[B] = - Manip.MapOpt(fa, f) - def ap[A, B](ff: Manip[A => B])(fa: Manip[A]): Manip[B] = - Manip.Ap(ff, fa) - def pure[A](x: A): Manip[A] = Manip.Pure(x) - override def as[A, B](fa: Manip[A], b: B): Manip[B] = - productR(fa)(pure(b)) - override def productL[A, B](fa: Manip[A])(fb: Manip[B]): Manip[A] = - Manip.KeepLeft(fa, fb) - override def productR[A, B](fa: Manip[A])(fb: Manip[B]): Manip[B] = - Manip.KeepRight(fa, fb) - - given monoidK(using DebugInfo): cats.MonoidK[Manip] with - def empty[A]: Manip[A] = Manip.Backtrack(summon[DebugInfo]) - def combineK[A](x: Manip[A], y: Manip[A]): Manip[A] = - (x, y) match - case (Manip.Backtrack(_), right) => right - case (left, Manip.Backtrack(_)) => left - // case ( - // Manip.Negated(manip1, debugInfo1), - // Manip.Negated(manip2, debugInfo2) - // ) => - // Manip.Negated(combineK(manip1, manip2), debugInfo1 ++ debugInfo2) - case _ => - Manip.Disjunction(x, y, summon[DebugInfo]) - - class Lookahead[T](val manip: Manip[T]) extends AnyVal: - def flatMap[U](fn: T => Manip[U]): Manip[U] = - Manip.FlatMap(manip, fn) - - object Lookahead: - given applicative: cats.Applicative[Lookahead] with - def ap[A, B](ff: Lookahead[A => B])(fa: Lookahead[A]): Lookahead[B] = - Lookahead(ff.manip.ap(fa.manip)) - def pure[A](x: A): Lookahead[A] = - Lookahead(Manip.pure(x)) - given monoidK(using DebugInfo): cats.MonoidK[Lookahead] with - def empty[A]: Lookahead[A] = Lookahead(Manip.monoidK.empty) - def combineK[A](x: Lookahead[A], y: Lookahead[A]): Lookahead[A] = - Lookahead(x.manip.combineK(y.manip)) -end Manip diff --git a/src/manip/Manip.test.scala b/src/manip/Manip.test.scala deleted file mode 100644 index faa6358..0000000 --- a/src/manip/Manip.test.scala +++ /dev/null @@ -1,192 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.manip - -import cats.syntax.all.given - -import forja.* -import forja.dsl.* - -import scala.concurrent.duration.{Duration, MINUTES} - -class ManipTests extends munit.FunSuite: - // or CI will kill us for taking slightly over 30s - override val munitTimeout = Duration(5, MINUTES) - import ManipTests.* - - def repeat(breadth: Int, iterFn: () => Iterator[Node]): Iterator[List[Node]] = - breadth match - case 0 => Iterator.single(Nil) - case _ if breadth > 0 => - for - hd <- iterFn() - tl <- repeat(breadth - 1, iterFn) - yield hd.clone() :: tl - - def exampleNodes(depth: Int, shouldVary: Boolean): Iterator[Node] = - depth match - case 0 => Iterator.empty - case _ if depth > 0 => - for - breadth <- (0 until 3).iterator - parent <- Iterator(tok1(), if shouldVary then tok2() else tok1()) - children <- repeat(breadth, () => exampleNodes(depth - 1, shouldVary)) - yield locally: - val p = parent.clone() - p.children.addAll(children) - p - - def examples(depth: Int, shouldVary: Boolean): Iterator[Node.Top] = - for - breadth <- (0 until 3).iterator - children <- repeat(breadth, () => exampleNodes(depth - 1, shouldVary)) - yield Node.Top(children) - - def treePairs: Iterator[(Node.Top, Node.Top)] = - examples(4, shouldVary = true).zip(examples(4, shouldVary = false)) - - // test sanity condition - test("sanity: all trees are equal to themselves".fail): - treePairs.foreach: (bi, mono) => - assertEquals(bi, mono) - - val unifyColors1 = - pass(once = true) - .rules: - on(tok2).rewrite: node => - spliceThen(tok1(node.unparentedChildren)): - continuePassAtNextNode - - test("treePairs unifyColors1"): - treePairs.foreach: (bi, mono) => - initNode(bi)(unifyColors1).perform() - assertEquals(bi, mono) - - test("unifyColors1 on a single node"): - val bi = Node.Top(tok2()) - val mono = Node.Top(tok1()) - initNode(bi)(unifyColors1).perform() - assertEquals(bi, mono) - - val unifyColors2 = - pass(once = false) - .rules: - on(tok2).rewrite: node => - splice(tok1(node.unparentedChildren)) - - test("treePairs unifyColors2"): - treePairs.foreach: (bi, mono) => - initNode(bi)(unifyColors2).perform() - assertEquals(bi, mono) - - val unifyColors3 = - pass(once = true, strategy = pass.bottomUp) - .rules: - on(tok2).rewrite: node => - splice(tok1(node.unparentedChildren)) - - test("treePairs unifyColors3"): - treePairs.foreach: (bi, mono) => - initNode(bi)(unifyColors3).perform() - assertEquals(bi, mono) - - val unifyColors4 = - pass(once = true, strategy = pass.bottomUp) - .rules: - on(repeated1(tok2)).rewrite: nodes => - splice(nodes.map(node => tok1(node.unparentedChildren))) - - test("treePairs unifyColors4"): - treePairs.foreach: (bi, mono) => - initNode(bi)(unifyColors4).perform() - assertEquals(bi, mono) - - val unifyColors5 = - pass(once = true, strategy = pass.bottomUp) - .rules: - on(nodeSpanMatchedBy(repeated1(tok2).void)).rewrite: span => - splice( - span.map(node => tok1(node.asInstanceOf[Node].unparentedChildren)), - ) - - test("treePairs unifyColors5"): - treePairs.foreach: (bi, mono) => - initNode(bi)(unifyColors5).perform() - assertEquals(bi, mono) - - val unifyColors6 = - pass(once = true, strategy = pass.bottomUp) - .rules: - on(nodeSpanMatchedBy(repeated1(anyChild <* not(tok1)).void)).rewrite: - span => - splice( - span.map(node => tok1(node.asInstanceOf[Node].unparentedChildren)), - ) - - test("treePairs unifyColors6"): - treePairs.foreach: (bi, mono) => - initNode(bi)(unifyColors6).perform() - assertEquals(bi, mono) - - val unifyColors7 = - pass(once = true, strategy = pass.bottomUp) - .rules: - on(nodeSpanMatchedBy(repeated1(anyChild <* not(repeated1(tok1))).void)) - .rewrite: span => - splice( - span.map(node => tok1(node.asInstanceOf[Node].unparentedChildren)), - ) - - test("treePairs unifyColors7"): - treePairs.foreach: (bi, mono) => - initNode(bi)(unifyColors7).perform() - assertEquals(bi, mono) - - val unifyColors8 = - pass(once = true, strategy = pass.bottomUp) - .rules: - on(tok(tok2) *> repeated1(tok2)).rewrite: nodes => - splice(nodes.map(node => tok1(node.unparentedChildren))) - - test("treePairs unifyColors8"): - treePairs.foreach: (bi, mono) => - initNode(bi)(unifyColors8).perform() - assertEquals(bi, mono) - - test("error node is skipped"): - val example = Node.Top: - tok2( - tok2(tok1()), - tok1(Builtin.Error("???", tok2())), - ) - val expected = Node.Top: - tok1( - tok1(tok1()), - tok1(Builtin.Error("???", tok2())), - ) - - // topDown - val result1 = example.clone() - initNode(result1)(unifyColors1).perform() - assertEquals(result1, expected) - - // bottomUp - val result2 = example.clone() - initNode(result2)(unifyColors3).perform() - assertEquals(result2, expected) - -object ManipTests: - object tok1 extends Token - object tok2 extends Token diff --git a/src/manip/ManipOps.scala b/src/manip/ManipOps.scala deleted file mode 100644 index 4433104..0000000 --- a/src/manip/ManipOps.scala +++ /dev/null @@ -1,474 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.manip - -import cats.syntax.all.given - -import forja.* -import forja.dsl.* -import forja.util.toShortString - -import scala.reflect.ClassTag - -import Manip.* - -trait ManipOps: - def instrumentWithTracer[T](tracer: Tracer)(fn: => T): T = - require( - tracerVar.get() eq null, - s"tried to set over existing tracer ${tracerVar.get()}", - ) - tracerVar.set(tracer) - try fn - finally - tracer.close() - assert( - tracerVar.get() eq tracer, - s"tracer $tracer was replaced by ${tracerVar.get()} during execution", - ) - tracerVar.remove() - - def instrumentWithTracerReentrant[T, Tr <: Tracer: ClassTag](tracer: Tr)( - fn: => T, - ): T = - tracerVar.get() match - case null => instrumentWithTracer(tracer)(fn) - case existingTracer: Tr => - tracerVar.remove() - try instrumentWithTracer(tracer)(fn) - finally tracerVar.set(existingTracer) - case existingTracer: Tracer => - throw IllegalArgumentException( - s"re-entrant tracer $tracer is not of the same type as existing tracer $existingTracer", - ) - - def defer[T](manip: => Manip[T]): Manip[T] = - lazy val impl = manip - Manip.Deferred(() => impl) - - def commit[T](using DebugInfo)(manip: Manip[T]): Manip[T] = - Manip.Commit(manip, summon[DebugInfo]) - - def effect[T](fn: => T): Manip[T] = - Manip.Effect(() => fn) - - def backtrack(using DebugInfo): Manip[Nothing] = - Manip.Backtrack(summon[DebugInfo]) - - def initNode[T](using - DebugInfo, - )(node: Node.All)(manip: Manip[T]): Manip[T] = - initHandle(Handle.fromNode(node))(manip) - - def initHandle[T](using - DebugInfo, - )(handle: Handle)(manip: Manip[T]): Manip[T] = - Handle.ref.init(handle)(manip) - - def atNode[T](using DebugInfo)(node: Node.All)(manip: Manip[T]): Manip[T] = - atHandle(Handle.fromNode(node))(manip) - - def atHandle[T](using - DebugInfo, - )(handle: Handle)(manip: Manip[T]): Manip[T] = - restrictHandle(manip)(_ => Some(handle)) - - def getHandle(using DebugInfo): Manip[Handle] = - Handle.ref.get - - def getTracer: Manip[Tracer] = - Manip.GetTracer - - def restrictHandle[T](using - DebugInfo, - )(manip: Manip[T])(fn: PartialFunction[Handle, Handle]): Manip[T] = - Manip.RestrictHandle(fn, manip, summon[DebugInfo]) - - def restrictHandle[T](using - DebugInfo, - )(manip: Manip[T])(fn: Handle => Option[Handle]): Manip[T] = - Manip.RestrictHandle(fn.unlift, manip, summon[DebugInfo]) - - def keepHandleIdx[T](using DebugInfo)(manip: Manip[T]): Manip[T] = - restrictHandle(manip)(_.keepIdx) - - def keepHandlePtr[T](using DebugInfo)(manip: Manip[T]): Manip[T] = - restrictHandle(manip)(PartialFunction.fromFunction(_.keepPtr)) - - def getNode(using DebugInfo.Ctx): Manip[Node.All] = - getHandle - .tapEffect(_.assertCoherence()) - .restrict: - case Handle.AtTop(top) => top - case Handle.AtChild(_, _, child) => child - - def atRightSibling[T](using DebugInfo)(manip: Manip[T]): Manip[T] = - restrictHandle(manip)(_.rightSibling) - - def atLeftSibling[T](using DebugInfo)(manip: Manip[T]): Manip[T] = - restrictHandle(manip)(_.leftSibling) - - def atIdx[T](using DebugInfo)(idx: Int)(manip: Manip[T]): Manip[T] = - restrictHandle(manip)(_.atIdx(idx)) - - def atIdxFromRight[T](using - DebugInfo, - )(idx: Int)(manip: Manip[T]): Manip[T] = - restrictHandle(manip)(_.atIdxFromRight(idx)) - - def atFirstChild[T](using DebugInfo)(manip: Manip[T]): Manip[T] = - restrictHandle(manip)(_.findFirstChild) - - def atLastChild[T](using DebugInfo)(manip: Manip[T]): Manip[T] = - restrictHandle(manip)(_.findLastChild) - - def atFirstSibling[T](using DebugInfo)(manip: Manip[T]): Manip[T] = - atParent(atFirstChild(manip)) - - def atLastSibling[T](using DebugInfo)(manip: Manip[T]): Manip[T] = - atParent(atLastChild(manip)) - - def atParent[T](using DebugInfo)(manip: Manip[T]): Manip[T] = - restrictHandle(manip)(_.findParent) - - def atAncestor[T](using DebugInfo.Ctx)(manip: Manip[T]): Manip[T] = - lazy val impl: Manip[T] = - manip | atParent(defer(impl)) - - atParent(impl) - - def addChild[T](using - DebugInfo.Ctx, - )(child: => Node.Child): Manip[Node.Child] = - getNode.lookahead.flatMap: - case thisParent: Node.Parent => - effect: - val tmp = child - thisParent.children.addOne(tmp) - tmp - case _ => backtrack - - final class on[T](val pattern: SeqPattern[T]): - def raw: Manip[SeqPattern.Result[T]] = - pattern.manip - - def value: Manip[T] = - raw.map(_.value) - - def check: Manip[Unit] = - raw.void - - def rewrite(using - DebugInfo.Ctx, - )( - action: on.Ctx ?=> T => Rules, - ): Rules = - (raw, getHandle, getTracer).tupled - .flatMap: (patResult, handle, tracer) => - handle.assertCoherence() - val value = patResult.value - val (parent, startIdx) = - handle match - case Handle.AtTop(top) => - throw NodeError( - s"tried to rewrite top ${top.toShortString()}", - ) - case Handle.AtChild(parent, idx, _) => (parent, idx) - case Handle.Sentinel(parent, idx) => (parent, idx) - val matchedCount = - patResult match - case SeqPattern.Result.Top(top, _) => - throw NodeError( - s"ended pattern at top ${top.toShortString()}", - ) - case patResult: (SeqPattern.Result.Look[T] | - SeqPattern.Result.Match[T]) => - assert(patResult.parent eq parent) - if patResult.isMatch - then patResult.idx - startIdx + 1 - else patResult.idx - startIdx - - assert( - matchedCount >= 1, - "can't rewrite based on a pattern that matched nothing", - ) - tracer.onRewriteMatch( - summon[DebugInfo], - parent, - startIdx, - matchedCount, - ) - action(using on.Ctx(matchedCount))(value) - - object on: - final case class Ctx(matchedCount: Int) - - def spliceThen(using - DebugInfo.Ctx, - )(using - onCtx: on.Ctx, - )( - nodes: Iterable[Node.Child], - )( - manip: on.Ctx ?=> Rules, - ): Rules = - /* Preparation for the rewrite may have reparented the node we were "on" - * When you immediately call splice, this pretty much always means "stay at - * that index and put something there", so that's what we do. - * Much easier than forcing the end user to understand such a confusing edge - * case. */ - keepHandleIdx: - (getHandle, getTracer).tupled - .tapEffect: (handle, tracer) => - handle.assertCoherence() - val (parent, idx) = - handle match - case Handle.AtTop(top) => - throw NodeError(s"tried to splice top ${top.toShortString()}") - case Handle.AtChild(parent, idx, _) => (parent, idx) - case Handle.Sentinel(parent, idx) => (parent, idx) - parent.children.patchInPlace(idx, nodes, onCtx.matchedCount) - tracer.onRewriteComplete( - summon[DebugInfo], - parent, - idx, - nodes.size, - ) - *> keepHandleIdx( - pass.resultRef.updated(_ => RulesResult.Progress)( - manip(using on.Ctx(nodes.size)), - ), - ) - - def spliceThen(using - DebugInfo, - on.Ctx, - )(nodes: Node.Child*)( - manip: on.Ctx ?=> Rules, - ): Rules = - spliceThen(nodes)(manip) - - def spliceThen(using - DebugInfo, - on.Ctx, - )(nodes: IterableOnce[Node.Child])( - manip: on.Ctx ?=> Rules, - ): Rules = - spliceThen(nodes.iterator.toArray)(manip) - - def splice(using - DebugInfo.Ctx, - )(using - onCtx: on.Ctx, - passCtx: pass.Ctx, - )( - nodes: Iterable[Node.Child], - ): Rules = - spliceThen(nodes): - /* If we _now_ have a match count of 0, it means the splice deleted our - * match. In that case, it's safe to retry at same position because we - * changed something. Normally that's bad, because it means we matched - * nothing and will retry in same position. */ - if summon[on.Ctx].matchedCount == 0 - then continuePass - else skipMatch - - def splice(using DebugInfo, on.Ctx, pass.Ctx)(nodes: Node.Child*): Rules = - splice(nodes) - - def splice(using - DebugInfo, - on.Ctx, - pass.Ctx, - )( - nodes: IterableOnce[Node.Child], - ): Rules = - splice(nodes.iterator.toArray) - - def continuePass(using passCtx: pass.Ctx): Rules = - passCtx.loop - - def continuePassAtNextNode(using DebugInfo)(using pass.Ctx): Rules = - atNextNode(continuePass) - - def atNextNode(using - DebugInfo, - )(using passCtx: pass.Ctx)(manip: Rules): Rules = - passCtx.strategy.atNext(manip) - - def endPass(using DebugInfo): Rules = - pass.resultRef.get - - def skipMatch(using - DebugInfo.Ctx, - )(using onCtx: on.Ctx, passCtx: pass.Ctx): Rules = - require( - onCtx.matchedCount > 0, - s"must have matched at least one node to skip. Matched ${onCtx.matchedCount}", - ) - getHandle.lookahead.flatMap: handle => - handle.assertCoherence() - val idxOpt = - handle match - case Handle.AtTop(top) => None - case Handle.AtChild(_, idx, _) => Some(idx) - case Handle.Sentinel(_, idx) => Some(idx) - - idxOpt match - case None => endPass - case Some(idx) => - atIdx(idx + onCtx.matchedCount)(continuePass) - - extension [T](lhs: Manip[T]) - @scala.annotation.targetName("or") - def |(using DebugInfo)(rhs: Manip[T]): Manip[T] = - Manip.monoidK.combineK(lhs, rhs) - def flatMap[U](using DebugInfo)(fn: T => Manip[U]): Manip[U] = - Manip.FlatMap(lhs, t => commit(fn(t))) - def lookahead: Lookahead[T] = - Lookahead(lhs) - def restrict[U](using DebugInfo)(fn: PartialFunction[T, U]): Manip[U] = - Manip.Restrict(lhs, fn, summon[DebugInfo]) - def filter(using DebugInfo)(pred: T => Boolean): Manip[T] = - lhs.restrict: - case v if pred(v) => v - def withFinally(fn: => Unit): Manip[T] = - Manip.Finally(lhs, () => fn) - def tapEffect(fn: T => Unit): Manip[T] = - Manip.TapEffect(lhs, fn) - - extension (lhs: Manip[Node.All]) - def here[U](using DebugInfo.Ctx)(manip: Manip[U]): Manip[U] = - lhs.lookahead.flatMap: node => - restrictHandle(manip)(_ => Some(Handle.fromNode(node))) - - final class pass( - strategy: pass.TraversalStrategy = pass.topDown, - once: Boolean = false, - wrapFn: Manip[Unit] => Manip[Unit] = identity, - ): - def rules(using DebugInfo.Ctx)(rules: pass.Ctx ?=> Rules): Manip[Unit] = - val before = getTracer - .flatMap: tracer => - effect(tracer.beforePass(summon[DebugInfo])) - val after = getTracer - .flatMap: tracer => - effect(tracer.afterPass(summon[DebugInfo])) - - // TODO: consider optimizing the ancestor check if it becomes a bottleneck - def exceptInError[T](manip: Manip[T])(using DebugInfo.Ctx): Manip[T] = - on(not(ancestor(Builtin.Error))).check *> manip - - lazy val loop: Manip[RulesResult] = - commit: - exceptInError(rules(using pass.Ctx(strategy, defer(loop)))) - | strategy.atNext(defer(loop)) - - wrapFn: - lazy val bigLoop: Manip[Unit] = - pass.resultRef.reset: - pass.resultRef.init(RulesResult.NoProgress): - (before *> strategy.atInit(loop) <* after).flatMap: - case RulesResult.Progress if !once => - bigLoop - case RulesResult.Progress | RulesResult.NoProgress => - pure(()) - - bigLoop - - def withState[T](ref: Ref[T])(init: => T): pass = - pass( - strategy = strategy, - once = once, - wrapFn = manip => ref.init(init)(defer(wrapFn(manip))), - ) - end pass - - object pass: - object resultRef extends Ref[RulesResult] - - final case class Ctx( - strategy: TraversalStrategy, - loop: Manip[RulesResult], - )(using DebugInfo.Ctx): - lazy val loopAtNext: Manip[RulesResult] = - strategy.atNext(loop) - - trait TraversalStrategy: - def atInit(manip: Manip[RulesResult])(using - DebugInfo.Ctx, - ): Manip[RulesResult] - def atNext(manip: Manip[RulesResult])(using - DebugInfo.Ctx, - ): Manip[RulesResult] - - object topDown extends TraversalStrategy: - def atInit(manip: Manip[RulesResult])(using - DebugInfo.Ctx, - ): Manip[RulesResult] = manip - - def atNext(manip: Manip[RulesResult])(using - DebugInfo.Ctx, - ): Manip[RulesResult] = - val next = commit(manip) - commit: - atFirstChild(next) - | atRightSibling(next) - | (on(atEnd).check - *> atParent( - commit( - /* going right finds either real sibling or sentinel, unless - * at top */ - atRightSibling(next) - | on(theTop).check *> endPass, - ), - )) - - object bottomUp extends TraversalStrategy: - def atInit(manip: Manip[RulesResult])(using - DebugInfo.Ctx, - ): Manip[RulesResult] = - lazy val impl: Manip[RulesResult] = - commit: - atFirstChild(defer(impl)) - | manip - - impl - - def atNext(manip: Manip[RulesResult])(using - DebugInfo.Ctx, - ): Manip[RulesResult] = - val next = commit(manip) - def atNextCousin[T](manip: Manip[T]): Manip[T] = - lazy val impl: Manip[T] = - atFirstChild(manip) - | atRightSibling(defer(impl)) - - atParent(atRightSibling(impl)) - - commit: - /* same layer, same parent (next case ensures we already processed any - * children) */ - atRightSibling(next) - // go all the way into next subtree on the right - | atNextCousin(atInit(next)) - /* up one layer, far left, knowing we looked at all reachable - * children */ - | atParent(atFirstSibling(next)) - // special case: parent is top - | atParent(on(theTop).check *> next) - // onward from top --> done - | on(theTop).check *> endPass -end ManipOps diff --git a/src/manip/ReferenceTracer.scala b/src/manip/ReferenceTracer.scala deleted file mode 100644 index 73cba2b..0000000 --- a/src/manip/ReferenceTracer.scala +++ /dev/null @@ -1,398 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.manip - -import java.io.{ByteArrayOutputStream, PrintStream} - -import cats.StackSafeMonad - -import forja.* -import forja.dsl.* - -import scala.collection.mutable - -final class ReferenceTracer(manip: Manip[?])(using - baseDebugInfo: DebugInfo, -) extends Tracer: - import ReferenceTracer.* - private val referenceActionSrc: () => Action = - referenceEval(manip, baseDebugInfo) - - private var referenceHasTerminated = false - private val actionsSoFar = mutable.ListBuffer[Action]() - - private def reportErr(action: Action, expectedAction: Action): Nothing = - val tmpDir = os.pwd / ".ref_compare" - os.makeDir.all(tmpDir) - val outBuf = StringBuilder() - - def showAction(action: Action): Unit = - action match - case Action.Terminate => - outBuf ++= "terminate" - case Action.AfterTerm => - outBuf ++= "after end" - case Action.Assign(ref, value) => - outBuf ++= s"assign $ref <-- $value" - case Action.Del(ref, debugInfo) => - outBuf ++= s"del $ref at $debugInfo" - case Action.Read(ref, value, debugInfo) => - outBuf ++= s"read $ref --> $value at $debugInfo" - case Action.Branch(debugInfo) => - outBuf ++= s"branch at $debugInfo" - case Action.Commit(debugInfo) => - outBuf ++= s"commit at $debugInfo" - case Action.Backtrack(debugInfo) => - outBuf ++= s"backtrack at $debugInfo" - case Action.Fatal(debugInfo) => - outBuf ++= s"fatal at $debugInfo" - case Action.Crash(ex) => - outBuf ++= "crash " - val out = ByteArrayOutputStream() - ex.printStackTrace(PrintStream(out)) - outBuf ++= out.toString() - - actionsSoFar.foreach: action => - showAction(action) - outBuf += '\n' - - outBuf ++= "!! " - showAction(action) - outBuf += '\n' - outBuf ++= "EE " - showAction(expectedAction) - outBuf += '\n' - - val outFile = - os.temp(dir = tmpDir, contents = outBuf.toString(), deleteOnExit = false) - throw new AssertionError( - s"diverged from reference implementation; see $outFile", - ) - end reportErr - - private def perform(action: Action): Unit = - if referenceHasTerminated - then reportErr(action, Action.AfterTerm) - - val expectedAction = referenceActionSrc() - - if expectedAction == Action.Terminate - then referenceHasTerminated = true - if expectedAction != action - then reportErr(action, expectedAction) - else actionsSoFar += action - end perform - - def close(): Unit = () - - def beforePass(debugInfo: DebugInfo): Unit = () - - def afterPass(debugInfo: DebugInfo): Unit = () - - def onRead( - manip: Manip[?], - ref: Manip.Ref[?], - value: Any, - debugInfo: DebugInfo, - ): Unit = - perform(Action.Read(ref, ValueWrapper(value), debugInfo)) - - def onAssign(manip: Manip[?], ref: Manip.Ref[?], value: Any): Unit = - perform(Action.Assign(ref, ValueWrapper(value))) - - def onDel(manip: Manip[?], ref: Manip.Ref[?], debugInfo: DebugInfo): Unit = - perform(Action.Del(ref, debugInfo)) - - def onBranch(manip: Manip[?], debugInfo: DebugInfo): Unit = - perform(Action.Branch(debugInfo)) - - def onBacktrack(manip: Manip[?], debugInfo: DebugInfo): Unit = - perform(Action.Backtrack(debugInfo)) - - def onCommit(manip: Manip[?], debugInfo: DebugInfo): Unit = - perform(Action.Commit(debugInfo)) - - def onRewriteMatch( - debugInfo: DebugInfo, - parent: Node.Parent, - idx: Int, - matchCount: Int, - ): Unit = () - - def onRewriteComplete( - debugInfo: DebugInfo, - parent: Node.Parent, - idx: Int, - resultCount: Int, - ): Unit = () - - def onFatal(manip: Manip[?], debugInfo: DebugInfo, from: DebugInfo): Unit = - perform(Action.Fatal(debugInfo)) -end ReferenceTracer - -object ReferenceTracer: - private class ValueWrapper(target: Any): - val value = target match - case _: mutable.Builder[?, ?] => null - case _ => target - override def equals(that: Any): Boolean = - that match - case that: ValueWrapper => value == that.value - case _ => false - - override def hashCode(): Int = - value match - case null => -1 - case value => value.hashCode() - - override def toString(): String = - value match - case null => "null" - case value => value.toString() - - private enum Action: - case Terminate - case AfterTerm - case Assign(ref: Manip.Ref[?], value: ValueWrapper) - case Read(ref: Manip.Ref[?], value: ValueWrapper, debugInfo: DebugInfo) - case Del(ref: Manip.Ref[?], debugInfo: DebugInfo) - case Branch(debugInfo: DebugInfo) - case Commit(debugInfo: DebugInfo) - case Backtrack(debugInfo: DebugInfo) - case Fatal(debugInfo: DebugInfo) - case Crash(throwable: Throwable) - - private final class BacktrackException(val debugInfo: DebugInfo) - extends RuntimeException(null, null, true, false) - private final class ExitException(cause: Throwable | Null) - extends RuntimeException(cause): - def this() = - this(null) - - private enum Result[+T]: - case Backtrack(posInfo: DebugInfo) - case Resumable(action: Action, resume: () => Result[T]) - case Value(value: T) - - def map[U](fn: T => U): Result[U] = - flatMap(v => Value(fn(v))) - - def flatMap[U](fn: T => Result[U]): Result[U] = - this match - case Backtrack(posInfo) => Backtrack(posInfo) - case Resumable(action, resume) => - Resumable(action, () => resume().flatMap(fn)) - case Value(value) => - fn(value) - - def always(fn: => () => Unit): Result[T] = - this match - case bt @ Backtrack(_) => - fn() - bt - case Resumable(action, resume) => - Resumable(action, () => resume().always(fn)) - case v @ Value(_) => - fn() - v - this - - def recover[U >: T](fn: DebugInfo => Result[U]): Result[U] = - this match - case Backtrack(posInfo) => fn(posInfo) - case Resumable(action, resume) => - Resumable(action, () => resume().recover(fn)) - case Value(value) => Value(value) - - def run(): () => Action = - var trampoline: () => (Result[?] | Null) = () => this - () => - trampoline() match - case null => - throw RuntimeException( - "tried to get actions from exhausted reference impl", - ) - case Backtrack(posInfo) => - throw RuntimeException(s"unrecovered backtrack $posInfo") - case Resumable(action, resume) => - trampoline = resume - action - case Value(value) => - trampoline = () => null - Action.Terminate - end run - - private object Result: - export monad.pure - - // Note: stack safe _assuming we log regularly_ - given monad: StackSafeMonad[Result] with - def pure[A](x: A): Result[A] = Result.Value(x) - def flatMap[A, B](fa: Result[A])(f: A => Result[B]): Result[B] = - fa.flatMap(f) - - /* FIXME: allow yielding actions without constructing a giant object tree, - * then we'll be acceptably efficient */ - - private def referenceEval( - manip: Manip[?], - baseDebugInfo: DebugInfo, - ): () => Action = - import cats.syntax.all.given - import Manip.* - - def perform(action: Action): Result[Unit] = - Result.Resumable(action, () => Result.pure(())) - - type BacktrackFn = DebugInfo => Result[Nothing] - - def backtrackFatal( - debugInfo: DebugInfo, - )(posInfo: DebugInfo): Result[Nothing] = - for - _ <- perform(Action.Fatal(debugInfo)) - result <- Result.pure(???) - yield result - - def impl[T](self: Manip[T])(using refMap: Manip.RefMap): Result[T] = - def doBacktrack(debugInfo: DebugInfo): Result[T] = - perform(Action.Backtrack(debugInfo)) - *> Result.Backtrack(debugInfo) - - self match - case Backtrack(debugInfo) => - doBacktrack(debugInfo) - case Pure(value) => - Result.pure(value) - case Ap(ff, fa) => - for - ffVal <- impl(ff) - faVal <- impl(fa) - yield ffVal(faVal) - case MapOpt(manip, fn) => - impl(manip).map(fn) - case FlatMap(manip, fn) => - for - value <- impl(manip) - result <- impl(fn(value)) - yield result - case Restrict(manip, restriction, debugInfo) => - impl(manip).flatMap: - case restriction(value) => Result.pure(value) - case badVal => doBacktrack(debugInfo) - case Effect(fn) => - Result.pure(fn()) - case Finally(manip, fn) => - impl(manip).always(fn) - case KeepLeft(left, right) => - impl(left).flatMap: lVal => - impl(right).map(_ => lVal) - case KeepRight(left, right) => - impl(left).flatMap: _ => - impl(right) - case Commit(manip, debugInfo) => - perform(Action.Commit(debugInfo)) - *> impl(manip).recover(backtrackFatal(debugInfo)) - case refInit: RefInit[u, t] => - refMap.get(refInit.ref) match - case Some(_) => - doBacktrack(refInit.debugInfo) - case None => - var value = refInit.initFn() - // On first node init, we are being given a ref to a fresh node. - // Clone it so we don't step all over the "real one's" work. - if refInit.ref == Handle.ref - then - value.asInstanceOf[Handle] match - case Handle.AtTop(top) => - value = Handle.AtTop(top.clone()).asInstanceOf[u] - case Handle.AtChild(parent, idx, child) => - val pClone = parent.clone() - value = Handle - .AtChild(pClone, idx, pClone.children(idx)) - .asInstanceOf[u] - case Handle.Sentinel(parent, idx) => - val pClone = parent.clone() - value = Handle.Sentinel(pClone, idx).asInstanceOf[u] - - for - _ <- perform(Action.Assign(refInit.ref, ValueWrapper(value))) - result <- impl(refInit.manip)(using - refMap.updated(refInit.ref, value), - ) - _ <- perform(Action.Del(refInit.ref, refInit.debugInfo)) - yield result - case RefReset(ref, manip, debugInfo) => - for - nextRefMap <- refMap.get(ref) match - case None => Result.pure(refMap) - case Some(_) => - perform(Action.Del(ref, debugInfo)) - *> Result.pure(refMap.removed(ref)) - result <- impl(manip)(using nextRefMap) - yield result - case RefGet(ref, debugInfo) => - refMap.get(ref) match - case None => doBacktrack(debugInfo) - case Some(value) => - perform(Action.Read(ref, ValueWrapper(value), debugInfo)) - *> Result.pure(value) - case RefUpdated(ref, fn, manip, debugInfo) => - refMap.get(ref) match - case None => doBacktrack(debugInfo) - case Some(oldValue) => - for - _ <- perform( - Action.Read(ref, ValueWrapper(oldValue), debugInfo), - ) - value = fn(oldValue) - _ <- perform(Action.Assign(ref, ValueWrapper(value))) - result <- impl(manip)(using refMap.updated(ref, value)) - yield result - case GetTracer => Result.pure(NopTracer) - case Disjunction(first, second, debugInfo) => - perform(Action.Branch(debugInfo)) - *> impl(first).recover: _ => - impl(second) - case Deferred(fn) => - impl(fn()) - case TapEffect(manip, fn) => - for - value <- impl(manip) - _ = fn(value) - yield value - case RestrictHandle(fn, manip, debugInfo) => - refMap.get(Handle.ref) match - case None => doBacktrack(debugInfo) - case Some(oldHandle) => - perform( - Action - .Read(Handle.ref, ValueWrapper(oldHandle), debugInfo), - ) - *> (oldHandle match - case fn(handle) => - perform( - Action.Assign(Handle.ref, ValueWrapper(handle)), - ) - *> impl(manip)(using - refMap.updated(Handle.ref, handle), - ) - case _ => doBacktrack(debugInfo)) - end impl - - impl(manip)(using RefMap.empty) - .recover(backtrackFatal(baseDebugInfo)) - .run() -end ReferenceTracer diff --git a/src/manip/SegmentedStack.scala b/src/manip/SegmentedStack.scala deleted file mode 100644 index 37d8d2f..0000000 --- a/src/manip/SegmentedStack.scala +++ /dev/null @@ -1,77 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.manip - -import scala.util.NotGiven - -final class SegmentedStack[T <: AnyRef](using NotGiven[Array[?] <:< T]): - inline val stackSegmentSize = 16 - private var stack = Array.ofDim[AnyRef](stackSegmentSize) - private var base = stack - private var stackSize = 0 - - def consumeInReverse(fn: T => Unit): Unit = - var idx = 0 - while base ne stack do - base(idx) match - case next: Array[AnyRef] if idx == 0 => - idx += 1 // back ref, skip - case next: Array[AnyRef] if idx == base.length - 1 => - base = next - idx = 0 - case elem => - fn(elem.asInstanceOf[T]) - idx += 1 - end while - - while idx < stackSize do - base(idx) match - case _: Array[AnyRef] if idx == 0 => // skip - case elem => - fn(elem.asInstanceOf[T]) - base(idx) = null.asInstanceOf[AnyRef] - idx += 1 - end while - stackSize = 0 - end consumeInReverse - - def push(rec: T): Unit = - if stackSize == stack.length - then - val oldStack = stack - val prevElem = stack(stackSize - 1) - stack = Array.ofDim[AnyRef](stackSegmentSize) - oldStack(stackSize - 1) = stack - stack(0) = oldStack - stack(1) = prevElem - stackSize = 2 - - stack(stackSize) = rec - stackSize += 1 - - @scala.annotation.tailrec - def pop(): T | Null = - if stackSize == 0 - then null - else - stackSize -= 1 - stack(stackSize) match - case lowerStack: Array[AnyRef] => - stack = lowerStack - stack(stack.length - 1) = null.asInstanceOf[AnyRef] // drop next-ptr - stackSize = stack.length - 1 - pop() - case rec => rec.asInstanceOf[T] -end SegmentedStack diff --git a/src/manip/SegmentedStack.test.scala b/src/manip/SegmentedStack.test.scala deleted file mode 100644 index b7f0620..0000000 --- a/src/manip/SegmentedStack.test.scala +++ /dev/null @@ -1,78 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.manip - -import munit.Location -import munit.diff.DiffOptions - -class SegmentedStackTests extends munit.FunSuite: - given munit.Compare[Integer | Null, Int] with - def isEqual(obtained: Integer | Null, expected: Int): Boolean = - obtained match - case null => false - case int: Integer => int == expected - given munit.Compare[Int, Integer] with - def isEqual(obtained: Int, expected: Integer): Boolean = - obtained == expected - - // 33 is more than twice 16, the segment size - (0 to 33).foreach: count => - test(s"push ascending numbers 0-$count"): - val stack = SegmentedStack[Integer]() - - (0 to count).foreach: num => - stack.push(num) - - (0 to count).reverse.foreach: num => - assertEquals(stack.pop(), num) - - (0 until 3).foreach: _ => - assertEquals(stack.pop(), null) - - test(s"push-pop ascending numbers 0-$count"): - val stack = SegmentedStack[Integer]() - - (0 to count).foreach: num => - stack.push(num) - assertEquals(stack.pop(), num) - stack.push(num) - - (0 to count).reverse.foreach: num => - assertEquals(stack.pop(), num) - stack.push(num) - assertEquals(stack.pop(), num) - - (0 until 3).foreach: _ => - assertEquals(stack.pop(), null) - - test(s"consumeInReverse 0-$count"): - val stack = SegmentedStack[Integer]() - - (0 to count).foreach: num => - stack.push(num) - var idx = 0 - stack.consumeInReverse: actualIdx => - assertEquals(idx, actualIdx) - idx += 1 - assertEquals(stack.pop(), null) - - // do it twice in case we're corrupted - (0 to count).foreach: num => - stack.push(num) - idx = 0 - stack.consumeInReverse: actualIdx => - assertEquals(idx, actualIdx) - idx += 1 - assertEquals(stack.pop(), null) diff --git a/src/manip/SeqPattern.scala b/src/manip/SeqPattern.scala deleted file mode 100644 index 06f55cf..0000000 --- a/src/manip/SeqPattern.scala +++ /dev/null @@ -1,211 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.manip - -import cats.syntax.all.given - -import forja.* -import forja.dsl.* - -import scala.util.NotGiven - -final class SeqPattern[+T](val manip: Manip[SeqPattern.Result[T]]): - private inline given poison(using NotGiven[DebugInfo.Ctx]): DebugInfo = - DebugInfo.poison - import SeqPattern.* - - def |[U >: T](using DebugInfo.Ctx)(other: SeqPattern[U]): SeqPattern[U] = - SeqPattern(manip.combineK(other.manip)) - - def productAtRightSibling[U](using - DebugInfo.Ctx, - )(other: SeqPattern[U]): SeqPattern[(T, U)] = - SeqPattern: - manip.lookahead.flatMap: - case Result.Top(_, _) => backtrack - case Result.Look(_, idx, t) => - atIdx(idx)(other.map((t, _)).manip) - case Result.Match(_, idx, t) => - atIdx(idx + 1)(other.map((t, _)).manip) - - def restrict[U](using - DebugInfo.Ctx, - )(fn: PartialFunction[T, U]): SeqPattern[U] = - SeqPattern: - manip.restrict: - case Result.Top(top, fn(u)) => Result.Top(top, u) - case Result.Look(parent, idx, fn(u)) => Result.Look(parent, idx, u) - case Result.Match(parent, idx, fn(u)) => Result.Match(parent, idx, u) - - def filter(using DebugInfo.Ctx)(pred: T => Boolean): SeqPattern[T] = - SeqPattern(manip.filter(res => pred(res.value))) - - def asManip: Manip[T] = - manip.map(_.value) - - @scala.annotation.targetName("fieldsConcat") - def ~[Tpl1 <: Tuple, Tpl2 <: Tuple](using - T <:< Fields[Tpl1], - )(using - DebugInfo.Ctx, - )( - other: SeqPattern[Fields[Tpl2]], - ): SeqPattern[Fields[Tuple.Concat[Tpl1, Tpl2]]] = - this - .productAtRightSibling(other) - .map: (flds1, flds2) => - Fields(flds1.fields ++ flds2.fields) - - @scala.annotation.targetName("fieldsEnd") - def ~[Tpl <: Tuple, T2](using - T <:< Fields[Tpl], - )(using - maybeStrip: Fields.MaybeStripTuple1[Tpl, T2], - )(using DebugInfo.Ctx)(other: eof.type): SeqPattern[T2] = - this.productAtRightSibling(atEnd).map(flds => maybeStrip(flds._1.fields)) - - @scala.annotation.targetName("fieldsTrailing") - def ~[Tpl <: Tuple, T2](using - T <:< Fields[Tpl], - )(using - maybeStrip: Fields.MaybeStripTuple1[Tpl, T2], - )( - other: trailing.type, - ): SeqPattern[T2] = - this.map(flds => maybeStrip(flds.fields)) - - def stripFields[TT](using T <:< Fields[TT]): SeqPattern[TT] = - this.map(t => t.fields) - - @scala.annotation.targetName("logicalAnd") - def &&(other: SeqPattern[?]): SeqPattern[Unit] = - this *> other.void - -object SeqPattern: - private inline given poison(using NotGiven[DebugInfo.Ctx]): DebugInfo = - DebugInfo.poison - export applicative.{pure, unit} - - given applicative: cats.Applicative[SeqPattern] with - override val unit: SeqPattern[Unit] = pure(()) - def pure[A](x: A): SeqPattern[A] = SeqPattern: - getHandle.map: - case Handle.AtTop(top) => - Result.Top(top, x) - case handle: (Handle.Sentinel | Handle.AtChild) => - Result.Look(handle.parent, handle.idx, x) - override def map[A, B](fa: SeqPattern[A])(f: A => B): SeqPattern[B] = - SeqPattern: - fa.manip.map: result => - result.withValue(f(result.value)) - def ap[A, B](ff: SeqPattern[A => B])(fa: SeqPattern[A]): SeqPattern[B] = - SeqPattern: - (ff.manip, fa.manip).mapN: (result1, result2) => - result1.combine(result2)(_.apply(_)) - - given semigroupk: cats.SemigroupK[SeqPattern] with - def combineK[A](x: SeqPattern[A], y: SeqPattern[A]): SeqPattern[A] = - SeqPattern(x.manip | y.manip) - - enum Result[+T]: - val value: T - - case Top(top: Node.Top, value: T) - case Look(parent: Node.Parent, idx: Int, value: T) - case Match(parent: Node.Parent, idx: Int, value: T) - - def isLook: Boolean = - this match - case _: Look[?] => true - case _ => false - - def isMatch: Boolean = - this match - case _: Match[?] => true - case _ => false - - def withValue[U](value: U): Result[U] = - if this.value.asInstanceOf[AnyRef] eq value.asInstanceOf[AnyRef] - then this.asInstanceOf - else - this match - case Top(top, _) => Top(top, value) - case Look(parent, idx, _) => Look(parent, idx, value) - case Match(parent, idx, _) => Match(parent, idx, value) - - def combine[U, V](other: Result[U])(fn: (T, U) => V): Result[V] = - def result = fn(value, other.value) - (this, other) match - case (Top(top1, _), Top(top2, _)) => - assert(top1 eq top2) - Top(top1, result) - case (Top(_, _), _) | (_, Top(_, _)) => - throw NodeError( - "one part of the same pattern matched top while the other did not", - ) - case (lhs: (Look[T] | Match[T]), rhs: (Look[U] | Match[U])) => - assert(rhs.parent eq rhs.parent) - val parent = lhs.parent - def mkLook(idx: Int): Result[V] = - Look(parent, idx, result) - def mkMatch(idx: Int): Result[V] = - Match(parent, idx, result) - - (lhs, rhs) match - case (Look(_, idx1, _), Look(_, idx2, _)) => - mkLook(idx1.max(idx2)) - case (Look(_, idx1, _), Match(_, idx2, _)) if idx1 <= idx2 => - mkMatch(idx2) - case (Look(_, idx1, _), Match(_, idx2, _)) => - assert(idx1 > idx2) - mkLook(idx1) - case (Match(_, idx1, _), Look(_, idx2, _)) if idx1 < idx2 => - mkLook(idx2) - case (Match(_, idx1, _), Look(_, idx2, _)) => - assert(idx1 >= idx2) - mkMatch(idx1) - case (Match(_, idx1, _), Match(_, idx2, _)) => - mkMatch(idx1.max(idx2)) - - object Result: - extension [T](result: Result.Look[T] | Result.Match[T]) - def parent: Node.Parent = - result match - case Result.Look(parent, _, _) => parent - case Result.Match(parent, _, _) => parent - def idx: Int = - result match - case Result.Look(_, idx, _) => idx - case Result.Match(_, idx, _) => idx - - import scala.language.implicitConversions - implicit def tokenAsTok(using DebugInfo)(token: Token): SeqPattern[Node] = - tok(token) - - final class Fields[T](val fields: T) extends AnyVal - object Fields: - sealed trait MaybeStripTuple1[Tpl <: Tuple, T]: - def apply(tpl: Tpl): T - - given stripTuple1[T]: MaybeStripTuple1[Tuple1[T], T] with - def apply(tpl: Tuple1[T]): T = tpl._1 - - given notTuple1[Tpl <: Tuple](using - scala.util.NotGiven[Tpl <:< Tuple1[?]], - ): MaybeStripTuple1[Tpl, Tpl] with - def apply(tpl: Tpl): Tpl = tpl - - case object FieldsEndMarker - case object FieldsTrailingMarker diff --git a/src/manip/SeqPattern.test.scala b/src/manip/SeqPattern.test.scala deleted file mode 100644 index 78e0bd5..0000000 --- a/src/manip/SeqPattern.test.scala +++ /dev/null @@ -1,141 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.manip - -import cats.syntax.all.given - -import forja.dsl.* - -class SeqPatternTests extends munit.FunSuite: - import PatternTests.* - - extension [T](pat: on[T]) - def onChildren(using - munit.Location, - )(name: String)(children: Node.Child*)( - expected: T, - ): pat.type = - val top = Node.Top(children) - test(name): - val result = - initNode(top)(atFirstChild(pat.value)) - .perform() - assertEquals(result, expected) - pat - - on( - tok(tok1) - | SeqPattern.pure("no"), - ) - .onChildren("tok: match tok1")(tok1())(tok1()) - .onChildren("tok: no match tok1")(tok2())("no") - .onChildren("tok: empty")()("no") - - on( - field(tok(tok1)) - ~ field(tok(tok2)) - ~ eof - | SeqPattern.pure("no"), - ) - .onChildren("fields: match tok1, tok2")(tok1(), tok2())((tok1(), tok2())) - .onChildren("fields: too short")(tok1())("no") - .onChildren("fields: too long")(tok1(), tok2(), tok1())("no") - .onChildren("fields: switched")(tok2(), tok1())("no") - .onChildren("fields: empty")()("no") - - on( - field(repeated(tok(tok1))) - ~ eof - | SeqPattern.pure("no"), - ) - .onChildren("fields repeated: 0")()(Nil) - .onChildren("fields repeated: 1")(tok1())(List(tok1())) - .onChildren("fields repeated: 2")(tok1(), tok1())(List(tok1(), tok1())) - .onChildren("fields repeated: 3")(tok1(), tok1(), tok1())( - List(tok1(), tok1(), tok1()), - ) - .onChildren("fields repeated: odd one out")(tok1(), tok2(), tok1())("no") - .onChildren("fields repeated: prefix but end assert")( - tok1(), - tok1(), - tok2(), - )("no") - - on( - skip(tok(tok1)) - ~ field(tok(tok2)) - ~ skip(tok(tok3)) - ~ eof - | SeqPattern.pure("no"), - ) - .onChildren("fields skips: exact")(tok1(), tok2(), tok3())(tok2()) - .onChildren("fields skip: first missing")(tok2(), tok3())("no") - .onChildren("fields skip: last missing")(tok1(), tok2())("no") - .onChildren("fields skip: empty")()("no") - - on( - field( - tok(tok1).withChildren: - field(tok(tok2)) - ~ eof, - ) - ~ eof - | SeqPattern.pure("no"), - ) - .onChildren("fields withChildren: exact")(tok1(tok2()))(tok2()) - .onChildren("fields withChildren: missing parent")(tok2(tok2()))("no") - .onChildren("fields withChildren: missing child")(tok1(tok1()))("no") - .onChildren("fields parent: empty")()("no") - - on( - field( - anyNode.withChildren: - parent(tok(tok1)) *> field(tok(tok2)) - ~ eof, - ) - ~ eof - | SeqPattern.pure("no"), - ) - .onChildren("fields parent: exact")(tok1(tok2()))(tok2()) - .onChildren("fields parent: missing parent")(tok2(tok2()))("no") - .onChildren("fields parent: missing child")(tok1(tok1()))("no") - .onChildren("fields parent: empty")()("no") - - on( - skip(anyNode) - ~ field(leftSibling(tok(tok1)) *> tok(tok2)) - ~ eof - | SeqPattern.pure("no"), - ) - .onChildren("fields leftSibling: exact")(tok1(), tok2())(tok2()) - .onChildren("fields leftSibling: different left")(tok2(), tok2())("no") - .onChildren("fields leftSibling: different right")(tok1(), tok1())("no") - .onChildren("fields leftSibling: empty")()("no") - - on( - field(rightSibling(tok(tok2)) *> tok(tok1)) - ~ skip(anyNode) - ~ eof - | SeqPattern.pure("no"), - ) - .onChildren("fields rightSibling: exact")(tok1(), tok2())(tok1()) - .onChildren("fields rightSibling: different left")(tok2(), tok2())("no") - .onChildren("fields rightSibling: different right")(tok1(), tok1())("no") - .onChildren("fields rightSibling: empty")()("no") - -object PatternTests: - object tok1 extends Token - object tok2 extends Token - object tok3 extends Token diff --git a/src/manip/SeqPatternOps.scala b/src/manip/SeqPatternOps.scala deleted file mode 100644 index 1e73c63..0000000 --- a/src/manip/SeqPatternOps.scala +++ /dev/null @@ -1,220 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.manip - -import cats.syntax.all.given - -import forja.* -import forja.dsl.* -import forja.source.{Source, SourceRange} - -import scala.collection.IndexedSeqView - -import SeqPattern.* - -trait SeqPatternOps: - def refine[T](manip: Manip[T]): SeqPattern[T] = - SeqPattern: - (SeqPattern.unit.manip, manip).mapN(_.withValue(_)) - - @scala.annotation.targetName("deferSeqPattern") - def defer[T](pattern: => SeqPattern[T]): SeqPattern[T] = - SeqPattern(dsl.defer(pattern.manip)) - - def field[T](pattern: SeqPattern[T]): SeqPattern[Fields[Tuple1[T]]] = - pattern.map(t => Fields(Tuple1(t))) - - def fields[Tpl <: Tuple]( - pattern: SeqPattern[Tpl], - ): SeqPattern[Fields[Tpl]] = - pattern.map(Fields(_)) - - def skip[T](pattern: SeqPattern[T]): SeqPattern[Fields[EmptyTuple]] = - pattern.as(Fields(EmptyTuple)) - - def anyNode(using DebugInfo): SeqPattern[Node] = - SeqPattern: - getNode.restrict: - case node: Node => - Result.Match(node.parent.get, node.idxInParent, node) - - def anyChild(using DebugInfo): SeqPattern[Node.Child] = - SeqPattern: - getNode.restrict: - case child: Node.Child => - Result.Match(child.parent.get, child.idxInParent, child) - - def tok(using DebugInfo)(tokens: Token*): SeqPattern[Node] = - SeqPattern: - val tokenSet = tokens.toSet - getNode.restrict: - case node: Node if tokenSet(node.token) => - Result.Match(node.parent.get, node.idxInParent, node) - - def atEnd(using DebugInfo): SeqPattern[Unit] = - SeqPattern: - getHandle.restrict: - case Handle.Sentinel(parent, idx) => - Result.Look(parent, idx, ()) - - def atBegin(using DebugInfo): SeqPattern[Unit] = - SeqPattern: - getHandle.restrict: - case Handle.Sentinel(parent, 0) => - Result.Look(parent, 0, ()) - case Handle.AtChild(parent, 0, _) => - Result.Look(parent, 0, ()) - - def nodeSpanMatchedBy(using - DebugInfo.Ctx, - )( - pattern: SeqPattern[?], - ): SeqPattern[IndexedSeqView[Node.Child]] = - SeqPattern: - getHandle - .restrict: - case handle: (Handle.Sentinel | Handle.AtChild) => - (handle.parent, handle.idx) - .product(pattern.void.manip) - .restrict: - case ( - (parent, startIdx), - patResult: (Result.Look[Unit] | Result.Match[Unit]), - ) => - assert(patResult.parent eq parent) - patResult.withValue: - parent.children.view.slice( - from = startIdx, - until = - // Endpoint is exclusive. - // If we matched idx, add 1 to include it. - // Otherwise, default is fine. - if patResult.isMatch - then patResult.idx + 1 - else patResult.idx, - ) - - export SeqPattern.{FieldsEndMarker as eof, FieldsTrailingMarker as trailing} - - def not[T](using DebugInfo.Ctx)(pattern: SeqPattern[T]): SeqPattern[Unit] = - SeqPattern: - ((pattern.manip *> Manip.pure(true)) | Manip.pure(false)) - .restrict: - case false => () - *> SeqPattern.unit.manip - - def optional[T](using - DebugInfo.Ctx, - )( - pattern: SeqPattern[T], - ): SeqPattern[Option[T]] = - pattern.map(Some(_)) | pure(None) - - def repeated[T](using - DebugInfo.Ctx, - )( - pattern: SeqPattern[T], - ): SeqPattern[List[T]] = - lazy val impl: SeqPattern[List[T]] = - (field(pattern) ~ field(defer(impl)) ~ trailing) - .map(_ :: _) - | pure(Nil) - - impl - - def repeated1[T](using - DebugInfo.Ctx, - )( - pattern: SeqPattern[T], - ): SeqPattern[List[T]] = - (field(pattern) ~ field(repeated(pattern)) ~ trailing).map(_ :: _) - - def repeatedSepBy1[T](using - DebugInfo.Ctx, - )(sep: SeqPattern[?])(pattern: SeqPattern[T]): SeqPattern[List[T]] = - (field(pattern) ~ field( - repeated(skip(sep) ~ field(pattern) ~ trailing), - ) ~ trailing) - .map(_ :: _) - - def repeatedSepBy[T](using - DebugInfo.Ctx, - )(sep: SeqPattern[?])( - pattern: SeqPattern[T], - ): SeqPattern[List[T]] = - repeatedSepBy1(sep)(pattern) - | pure(Nil) - - def theTop(using DebugInfo): SeqPattern[Node.Top] = - SeqPattern: - getHandle.restrict: - case Handle.AtTop(top) => - Result.Top(top, top) - - def theFirstChild(using DebugInfo): SeqPattern[Node.Child] = - SeqPattern: - getHandle.restrict: - case Handle.AtChild(parent, 0, child) => - Result.Match(parent, 0, child) - - def children[T](using DebugInfo)(pattern: SeqPattern[T]): SeqPattern[T] = - refine(atFirstChild(pattern.asManip)) - - def onlyChild[T](using DebugInfo)(pattern: SeqPattern[T]): SeqPattern[T] = - refine(atFirstChild((field(pattern) ~ eof).asManip)) - - def parent[T](using DebugInfo)(pattern: SeqPattern[T]): SeqPattern[T] = - refine(atParent(on(pattern).value)) - - def leftSibling[T](using DebugInfo)(pattern: SeqPattern[T]): SeqPattern[T] = - refine(atLeftSibling(on(pattern).value)) - - def rightSibling[T](using DebugInfo)(pattern: SeqPattern[T]): SeqPattern[T] = - refine(atRightSibling(on(pattern).value)) - - def ancestor[T](using DebugInfo)(pattern: SeqPattern[T]): SeqPattern[T] = - refine(atAncestor(on(pattern).value)) - - def lastChild[T](using DebugInfo)(pattern: SeqPattern[T]): SeqPattern[T] = - refine(atLastChild(on(pattern).value)) - - def embed[T: EmbedMeta](using DebugInfo): SeqPattern[T] = - anyChild.restrict: - case embed @ Node.Embed(t) if embed.meta == EmbedMeta[T] => - t.asInstanceOf[T] - - extension [P <: Node.Parent](parentPattern: SeqPattern[P]) - def withChildren[T](using - DebugInfo, - )( - pattern: SeqPattern[T], - ): SeqPattern[T] = - SeqPattern: - parentPattern.manip.lookahead.flatMap: result => - val parent = result.value - atNode(parent)(atFirstChild(pattern.asManip)) - .map(result.withValue) - - extension (nodePattern: SeqPattern[Node]) - def src(using DebugInfo)(sourceRange: SourceRange): SeqPattern[Node] = - nodePattern.filter(_.sourceRange == sourceRange) - - def src(using DebugInfo)(str: String): SeqPattern[Node] = - src(SourceRange.entire(Source.fromString(str))) - - extension [T](hdPattern: SeqPattern[T]) - def *:[Tpl <: Tuple](tlPattern: SeqPattern[Tpl]): SeqPattern[T *: Tpl] = - (hdPattern, tlPattern).mapN(_ *: _) -end SeqPatternOps diff --git a/src/manip/Tracer.scala b/src/manip/Tracer.scala deleted file mode 100644 index dd83f6d..0000000 --- a/src/manip/Tracer.scala +++ /dev/null @@ -1,266 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.manip - -import forja.* -import forja.util.{++, toShortString} -import forja.wf.Wellformed - -import com.github.difflib.{DiffUtils, UnifiedDiffUtils} -import scala.jdk.CollectionConverters.* - -import Manip.Ref - -trait Tracer extends java.io.Closeable: - def beforePass(debugInfo: DebugInfo): Unit - def afterPass(debugInfo: DebugInfo): Unit - def onRead( - manip: Manip[?], - ref: Ref[?], - value: Any, - debugInfo: DebugInfo, - ): Unit - def onAssign(manip: Manip[?], ref: Ref[?], value: Any): Unit - def onDel(manip: Manip[?], ref: Ref[?], debugInfo: DebugInfo): Unit - def onBranch(manip: Manip[?], debugInfo: DebugInfo): Unit - def onCommit(manip: Manip[?], debugInfo: DebugInfo): Unit - def onBacktrack(manip: Manip[?], debugInfo: DebugInfo): Unit - def onFatal(manip: Manip[?], debugInfo: DebugInfo, from: DebugInfo): Unit - def onRewriteMatch( - debugInfo: DebugInfo, - parent: Node.Parent, - idx: Int, - matchedCount: Int, - ): Unit - def onRewriteComplete( - debugInfo: DebugInfo, - parent: Node.Parent, - idx: Int, - resultCount: Int, - ): Unit - -abstract class AbstractNopTracer extends Tracer: - def beforePass(debugInfo: DebugInfo): Unit = () - def afterPass(debugInfo: DebugInfo): Unit = () - def onRead( - manip: Manip[?], - ref: Ref[?], - value: Any, - debugInfo: DebugInfo, - ): Unit = () - def onAssign(manip: Manip[?], ref: Ref[?], value: Any): Unit = - () - def onDel(manip: Manip[?], ref: Ref[?], debugInfo: DebugInfo): Unit = () - def onBranch(manip: Manip[?], debugInfo: DebugInfo): Unit = () - def onCommit(manip: Manip[?], debugInfo: DebugInfo): Unit = () - def onBacktrack(manip: Manip[?], debugInfo: DebugInfo): Unit = - () - def onFatal(manip: Manip[?], debugInfo: DebugInfo, from: DebugInfo): Unit = - () - def onRewriteMatch( - debugInfo: DebugInfo, - parent: Node.Parent, - idx: Int, - matchedCount: Int, - ): Unit = () - def onRewriteComplete( - debugInfo: DebugInfo, - parent: Node.Parent, - idx: Int, - resultCount: Int, - ): Unit = () - def close(): Unit = () - -object NopTracer extends AbstractNopTracer - -final class LogTracer(out: java.io.OutputStream, limit: Int = -1) - extends Tracer: - def this(path: os.Path, limit: Int) = - this(os.write.over.outputStream(path, createFolders = true), limit) - def this(path: os.Path) = - this(path, limit = -1) - - export out.close - - private var lineCount: Int = 0 - private var currHandle: Option[Handle] = None - - private def logln(str: String): Unit = - out.write(str.getBytes()) - out.write('\n') - out.flush() - - lineCount += 1 - - if limit != -1 && lineCount >= limit - then throw RuntimeException(s"logged $lineCount lines") - - private def treeDesc: geny.Writable = - currHandle match - case None => "" - case Some(handle) => - handle.findTop match - case None => "" - case Some(top) => - top.toPrettyWritable(Wellformed.empty) - - def beforePass(debugInfo: DebugInfo): Unit = - logln(s"pass init $debugInfo at $treeDesc") - - def afterPass(debugInfo: DebugInfo): Unit = - logln(s"pass end $debugInfo at $treeDesc") - - def onRead( - manip: Manip[?], - ref: Ref[?], - value: Any, - debugInfo: DebugInfo, - ): Unit = - value match - case value: AnyVal => - logln(s"read $ref --> $value at $treeDesc") - case value: AnyRef => - logln(s"read $ref --> ${value.toShortString()} at $treeDesc") - - def onAssign(manip: Manip[?], ref: Ref[?], value: Any): Unit = - value match - case value: AnyVal => - logln(s"assign $ref <-- ${value} at $treeDesc") - case value: AnyRef => - logln(s"assign $ref <-- ${value.toShortString} at $treeDesc") - - def onDel(manip: Manip[?], ref: Ref[?], debugInfo: DebugInfo): Unit = - logln(s"del $ref $debugInfo") - - def onBranch(manip: Manip[?], debugInfo: DebugInfo): Unit = - logln(s"branch $debugInfo at $treeDesc") - - def onCommit(manip: Manip[?], debugInfo: DebugInfo): Unit = - logln(s"commit $debugInfo at $treeDesc") - - def onBacktrack(manip: Manip[?], debugInfo: DebugInfo): Unit = - logln(s"backtrack $debugInfo at $treeDesc") - - def onFatal(manip: Manip[?], debugInfo: DebugInfo, from: DebugInfo): Unit = - logln(s"fatal $debugInfo from $from at $treeDesc") - - def onRewriteMatch( - debugInfo: DebugInfo, - parent: Node.Parent, - idx: Int, - matchedCount: Int, - ): Unit = - logln( - s"rw match $debugInfo, from $idx, $matchedCount nodes in parent $parent", - ) - - def onRewriteComplete( - debugInfo: DebugInfo, - parent: Node.Parent, - idx: Int, - resultCount: Int, - ): Unit = - logln( - s"rw done $debugInfo, from $idx, $resultCount nodes in parent $parent", - ) - -final class RewriteDebugTracer(debugFolder: os.Path) extends AbstractNopTracer: - os.remove.all(debugFolder) // no confusing left-over files! - os.makeDir.all(debugFolder) - private var passCounter = 0 - private var rewriteCounter = 0 - - private var currHandle: Option[Handle] = None - - private def treeDesc: geny.Writable = - currHandle match - case None => "" - case Some(handle) => - handle.findTop match - case None => "" - case Some(top) => - top.toPrettyWritable(Wellformed.empty) - - private def pathAt( - passIdx: Int, - rewriteIdx: Int, - isDiff: Boolean = false, - ): os.Path = - val ext = if isDiff then ".diff" else ".txt" - debugFolder / f"$passIdx%03d" / f"$rewriteIdx%03d$ext%s" - - private def currPath: os.Path = - pathAt(passCounter, rewriteCounter) - - private def prevPath: os.Path = - pathAt(passCounter, rewriteCounter - 1) - - private def diffPath: os.Path = - pathAt(passCounter, rewriteCounter, isDiff = true) - - private def takeSnapshot(debugInfo: DebugInfo): Unit = - os.write( - target = currPath, - data = (s"//! $debugInfo\n": geny.Writable) ++ treeDesc ++ "\n", - createFolders = true, - ) - - private def takeDiff(): Unit = - val prevLines = os.read.lines(prevPath).asJava - val currLines = os.read.lines(currPath).asJava - val patch = DiffUtils.diff(prevLines, currLines) - val unifiedDiff = UnifiedDiffUtils.generateUnifiedDiff( - prevPath.toString(), - currPath.toString(), - prevLines, - patch, - 3, - ) - - os.write.over( - diffPath, - unifiedDiff.asScala.mkString("\n"), - ) - - override def onAssign(manip: Manip[?], ref: Ref[?], value: Any): Unit = - if ref == Handle.ref - then currHandle = Some(value.asInstanceOf[Handle]) - - override def onDel( - manip: Manip[?], - ref: Ref[?], - debugInfo: DebugInfo, - ): Unit = - if ref == Handle.ref - then currHandle = None - - override def beforePass(debugInfo: DebugInfo): Unit = - passCounter += 1 - rewriteCounter = 0 - takeSnapshot(debugInfo) - - override def afterPass(debugInfo: DebugInfo): Unit = - rewriteCounter += 1 - takeSnapshot(debugInfo) - - override def onRewriteComplete( - debugInfo: DebugInfo, - parent: Node.Parent, - idx: Int, - resultCount: Int, - ): Unit = - rewriteCounter += 1 - takeSnapshot(debugInfo) - takeDiff() diff --git a/src/sexpr/SExprReader.scala b/src/sexpr/SExprReader.scala deleted file mode 100644 index cac087d..0000000 --- a/src/sexpr/SExprReader.scala +++ /dev/null @@ -1,192 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.sexpr - -import cats.syntax.all.given - -import forja.* -import forja.source.{Reader, SourceRange} -import forja.wf.Wellformed - -object SExprReader extends Reader: - import forja.dsl.* - import forja.Builtin.{Error, SourceMarker} - import Reader.* - - def wellformed: Wellformed = lang.wf - - private val alpha: Set[Char] = - ('a' to 'z').toSet ++ ('A' to 'Z').toSet - private val pseudoAlpha: Set[Char] = - Set('-', '.', '/', '_', ':', '*', '+', '=') - private val digit: Set[Char] = ('0' to '9').toSet - private val whitespace: Set[Char] = Set(' ', '\n', '\r', '\t') - - private lazy val unexpectedEOF: Manip[SourceRange] = - consumeMatch: m => - addChild(Error("unexpected EOF", SourceMarker(m))) - *> Manip.pure(m) - - def canBeEncodedAsToken(src: SourceRange): Boolean = - src.nonEmpty - && (alpha(src.head.toChar) || pseudoAlpha(src.head.toChar)) - && src.tail.forall(b => - alpha(b.toChar) || pseudoAlpha(b.toChar) || digit(b.toChar), - ) - - protected lazy val rules: Manip[SourceRange] = - commit: - bytes - .selecting[SourceRange] - .onOneOf(whitespace): - extendThisNodeWithMatch(rules) - .on('('): - addChild(lang.SList()) - .here(extendThisNodeWithMatch(rules)) - .on(')'): - extendThisNodeWithMatch: - atParent: - rules - | consumeMatch: m => - addChild(Error("unexpected end of list", SourceMarker(m))) - *> rules - .onOneOf(digit): - rawMode - .onOneOf(alpha ++ pseudoAlpha): - tokenMode - .on('"'): - dropMatch(stringMode) // opening `"` is not part of string - .fallback: - bytes.selectCount(1): - consumeMatch: m => - addChild(Error("invalid byte", SourceMarker(m))) - *> rules - | consumeMatch: m => - // a perfectly normal EOF - on(theTop).check - *> Manip.pure(m) - | unexpectedEOF - - private lazy val rawMode: Manip[SourceRange] = - commit: - bytes.selectManyLike(digit): - consumeMatch: m => - m.decodeString().toIntOption match - case None => - addChild( - Error( - "length doesn't fit in a machine int", - SourceMarker(m), - ), - ) - *> rules - case Some(lengthPrefix) => - bytes - .selecting[SourceRange] - .on(':'): - dropMatch: - bytes.selectCount(lengthPrefix): - consumeMatch: m => - addChild(lang.SAtom(m)) - *> rules - | bytes.getSrc.flatMap: src => - val srcEnd = src.drop(src.length) - addChild( - Error( - "unexpected EOF before end of raw atom", - SourceMarker(srcEnd), - ), - ) - *> Manip.pure(srcEnd) - .fallback: - addChild( - Error( - "expected : after length prefix", - SourceMarker(m), - ), - ) - *> rules - - private lazy val tokenMode: Manip[SourceRange] = - commit: - bytes - .selecting[SourceRange] - .onOneOf(alpha)(tokenMode) - .onOneOf(pseudoAlpha)(tokenMode) - .onOneOf(digit)(tokenMode) - .fallback: - consumeMatch: m => - addChild(lang.SAtom(m)) - *> rules - - private lazy val stringMode: Manip[SourceRange] = - object builderRef extends Manip.Ref[SourceRange.Builder] - - def addByte(byte: Byte): Manip[SourceRange] = - dropMatch: - builderRef.doEffect(_.addOne(byte)) - *> impl - - def addStr(str: String): Manip[SourceRange] = - dropMatch: - val bytes = str.getBytes() - builderRef.doEffect(_.addAll(bytes)) - *> impl - - def finish(rest: Manip[SourceRange]): Manip[SourceRange] = - dropMatch: - builderRef.get.flatMap: builder => - val stringContents = builder.result() - addChild(lang.SAtom(stringContents)) - *> rest - - lazy val impl: Manip[SourceRange] = - commit: - bytes - .selecting[SourceRange] - .on('"')(finish(rules)) - .onSeq("\\\"")(addByte('"')) - .onSeq("\\\\")(addByte('\\')) - .onSeq("\\'")(addByte('\'')) - .onSeq("\\n")(addByte('\n')) - .onSeq("\\t")(addByte('\t')) - .onSeq("\\r")(addByte('\r')) - .onSeq("\\\n\r")(impl) - .onSeq("\\\r\n")(impl) - .onSeq("\\\n")(impl) - .onSeq("\\\r")(impl) - .on('\\'): - bytes.selectOne: - consumeMatch: mark => - addChild( - Error( - s"invalid escape sequence ${mark.decodeString()}", - Builtin.SourceMarker(mark), - ), - ) - *> addByte('?') - .fallback: - /* everything else goes in the literal, except EOF in which case we - * bail to default rules */ - bytes.selectOne: - consumeMatch: m => - assert(m.length == 1) - addByte(m.head) - | finish(unexpectedEOF) - - commit: - builderRef.reset: - builderRef.init(SourceRange.newBuilder): - dropMatch(impl) diff --git a/src/sexpr/SExprReader.test.scala b/src/sexpr/SExprReader.test.scala deleted file mode 100644 index 9961169..0000000 --- a/src/sexpr/SExprReader.test.scala +++ /dev/null @@ -1,144 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.sexpr - -import forja.* -import forja.Builtin.{Error, SourceMarker} -import forja.sexpr.lang.{SAtom, SList} -import forja.source.{Source, SourceRange} - -class SExprReaderTests extends munit.FunSuite: - extension (str: String) - def parse: Node.Top = - sexpr.parse.fromSourceRange(SourceRange.entire(Source.fromString(str))) - - test("foo string literal"): - assertEquals("\"foo\"".parse, Node.Top(SAtom("foo"))) - - test("escape tab char"): - assertEquals("\"\\t\"".parse, Node.Top(SAtom("\t"))) - - test("ignore crlf"): - assertEquals("\"\\\r\n\"".parse, Node.Top(SAtom(""))) - - test("error: invalid string escape"): - assertEquals( - "\"\\^\"".parse, - Node.Top( - Error("invalid escape sequence \\^", Builtin.SourceMarker("\\^")), - SAtom("?"), - ), - ) - - test("empty string"): - assertEquals("".parse, Node.Top()) - test("one space"): - assertEquals(" ".parse, Node.Top()) - test("all misc whitespace"): - assertEquals(" \n\n \t\r\n".parse, Node.Top()) - test("one atom"): - assertEquals("8:deadbeef".parse, Node.Top(SAtom("deadbeef"))) - test("two atoms"): - assertEquals("3:foo 3:bar".parse, Node.Top(SAtom("foo"), SAtom("bar"))) - test("atom with weird chars"): - assertEquals("9:(foo bar)".parse, Node.Top(SAtom("(foo bar)"))) - - test("empty atom"): - assertEquals("0:".parse, Node.Top(SAtom(""))) - test("empty list"): - assertEquals("()".parse, Node.Top(SList())) - - test("list of two atoms"): - assertEquals( - "(3:foo 3:bar)".parse, - Node.Top(SList(SAtom("foo"), SAtom("bar"))), - ) - test("list of three lists"): - assertEquals( - "((3:foo) (3:bar) ())".parse, - Node.Top( - SList( - SList(SAtom("foo")), - SList(SAtom("bar")), - SList(), - ), - ), - ) - - test("error: stray list close"): - assertEquals( - ")".parse, - Node.Top(Error("unexpected end of list", SourceMarker(")"))), - ) - - test("error: stray list close, recovery"): - assertEquals( - ") 3:foo".parse, - Node.Top( - Error("unexpected end of list", SourceMarker(")")), - SAtom("foo"), - ), - ) - - test("error: missing list terminator"): - assertEquals( - "(4: foo".parse, - Node.Top( - SList(SAtom(" foo"), Error("unexpected EOF", SourceMarker())), - ), - ) - - test("empty string literal"): - assertEquals("\"\"".parse, Node.Top(SAtom(""))) - - test("3 empty string literals with a list"): - assertEquals( - "\"\" (\"\" ) \"\"".parse, - Node.Top( - SAtom(""), - SList(SAtom("")), - SAtom(""), - ), - ) - - test("list of 3 string literals"): - assertEquals( - raw"""("foo" "bar" " ")""".parse, - Node.Top( - SList( - SAtom("foo"), - SAtom("bar"), - SAtom(" "), - ), - ), - ) - - test("string with escapes"): - assertEquals( - raw""" "\tfoo\\bar" """.parse, - Node.Top(SAtom("\tfoo\\bar")), - ) - - test("misc token atoms"): - assertEquals( - "foo bar =ping= :this /42a".parse, - Node.Top( - SAtom("foo"), - SAtom("bar"), - SAtom("=ping="), - SAtom(":this"), - SAtom("/42a"), - ), - ) diff --git a/src/sexpr/package.scala b/src/sexpr/package.scala deleted file mode 100644 index 19bffa4..0000000 --- a/src/sexpr/package.scala +++ /dev/null @@ -1,146 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.sexpr - -import cats.syntax.all.given - -import forja.* -import forja.dsl.* -import forja.source.{Source, SourceRange} -import forja.wf.WellformedDef - -object lang extends WellformedDef: - lazy val topShape: Shape = repeated(choice(SAtom, SList)) - object SList extends t(topShape) - object SAtom extends t(Atom), Token.ShowSource - -object parse: - def fromFile(path: os.Path): Node.Top = - fromSourceRange: - SourceRange.entire(Source.mapFromFile(path)) - - def fromSourceRange(sourceRange: SourceRange): Node.Top = - SExprReader(sourceRange) - -object serialize: - /* Using TailCalls here rather than cats.Eval because we are mixing imperative - * and lazy code, and I ran into a bug where cats.Eval (reasonably for its - * normal use but not here) silently memoized an effectful computation. */ - import scala.util.control.TailCalls.* - import forja.util.TailCallsUtils.* - - def toPrettyString(top: Node.All): String = - val out = java.io.ByteArrayOutputStream() - toPrettyWritable(top).writeBytesTo(out) - out.toString() - - def toCompactWritable(top: Node.All): geny.Writable = - new geny.Writable: - override def writeBytesTo(out: java.io.OutputStream): Unit = - def impl(node: Node.All): TailRec[Unit] = - (node: @unchecked) match - case top: Node.Top => - top.children.iterator - .map(impl) - .traverse(identity) - case atom @ lang.SAtom() => - val sourceRange = atom.sourceRange - out.write(sourceRange.length.toString().getBytes()) - out.write(':') - sourceRange.writeBytesTo(out) - done(()) - case list @ lang.SList(_*) => - for - () <- done(out.write('(')) - () <- list.children.iterator - .traverse(impl) - () <- done(out.write(')')) - yield () - - impl(top).result - - def toPrettyWritable(top: Node.All): geny.Writable = - new geny.Writable: - override def writeBytesTo(out: java.io.OutputStream): Unit = - var indentLevel = 0 - - def lzy[T](fn: => T): TailRec[T] = - tailcall(done(fn)) - - val nl: TailRec[Unit] = - lzy: - out.write('\n') - (0 until indentLevel).foreach(_ => out.write(' ')) - - def withIndent(fn: => TailRec[Unit]): TailRec[Unit] = - indentLevel += 2 - for - () <- tailcall(fn) - () <- done(indentLevel -= 2) - yield () - - def impl(node: Node.All): TailRec[Unit] = - (node: @unchecked) match - case top: Node.Top => - top.children.iterator - .map(impl) - .intercalate(nl) - .traverse(identity) - case atom @ lang.SAtom() - if SExprReader.canBeEncodedAsToken(atom.sourceRange) => - atom.sourceRange.writeBytesTo(out) - done(()) - case atom @ lang.SAtom() => - val sourceRange = atom.sourceRange - out.write(sourceRange.length.toString().getBytes()) - out.write(':') - sourceRange.writeBytesTo(out) - done(()) - case lang.SList() => - out.write('(') - out.write(')') - done(()) - case lang.SList(child) => - for - () <- done(out.write('(')) - () <- impl(child) - () <- done(out.write(')')) - yield () - case list @ lang.SList(_*) => - def writeChildren(iter: Iterator[Node.Child]): TailRec[Unit] = - iter - .map(impl) - .intercalate(nl) - .traverse(identity) - - out.write('(') - for - () <- withIndent: - list.children.head match - case atom @ lang.SAtom() => - for - () <- impl(atom) - () <- nl - () <- writeChildren(list.children.iterator.drop(1)) - yield () - case _ => - for - () <- nl - () <- writeChildren(list.children.iterator) - yield () - () <- done(out.write(')')) - yield () - - impl(top).result diff --git a/src/sexpr/serialize.test.scala b/src/sexpr/serialize.test.scala deleted file mode 100644 index b04ddcc..0000000 --- a/src/sexpr/serialize.test.scala +++ /dev/null @@ -1,62 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.sexpr - -import java.io.ByteArrayOutputStream - -import forja.* -import forja.sexpr.lang.{SAtom, SList} -import forja.test.newlineUtils.* - -class serializeTests extends munit.FunSuite: - extension (writable: geny.Writable) - def writeToString: String = - val out = ByteArrayOutputStream() - writable.writeBytesTo(out) - out.toString() - - extension (top: Node.Top) - def serializeCompact: String = - serialize.toCompactWritable(top).writeToString - def serializePretty: String = - serialize.toPrettyWritable(top).writeToString - - val eg1 = Node.Top(SList(SAtom("foo"), SAtom("bar"))) - - test("eg1 compact"): - assertEquals(eg1.serializeCompact, "(3:foo3:bar)") - - test("eg1 pretty"): - assertEquals( - eg1.serializePretty, - """(foo - | bar)""".stripMargin.ensureLf, - ) - - val eg2 = Node.Top( - SList(SList(), SList(), SList(SList())), - ) - - test("nested lists compact"): - assertEquals(eg2.serializeCompact, "(()()(()))") - - test("nested lists pretty"): - assertEquals( - eg2.serializePretty, - """( - | () - | () - | (()))""".stripMargin.ensureLf, - ) diff --git a/src/source/Reader.scala b/src/source/Reader.scala deleted file mode 100644 index be5507f..0000000 --- a/src/source/Reader.scala +++ /dev/null @@ -1,230 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.source - -import java.nio.CharBuffer -import java.nio.charset.{Charset, StandardCharsets} - -import cats.syntax.all.given - -import forja.* -import forja.dsl.* -import forja.wf.Wellformed - -import scala.annotation.targetName - -trait Reader: - def wellformed: Wellformed - - protected def traceLimit: Int = -1 - protected def rules: Manip[SourceRange] - - final def apply(sourceRange: SourceRange): Node.Top = - import dsl.* - val top = Node.Top() - - val manip = - initNode(top): - Reader.srcRef.init(sourceRange): - Reader.matchedRef.init(sourceRange.take(0)): - rules.flatMap: _ => - if top.hasErrors - then Manip.unit - else wellformed.markErrorsPass - - manip.perform() - top - -object Reader: - import dsl.* - - object srcRef extends Manip.Ref[SourceRange] - object matchedRef extends Manip.Ref[SourceRange] - - // TODO: other ways to access matched ranges - - def consumeMatch[T](fn: SourceRange => Manip[T]): Manip[T] = - matchedRef.get.lookahead.flatMap: m => - matchedRef.updated(m => m.drop(m.length))(fn(m)) - - def dropMatch[T](manip: Manip[T]): Manip[T] = - matchedRef.updated(m => m.drop(m.length))(manip) - - def extendThisNodeWithMatch[T](using DebugInfo)(manip: Manip[T]): Manip[T] = - consumeMatch: m => - extendThisNode(m) - *> manip - - def extendThisNode(using DebugInfo)(m: SourceRange): Manip[Unit] = - getNode.lookahead.flatMap: - case node: Node => - effect(node.extendLocation(m)) - case top: Node.Top => - // if top is an extend target, ignore it. - Manip.pure(()) - case _ => backtrack - - object bytes: - private def advancingRefs[T](manip: Manip[T]): Manip[T] = - srcRef.updated(_.tail): - matchedRef.updated(_.extendRight): - manip - - final class selecting[T] private (private val cell: selecting.Cell[T]) - extends AnyVal: - @targetName("onOneOfChars") - def onOneOf(chars: IterableOnce[Char])(manip: => Manip[T]): selecting[T] = - onOneOf(chars.iterator.map(_.toByte))(manip) - - def on(char: Char)(manip: => Manip[T]): selecting[T] = - on(char.toByte)(manip) - - def onOneOf(bytes: IterableOnce[Byte])(manip: => Manip[T]): selecting[T] = - lazy val impl = manip - bytes.iterator.foldLeft(this)(_.on(_)(impl)) - - def on(byte: Byte)(manip: => Manip[T]): selecting[T] = - new selecting(cell.add(Iterator.single(byte), defer(manip))) - - def onSeq( - chars: CharSequence, - encoding: Charset = StandardCharsets.UTF_8, - )(manip: => Manip[T]): selecting[T] = - val bytes = SourceRange.entire( - Source.fromByteBuffer(encoding.encode(CharBuffer.wrap(chars))), - ) - new selecting(cell.add(bytes.iterator, defer(manip))) - - def fallback(using DebugInfo)(manip: => Manip[T]): Manip[T] = - val fallbackImpl = defer(manip) - srcRef.get.lookahead.flatMap: src => - val (matched, manip) = cell.lookup(src, fallbackImpl) - commit: - srcRef.updated(_.drop(matched.length)): - matchedRef.updated(_ <+> matched): - manip - - object selecting: - def apply[T]: selecting[T] = - new selecting[T](Cell.Branch(Map.empty, None)) - - enum Cell[T]: - case Branch(map: Map[Byte, Cell[T]], fallbackOpt: Option[Manip[T]]) - case Leaf(manip: Manip[T]) - - def lookup( - src: SourceRange, - fallback: Manip[T], - ): (SourceRange, Manip[T]) = - @scala.annotation.tailrec - def impl( - cell: Cell[T], - src: SourceRange, - matched: SourceRange, - otherwise: (SourceRange, Manip[T]), - ): (SourceRange, Manip[T]) = - cell match - case Branch(map, fallbackOpt) => - def noMatch: (SourceRange, Manip[T]) = - fallbackOpt.fold(otherwise)((matched, _)) - - if src.isEmpty - then noMatch - else - map.get(src.head) match - case None => noMatch - case Some(cell) => - impl(cell, src.tail, matched.extendRight, noMatch) - case Leaf(manip) => (matched, manip) - - impl(this, src, src.emptyAtOffset, (src.emptyAtOffset, fallback)) - - def add(seq: Iterator[Byte], manip: Manip[T]): Cell[T] = - if seq.hasNext - then - val seqHead = seq.next() - this match - case Branch(map, fallback) => - Branch( - map.updatedWith(seqHead)( - _.orElse(Some(Branch[T](Map.empty, None))) - .map(_.add(seq, manip)), - ), - fallback, - ) - case Leaf(existingManip) => - Branch( - Map(seqHead -> Branch(Map.empty, None).add(seq, manip)), - Some(existingManip), - ) - else - this match - case Branch(map, fallback) => - Branch(map, Some(fallback.fold(manip)(_ | manip))) - case Leaf(manip) => Leaf(manip | manip) - - def selectManyLike[T](using - DebugInfo, - )(bytes: Set[Byte])( - manip: Manip[T], - ): Manip[T] = - lazy val impl: Manip[T] = - srcRef.get.lookahead.flatMap: src => - if src.nonEmpty && bytes(src.head) - then advancingRefs(impl) - else Manip.Backtrack(summon[DebugInfo]) - | manip - - impl - - @targetName("selectManyLikeChars") - def selectManyLike[T](using - DebugInfo, - )(chars: Set[Char])( - manip: Manip[T], - ): Manip[T] = - selectManyLike(chars.map(_.toByte))(manip) - - def selectSeq[T](using DebugInfo)(str: String)(manip: Manip[T]): Manip[T] = - val bytes = str.getBytes() - - def impl(idx: Int): Manip[T] = - if idx < bytes.length - then - srcRef.get.lookahead.flatMap: src => - if src.nonEmpty && src.head == bytes(idx) - then advancingRefs(impl(idx + 1)) - else backtrack - else manip - - impl(0) - - def selectCount[T](using DebugInfo)(count: Int)(manip: Manip[T]): Manip[T] = - srcRef.get.lookahead.flatMap: src => - if count <= src.length - then - srcRef.updated(_.drop(count)): - matchedRef.updated(_.extendRightBy(count)): - manip - else Manip.Backtrack(summon[DebugInfo]) - - def selectOne[T](using DebugInfo)(manip: Manip[T]): Manip[T] = - srcRef.get.lookahead.flatMap: src => - if src.nonEmpty - then advancingRefs(manip) - else backtrack - - def getSrc: Manip[SourceRange] = - srcRef.get diff --git a/src/source/Source.scala b/src/source/Source.scala deleted file mode 100644 index 3e91360..0000000 --- a/src/source/Source.scala +++ /dev/null @@ -1,89 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.source - -import java.nio.ByteBuffer -import java.nio.channels.FileChannel -import java.nio.channels.FileChannel.MapMode -import java.nio.charset.StandardCharsets - -import scala.util.Using - -trait Source: - def origin: Option[os.Path] - def byteBuffer: ByteBuffer - - object lines: - val nlOffsets: IArray[Int] = - IArray.from: - SourceRange - .entire(Source.this) - .iterator - .zipWithIndex - .collect: - case ('\n', idx) => idx - - def lineColAtOffset(offset: Int): (Int, Int) = - import scala.collection.Searching.* - val lineIdx = - nlOffsets.search(offset) match - case Found(foundIndex) => foundIndex - case InsertionPoint(insertionPoint) => insertionPoint - val colIdx = - if lineIdx == 0 - then offset - else offset - 1 - nlOffsets(lineIdx - 1) - - (lineIdx, colIdx) - - def lineStartOffset(lineIdx: Int): Int = - if lineIdx == 0 - then 0 - else if lineIdx - 1 == nlOffsets.length - then SourceRange.entire(Source.this).length - else nlOffsets(lineIdx - 1) + 1 -end Source - -object Source: - object empty extends Source: - def origin: Option[os.Path] = None - val byteBuffer: ByteBuffer = - ByteBuffer.wrap(IArray.emptyByteIArray.unsafeArray) - - def fromString(string: String): Source = - StringSource(string) - - def fromByteBuffer(byteBuffer: ByteBuffer): Source = - ByteBufferSource(None, byteBuffer) - - def fromWritable(writable: geny.Writable): Source = - val out = java.io.ByteArrayOutputStream() - writable.writeBytesTo(out) - fromByteBuffer(ByteBuffer.wrap(out.toByteArray())) - - def mapFromFile(path: os.Path): Source = - Using.resource(FileChannel.open(path.toNIO)): channel => - val mappedBuf = channel.map(MapMode.READ_ONLY, 0, channel.size()) - ByteBufferSource(Some(path), mappedBuf) - - final class StringSource(val string: String) extends Source: - def origin: Option[os.Path] = None - lazy val byteBuffer: ByteBuffer = - StandardCharsets.UTF_8.encode(string) - - final class ByteBufferSource( - val origin: Option[os.Path], - override val byteBuffer: ByteBuffer, - ) extends Source diff --git a/src/source/SourceRange.scala b/src/source/SourceRange.scala deleted file mode 100644 index 04c483b..0000000 --- a/src/source/SourceRange.scala +++ /dev/null @@ -1,283 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.source - -import java.nio.ByteBuffer -import java.nio.channels.Channels -import java.nio.charset.{Charset, StandardCharsets} - -import scala.collection.immutable.IndexedSeq -import scala.collection.mutable - -final class SourceRange( - val source: Source, - val offset: Int, - val length: Int, -) extends IndexedSeq[Byte], - geny.Writable: - require( - 0 <= offset && 0 <= length && offset + length <= source.byteBuffer.limit(), - s"invalid offset = $offset, length = $length into source with limit ${source.byteBuffer.limit()}", - ) - - override def toString(): String = - s"SourceRange(${{ decodeString() }})" - - def apply(i: Int): Byte = - source.byteBuffer - .get(offset + i) - - def byteBuffer(): ByteBuffer = - source.byteBuffer - .duplicate() - .position(offset) - .limit(offset + length) - .slice() - - def decodeString(charset: Charset = StandardCharsets.UTF_8): String = - charset.decode(byteBuffer()).toString() - - def writeBytesTo(out: java.io.OutputStream): Unit = - val channel = Channels.newChannel(out) - val bufSlice = this.byteBuffer() - // It seems .write will make a significant effort to write all bytes, - // but just to be safe let's retry if it writes less. - var count = 0 - while count < length - do count += channel.write(bufSlice) - - def emptyAtOffset: SourceRange = - dropRight(length) - - def extendLeftBy(count: Int): SourceRange = - SourceRange(source, offset - count, length) - - def extendLeft: SourceRange = - extendLeftBy(1) - - def extendRightBy(count: Int): SourceRange = - SourceRange(source, offset, length + count) - - def extendRight: SourceRange = - extendRightBy(1) - - override def slice(from: Int, until: Int): SourceRange = - require( - 0 <= from && from <= until && until <= length, - s"[$from, $until) is outside the range [$offset,${offset + length})", - ) - SourceRange(source, offset + from, until - from) - - override def take(n: Int): SourceRange = - if n >= length - then this - else slice(0, n) - - override def takeWhile(p: Byte => Boolean): SourceRange = - take(iterator.takeWhile(p).size) - - override def drop(n: Int): SourceRange = - if n <= length - then slice(n, length) - else emptyAtOffset - - override def dropRight(n: Int): SourceRange = - if n <= length - then slice(0, length - n) - else emptyAtOffset - - override def init: SourceRange = - dropRight(1) - - override def tail: SourceRange = - drop(1) - - @scala.annotation.targetName("combine") - def <+>(other: SourceRange): SourceRange = - if source eq Source.empty - then other - else if other.source eq Source.empty - then this - else if source eq other.source - then - // convert to [start,end) spans so we can do min/max merge. - // Doesn't work on lengths because those are relative to offset, - // but start / end are independent. - val (startL, endL) = (offset, offset + length) - val (startR, endR) = (other.offset, other.offset + other.length) - val (startC, endC) = (startL.min(startR), endL.max(endR)) - SourceRange(source, startC, endC - startC) - else this - - def presentationStringShort: String = - val builder = StringBuilder() - builder ++= source.origin.map(_.toString).getOrElse("") - builder += ':' - - val (lineIdx, colIdx) = source.lines.lineColAtOffset(offset) - builder.append(lineIdx + 1) - builder += ':' - builder.append(colIdx + 1) - - if length > 0 - then - builder += '-' - val (lineIdx2, colIdx2) = source.lines.lineColAtOffset(offset + length) - if lineIdx == lineIdx2 - then builder.append(colIdx2 + 1) - else - builder.append(lineIdx2 + 1) - builder += ':' - builder.append(colIdx2 + 1) - - builder.result() - - def presentationStringLong: String = - s"$presentationStringShort:\n$showInSource" - - def showInSource: String = - extension (it: Iterator[Byte]) - // When we print a line, we don't want the trailing newline. - // We also don't want platform specific variants like \r. - def removeCrLf: Iterator[Byte] = - it.filter(ch => ch != '\r' && ch != '\n') - - def decodeString: String = - String(it.toArray, StandardCharsets.UTF_8) - - val entireSource = SourceRange.entire(source) - val builder = StringBuilder() - val (line1Idx, startCol) = source.lines.lineColAtOffset(offset) - val (line2Idx, endCol) = source.lines.lineColAtOffset(offset + length) - - val line1Start = source.lines.lineStartOffset(line1Idx) - val line1AfterEnd = source.lines.lineStartOffset(line1Idx + 1) - - if line1Idx == line2Idx - then - val lineFrag = entireSource.slice(line1Start, line1AfterEnd) - builder.addAll(lineFrag.iterator.removeCrLf.decodeString) - builder += '\n' - lineFrag - .slice(0, startCol) - .iterator - .removeCrLf - .foreach(_ => builder += ' ') - if startCol == endCol - then builder += '^' - else - lineFrag - .slice(startCol, endCol) - .iterator - .removeCrLf - .foreach(_ => builder += '^') - else - val line1Frag = entireSource.slice(line1Start, line1AfterEnd) - line1Frag - .slice(0, startCol) - .iterator - .removeCrLf - .foreach(_ => builder += ' ') - val line1EndCol = line1AfterEnd - line1Start - line1Frag - .slice(startCol, line1EndCol) - .iterator - .removeCrLf - .foreach(_ => builder += 'v') - builder += '\n' - builder.addAll(line1Frag.iterator.removeCrLf.decodeString) - - if line2Idx - line1Idx > 1 - then builder.addAll("\n...\n") - else builder += '\n' - - val line2Start = source.lines.lineStartOffset(line2Idx) - val line2AfterEnd = source.lines.lineStartOffset(line2Idx + 1) - val line2Frag = entireSource.slice(line2Start, line2AfterEnd) - builder.addAll(line2Frag.iterator.removeCrLf.decodeString) - builder += '\n' - line2Frag - .slice(0, endCol) - .iterator - .removeCrLf - .foreach(_ => builder += '^') - - builder.result() - -object SourceRange: - final class Builder extends mutable.Builder[Byte, SourceRange]: - private val arrayBuilder = Array.newBuilder[Byte] - def addOne(elem: Byte): this.type = - arrayBuilder.addOne(elem) - this - def clear(): Unit = arrayBuilder.clear() - def result(): SourceRange = - SourceRange.entire( - Source.fromByteBuffer(ByteBuffer.wrap(arrayBuilder.result())), - ) - - def newBuilder: Builder = Builder() - - def entire(source: Source): SourceRange = - SourceRange(source, 0, source.byteBuffer.limit()) - - extension (ctx: StringContext) - def src: srcImpl = - srcImpl(ctx) - - final class srcImpl(val ctx: StringContext) extends AnyVal: - def unapplySeq(sourceRange: SourceRange): Option[Seq[SourceRange]] = - val parts = ctx.parts - assert(parts.nonEmpty) - - extension (part: String) - def bytes: SourceRange = - SourceRange.entire: - Source.fromByteBuffer: - StandardCharsets.UTF_8.encode: - StringContext.processEscapes(part) - - val firstPart = parts.head.bytes - - if !sourceRange.startsWith(firstPart) - then return None - - var currRange = sourceRange.drop(firstPart.length) - val matchedParts = mutable.ListBuffer.empty[SourceRange] - val didMatchFail = - parts.iterator - .drop(1) - .map(_.bytes) - .map: part => - if part.isEmpty - then - val idx = currRange.length - matchedParts += currRange - currRange = currRange.drop(currRange.length) - idx - else - val idx = currRange.indexOfSlice(part) - if idx != -1 - then - matchedParts += currRange.take(idx) - currRange = currRange.drop(idx) - - idx - .contains(-1) - || currRange.nonEmpty - - if didMatchFail - then None - else Some(matchedParts.result()) diff --git a/src/source/SourceRange.test.scala b/src/source/SourceRange.test.scala deleted file mode 100644 index 59b7133..0000000 --- a/src/source/SourceRange.test.scala +++ /dev/null @@ -1,165 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.source - -import java.nio.charset.StandardCharsets - -import forja.test.newlineUtils.* - -import scala.collection.StringOps - -class SourceRangeTests extends munit.FunSuite: - val ipsum = - """Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor - |incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud - |exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure - |dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. - |Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit - |anim id est laborum.""".stripMargin - - extension (src: SourceRange) - def takeLine: SourceRange = - src.take(src.indexOf('\n')) - def dropLine: SourceRange = - src.drop(src.indexOf('\n') + 1) - def lastLine: SourceRange = - src.drop(src.lastIndexOf('\n') + 1) - def startingFrom(str: String): SourceRange = - val bytes = str.getBytes() - src.drop(src.indexOfSlice(bytes)) - def upTo(str: String): SourceRange = - val bytes = str.getBytes() - src.take(src.indexOfSlice(bytes) + bytes.size) - - private trait MarginStripper: - extension (str: String) def stripMargin: String - - test("sanity: crlf and lf normalization"): - assertEquals("a\nb\nc".ensureLf, "a\nb\nc") - assertEquals("a\r\nb\r\nc".ensureLf, "a\nb\nc") - assertEquals("a\nb\nc".ensureCrLf, "a\r\nb\r\nc") - assertEquals("a\r\nb\r\nc".ensureCrLf, "a\r\nb\r\nc") - - locally: - given MarginStripper with - extension (str: String) - def stripMargin: String = - StringOps(str).stripMargin.ensureLf - mkTests("[lf]", ipsum.ensureLf) - - locally: - given MarginStripper with - extension (str: String) - def stripMargin: String = - StringOps( - str, - ).stripMargin.ensureLf // output is standardized to use \n only - mkTests("[crlf]", ipsum.ensureCrLf) - - extension (str: String) - // This helps debug weird control character issues by making them explicit. - private def toHexView: String = - val bytes = str.getBytes(StandardCharsets.UTF_8) - bytes - .sliding(8, step = 8) - .map: octet => - val chars = octet - .map: - case '\n' => "\\n" - case '\r' => "\\r" - case ch => s" ${ch.toChar}" - .mkString - val hexs = octet.map(b => f"$b%02X").mkString(" ") - s"${chars.padTo(16, ' ')} | ${hexs}" - .mkString("\n") - - private def mkTests(mode: String, ipsum: String)(using MarginStripper): Unit = - val ipsumSrc = SourceRange.entire(Source.fromString(ipsum)) - - def assertHexEquals(expectedStr: String, actualStr: String)(using - munit.Location, - ): Unit = - assertEquals( - s"$expectedStr\n\n${expectedStr.toHexView}", - s"$actualStr\n\n${actualStr.toHexView}", - ) - - test(s"highlight entire first line $mode"): - assertHexEquals( - ipsumSrc.takeLine.showInSource, - """Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor - |^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^""".stripMargin, - ) - - test(s"highlight entire second line $mode"): - assertHexEquals( - ipsumSrc.dropLine.takeLine.showInSource, - """incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud - |^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^""".stripMargin, - ) - - test(s"highlight last line $mode"): - assertHexEquals( - ipsumSrc.lastLine.showInSource, - """anim id est laborum. - |^^^^^^^^^^^^^^^^^^^^""".stripMargin, - ) - - test(s"highlight second 2 lines $mode"): - assertHexEquals( - (ipsumSrc.dropLine.takeLine <+> ipsumSrc.dropLine.dropLine.takeLine).showInSource, - """vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv - |incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud - |exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure - |^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^""".stripMargin, - ) - - test(s"highlight second 3 lines $mode"): - assertHexEquals( - (ipsumSrc.dropLine.takeLine <+> ipsumSrc.dropLine.dropLine.dropLine.takeLine).showInSource, - """vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv - |incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud - |... - |dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. - |^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^""".stripMargin, - ) - - test(s"highlight all but 1st line $mode"): - assertHexEquals( - ipsumSrc.dropLine.showInSource, - """vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv - |incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud - |... - |anim id est laborum. - |^^^^^^^^^^^^^^^^^^^^""".stripMargin, - ) - - test(s"highlight amet to nisi $mode"): - assertHexEquals( - ipsumSrc.startingFrom("amet").upTo("nisi").showInSource, - """ vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv - |Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor - |... - |exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure - |^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^""".stripMargin, - ) - - test(s"highlight just nisi $mode"): - assertHexEquals( - ipsumSrc.startingFrom("nisi").take("nisi".size).showInSource, - """exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure - | ^^^^""".stripMargin, - ) - end mkTests diff --git a/src/test/WithClonedCorpus.scala b/src/test/WithClonedCorpus.scala deleted file mode 100644 index e935bf3..0000000 --- a/src/test/WithClonedCorpus.scala +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.test - -transparent trait WithClonedCorpus: - self: munit.FunSuite => - def repoURL: String - def intoName: String - - def isCorpusFile(file: os.Path): Boolean - - def clonesDir: os.Path = os.pwd / ".clones" - private def checkoutFolder = clonesDir / intoName - - def testWithCorpusFile(using munit.Location)(fn: os.Path => Unit): Unit = - if !os.exists(checkoutFolder) - then - os.makeDir.all(clonesDir) - os.proc("git", "clone", "--recurse-submodules", repoURL, checkoutFolder) - .call() - else os.proc("git", "pull").call(cwd = checkoutFolder) - - var foundAnything = false - - os.walk - .stream(checkoutFolder) - .filter(isCorpusFile) - // .take(3) // TODO: make unnecessary - .foreach: corpusFile => - foundAnything = true - test(corpusFile.relativeTo(clonesDir).toString)(fn(corpusFile)) - - test("sanity") { - assert( - foundAnything, - s"must find at least one test file in $checkoutFolder", - ) - } - end testWithCorpusFile diff --git a/src/test/WithTLACorpus.scala b/src/test/WithTLACorpus.scala deleted file mode 100644 index a3759ab..0000000 --- a/src/test/WithTLACorpus.scala +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.test - -transparent trait WithTLACorpus extends WithClonedCorpus: - self: munit.FunSuite => - val repoURL: String = "https://github.com/tlaplus/Examples.git" - val intoName: String = "Examples" - - def isCorpusFile(file: os.Path): Boolean = file.last.endsWith(".tla") diff --git a/src/test/newlineUtils.scala b/src/test/newlineUtils.scala deleted file mode 100644 index 1b21615..0000000 --- a/src/test/newlineUtils.scala +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.test - -object newlineUtils: - extension (str: String) - def ensureLf: String = - str.split("\r\n").mkString("\n") - def ensureCrLf: String = - str.ensureLf.split("\n").mkString("\r\n") diff --git a/src/util/ById.scala b/src/util/ById.scala deleted file mode 100644 index c1678ae..0000000 --- a/src/util/ById.scala +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.util - -final class ById[T <: AnyRef](val ref: T) extends Equals: - override def canEqual(that: Any): Boolean = - that.isInstanceOf[ById[?]] - - override def equals(that: Any): Boolean = - that match - case that: ById[t] => ref eq that.ref - case _ => false - - override def hashCode(): Int = - System.identityHashCode(ref) - - override def toString(): String = - s"ById(@${hashCode()} ${pprint.apply(ref)})" -end ById - -object ById: - final class unapplyImpl[T <: AnyRef](val id: ById[T]) extends AnyVal: - def isEmpty: false = false - def get: T = id.ref - - def unapply[T <: AnyRef](byId: ById[T]): unapplyImpl[T] = unapplyImpl(byId) -end ById diff --git a/src/util/DebugInfo.scala b/src/util/DebugInfo.scala deleted file mode 100644 index 1347a24..0000000 --- a/src/util/DebugInfo.scala +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja - -import sourcecode.* - -final case class DebugInfo( - file: String, - fileName: String, - line: Int, - outerOpt: Option[DebugInfo] = None, -) extends DebugInfo.Ctx: - override def toString(): String = - outerOpt match - case None => - s"$file:$line" - case Some(outer) => - s"$file:$line in $outer" - -object DebugInfo: - import scala.compiletime.{summonFrom, summonInline} - - sealed abstract class Ctx - - inline given instance(using - file: File, - fileName: FileName, - line: Line, - ): DebugInfo = - summonFrom: - case ctx: Ctx => - ctx match - case ctx: DebugInfo - if (file.value, fileName.value, line.value) != ( - ctx.file, - ctx.fileName, - ctx.line, - ) => - DebugInfo(file.value, fileName.value, line.value, Some(ctx)) - case ctx: DebugInfo => ctx - case _ => - DebugInfo(file.value, fileName.value, line.value) - - inline def apply()(using debugInfo: DebugInfo): DebugInfo = debugInfo - - /* Put this in implicit scope (as an inline given) when you want to be sure - * you're not accidentally summoning DebugInfo. - * It will flag all such mistakes with a compile-time error. */ - inline def poison: DebugInfo = - scala.compiletime.error("implementation bug: used a poison DebugInfo value") - - /* If you have a nested scope where you used the above but actually want - * DebugInfo to work again, shadow the poison given with another inline given - * that expands to this. */ - inline def notPoison: DebugInfo = - instance(using - file = summonInline[File], - fileName = summonInline[FileName], - line = summonInline[Line], - ) diff --git a/src/util/FuzzTestSuite.test.scala b/src/util/FuzzTestSuite.test.scala deleted file mode 100644 index 1881235..0000000 --- a/src/util/FuzzTestSuite.test.scala +++ /dev/null @@ -1,199 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.util - -import java.util.Random - -import edu.berkeley.cs.jqf.fuzz.ei.ZestGuidance -import edu.berkeley.cs.jqf.fuzz.junit.GuidedFuzzing -import scala.concurrent.duration.Duration -import scala.jdk.CollectionConverters.given -import scala.quoted.{Expr, Quotes, Type} - -trait FuzzTestSuite extends munit.FunSuite: - override def munitTimeout: Duration = Duration("5m") - - def trialLimit: Option[Long] = None - def fuzzTimeout: Duration = Duration("10s") - - protected inline def fuzzTestMethod(inline method: Any)(using - loc: munit.Location, - ): Unit = - ${ FuzzTestSuite.fuzzTestMethodImpl('this, 'method, 'loc) } - - protected final def doFuzzTest(className: String, methodName: String)(using - munit.Location, - ): Unit = - val buildProc = os.proc( - "scala-cli", - "compile", - "--test", - ".", - ) - // println(s"$$ ${buildProc.commandChunks.mkString(" ")}") - buildProc.call(cwd = os.pwd, mergeErrIntoOut = true) - - val ownClasspath = System.getProperty("java.class.path").split(":") - val instrumentJar = ownClasspath.find(_.contains("jqf-instrument")).get - val asmJar = ownClasspath.find(_.contains("/asm")).get - val classPathWithoutInstrument = ownClasspath - .filterNot(_.contains("jqf-instrument")) - .filterNot(_.contains("/asm")) - val runProc = os.proc( - "java", - s"-Xbootclasspath/a:$instrumentJar:$asmJar", - s"-javaagent:$instrumentJar", - // s"-Djanala.conf=${os.pwd / "janala.conf"}", - "-cp", - classPathWithoutInstrument.mkString(":"), - FuzzTestSuiteMain.getClass().getCanonicalName().stripSuffix("$"), - className, - methodName, - "--timeout", - fuzzTimeout.toMillis, - trialLimit.fold[List[os.Shellable]](Nil)(limit => - List("--trial-limit", limit), - ), - ) - // println(s"$$ ${runProc.commandChunks.mkString(" ")}") - val result = runProc.call( - cwd = os.pwd, - check = false, - mergeErrIntoOut = true, - // stdin = os.Inherit, - // stdout = os.Inherit, - // stderr = os.Inherit, - ) - if result.exitCode != 0 - then fail(result.toString()) -end FuzzTestSuite - -object FuzzTestSuite: - def fuzzTestMethodImpl[Self <: FuzzTestSuite: Type]( - selfRef: Expr[Self], - methodRef: Expr[Any], - sourceLoc: Expr[munit.Location], - )(using q: Quotes): Expr[Unit] = - import q.reflect.* - - val methodCallTerm = methodRef match - case '{ (arg1: t1) => $fn(arg1): Unit } => - Expr.betaReduce('{ $fn(???) }).asTerm - case '{ (arg1: t1, arg2: t2) => $fn(arg1, arg2): Unit } => - Expr.betaReduce('{ $fn(???, ???) }).asTerm - case '{ (arg1: t1, arg2: t2) => $fn(arg1, arg2): Unit } => - Expr.betaReduce('{ $fn(???, ???) }).asTerm - case '{ (arg1: t1, arg2: t2, arg3: t3) => $fn(arg1, arg2, arg3): Unit } => - Expr.betaReduce('{ $fn(???, ???, ???) }).asTerm - case '{ (arg1: t1, arg2: t2, arg3: t3, arg4: t4) => - $fn(arg1, arg2, arg3, arg4): Unit - } => - Expr.betaReduce('{ $fn(???, ???, ???, ???) }).asTerm - case '{ (arg1: t1, arg2: t2, arg3: t3, arg4: t4, arg5: t5) => - $fn(arg1, arg2, arg3, arg4, arg5): Unit - } => - Expr.betaReduce('{ $fn(???, ???, ???, ???, ???) }).asTerm - case '{ (arg1: t1, arg2: t2, arg3: t3, arg4: t4, arg5: t5, arg6: t6) => - $fn(arg1, arg2, arg3, arg4, arg5, arg6): Unit - } => - Expr.betaReduce('{ $fn(???, ???, ???, ???, ???, ???) }).asTerm - case _ => - report.errorAndAbort( - s"could not identify lambda ${methodRef.show} (note: we only support up to arity 6)", - ) - - object StripIrrelevant: - def unapply(term: Term): Some[Term] = - term match - case Inlined(_, _, StripIrrelevant(term)) => Some(term) - case Block(_, StripIrrelevant(term)) => Some(term) - case term => Some(term) - - methodCallTerm match - case StripIrrelevant(Apply(Select(classExpr, methodName), _)) => - classExpr.tpe.classSymbol match - case None => - report.errorAndAbort( - s"can't get the class type of ${classExpr.show}", - ) - case Some(classSym) => - val className = classSym.fullName - '{ - given munit.Location = $sourceLoc - $selfRef.test(${ Expr(s"fuzzTest $className#$methodName") }): - $selfRef.doFuzzTest(${ Expr(className) }, ${ Expr(methodName) }) - } - case _ => - report.errorAndAbort( - s"could not find method call in ${methodCallTerm.show}", - ) - end fuzzTestMethodImpl -end FuzzTestSuite - -object FuzzTestSuiteMain: - def main(argv: Array[String]): Unit = - var className: String = "" - var methodName: String = "" - var trialLimit: java.lang.Long | Null = null - var timeout: java.time.Duration | Null = null - - val parser = new scopt.OptionParser[Unit]("fuzz_test"): - arg[String]("class-name") - .required() - .foreach(className = _) - .text("fully qualified name of class to fuzz") - arg[String]("method-name") - .required() - .foreach(methodName = _) - .text("name of method within the class to fuzz") - opt[Long]("trial-limit") - .optional() - .foreach(trialLimit = _) - .text("maximum number of trials (default is infinite)") - opt[Long]("timeout") - .optional() - .foreach(millis => timeout = java.time.Duration.ofMillis(millis)) - .text("trial timeout in ms (default is none)") - - if parser.parse(argv, ()).isDefined - then - val outputDir = os.pwd / ".fuzz_output" / className / methodName - if !os.exists(outputDir) - then os.makeDir.all(outputDir) - - val guidance = new ZestGuidance( - methodName, // Name of the test method - timeout, - trialLimit, // Trial limit (missing if null) - outputDir.toIO, // Output directory for results - new Random(), // Random number generator - ) - println(s"fuzzing $className#$methodName") - - val result = GuidedFuzzing.run( - className, - methodName, - Thread.currentThread().getContextClassLoader(), - guidance, - System.out, - ) - - if !result.wasSuccessful() - then - println(s"see $outputDir for details") - sys.exit(1) - else sys.exit(2) - end main -end FuzzTestSuiteMain diff --git a/src/util/HasInstanceArray.scala b/src/util/HasInstanceArray.scala deleted file mode 100644 index 92a93f1..0000000 --- a/src/util/HasInstanceArray.scala +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.util - -transparent trait HasInstanceArray[T](using HasInstanceArray.Instances[T]): - val instances: IArray[T] = summon[HasInstanceArray.Instances[T]].array - -object HasInstanceArray: - final class Instances[T](val array: IArray[T]) extends AnyVal - - inline given summonInstances[T: reflect.ClassTag](using - mirror: deriving.Mirror.SumOf[T], - ): Instances[T] = - given [S <: Singleton](using v: ValueOf[S]): S = v.value - Instances: - compiletime - .summonAll[mirror.MirroredElemTypes] - .toIArray - .map(_.asInstanceOf[T]) diff --git a/src/util/Named.scala b/src/util/Named.scala deleted file mode 100644 index cc3bd87..0000000 --- a/src/util/Named.scala +++ /dev/null @@ -1,61 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.util - -import scala.annotation.constructorOnly -import scala.quoted.* - -transparent trait Named(using ownName: Named.OwnName @constructorOnly): - final val name: String = ownName.segments.mkString(".") - final val nameSegments: List[String] = ownName.segments -end Named - -object Named: - final class OwnName(val segments: List[String]) extends AnyVal - - private def findOwnNameImpl(using quotes: Quotes): Expr[OwnName] = - import quotes.reflect.* - - @scala.annotation.tailrec - def stripMacroConstructorStuff(sym: Symbol): TypeRepr = - if sym.flags.is(Flags.Macro) || sym.isClassConstructor - then stripMacroConstructorStuff(sym.owner) - else if sym.isClassDef && sym.companionModule.exists - then - val symTermRef = sym.companionModule.termRef - if symTermRef.isSingleton - then symTermRef - else report.errorAndAbort(s"${symTermRef.show} is not a singleton") - else - report.errorAndAbort( - s"${sym.fullName} is not a class/object, or has no companion object", - ) - - def getNameSegments(sym: Symbol): List[String] = - if sym == defn.RootClass - then Nil - /* Not sure about stripSuffix, but it seems to work well, I guess until a - * package has non-alphanumeric chars in its name. - * Not sure how else I might try to access the name without the $. */ - else sym.name.stripSuffix("$") :: getNameSegments(sym.owner) - - val theType = stripMacroConstructorStuff(Symbol.spliceOwner) - val nameSegments = getNameSegments(theType.typeSymbol).reverse - - '{ OwnName(${ Expr(nameSegments) }) } - - inline given findOwnName: OwnName = - ${ findOwnNameImpl } -end Named diff --git a/src/util/SymbolicMapFactory.scala b/src/util/SymbolicMapFactory.scala deleted file mode 100644 index 115dc97..0000000 --- a/src/util/SymbolicMapFactory.scala +++ /dev/null @@ -1,106 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.util - -import java.lang.ref.{ReferenceQueue, WeakReference} -import java.util.concurrent.atomic.AtomicInteger - -import scala.reflect.ClassTag - -abstract class SymbolicMapFactory: - trait Mapped: - val mapIdx: Int = - val idx = Mapped.refQueue.poll() match - case ref: Mapped.MappedRef => - Mapped.refKeepAlive.remove(ref) - ref.mapIdx - case null => Mapped.nextIdx.incrementAndGet() - - Mapped.refKeepAlive(Mapped.MappedRef(this, idx)) = () - idx - end mapIdx - end Mapped - - object Mapped: - private final class MappedRef(mapped: Mapped, val mapIdx: Int) - extends WeakReference[Mapped](mapped, refQueue) - private val refQueue = ReferenceQueue[Mapped]() - private val refKeepAlive = - scala.collection.concurrent.TrieMap[MappedRef, Unit]() - private var nextIdx = AtomicInteger() - end Mapped - - final class Map[@specialized T] private ( - private var array: Array[T], - default: => T, - )(using val classTag: ClassTag[T]): - private def defaultFn: T = default - - def apply(key: Mapped): T = - val idx = key.mapIdx - if idx < array.length - then array(idx) - else default - - def copyFrom(other: Map[T]): Unit = - ensureCapacity(other.array.length - 1 `max` 0) - Array.copy(other.array, 0, array, 0, other.array.length) - var idx = other.array.length - while idx < array.length do - array(idx) = default - idx += 1 - - private def ensureCapacity(idx: Int): Unit = - val oldLength = array.length - var length = oldLength - if length == 0 then length += 1 - while length <= idx do length *= 2 - assert(length >= 1) - if length != array.length - then - array = Array.copyOf(array, length) - var i = oldLength - while i < length do - array(i) = default - i += 1 - - def update(key: Mapped, value: T): Unit = - val idx = key.mapIdx - ensureCapacity(idx) - array(idx) = value - - def remove(key: Mapped): Unit = - val idx = key.mapIdx - if idx < array.length - then array(idx) = default - - def updated(key: Mapped, value: T): Map[T] = - val next = Map(array.clone(), default) - next.update(key, value) - next - - def removed(key: Mapped): Map[T] = - val next = Map(array.clone(), default) - next.remove(key) - next - end Map - - object Map: - def empty[T: ClassTag](default: => T): Map[T] = Map(Array.empty, default) - end Map -end SymbolicMapFactory - -object TokenMapFactory extends SymbolicMapFactory -export TokenMapFactory.Map as TokenMap diff --git a/src/util/TailCallsUtils.scala b/src/util/TailCallsUtils.scala deleted file mode 100644 index 647cadd..0000000 --- a/src/util/TailCallsUtils.scala +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.util - -import scala.util.control.TailCalls.* - -object TailCallsUtils: - extension [T](iter: Iterator[T]) - def intercalate(value: T): Iterator[T] = - new Iterator[T]: - var prependSep = false - def hasNext: Boolean = iter.hasNext - def next(): T = - if prependSep && iter.hasNext - then - prependSep = false - value - else - prependSep = true - iter.next() - - def traverse(fn: T => TailRec[Unit]): TailRec[Unit] = - def impl: TailRec[Unit] = - if !iter.hasNext - then done(()) - else - for - () <- tailcall(fn(iter.next())) - () <- tailcall(impl) - yield () - - impl diff --git a/src/util/geny.scala b/src/util/geny.scala deleted file mode 100644 index f080c00..0000000 --- a/src/util/geny.scala +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.util - -extension (lhs: geny.Writable) - @scala.annotation.targetName("writableConcat") - def ++(rhs: geny.Writable): geny.Writable = - new geny.Writable: - def writeBytesTo(out: java.io.OutputStream): Unit = - lhs.writeBytesTo(out) - rhs.writeBytesTo(out) diff --git a/src/util/toShortString.scala b/src/util/toShortString.scala deleted file mode 100644 index 680d1ff..0000000 --- a/src/util/toShortString.scala +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.util - -extension (obj: Object) - def toShortString(lineLimit: Int = 10): String = - val lines = obj.toString().linesIterator - val trimmed = - lines.take(lineLimit) - ++ (if lines.hasNext then Iterator.single("...") else Iterator.empty) - trimmed.mkString("\n") diff --git a/src/util/unreachable.scala b/src/util/unreachable.scala deleted file mode 100644 index 4e007ff..0000000 --- a/src/util/unreachable.scala +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.util - -final case class UnreachableError() - extends Error("this code should not be reachable") - -@scala.annotation.targetName("unreachable") -def !!! : Nothing = - throw UnreachableError() diff --git a/src/wf/Shape.scala b/src/wf/Shape.scala deleted file mode 100644 index 7ef77c7..0000000 --- a/src/wf/Shape.scala +++ /dev/null @@ -1,88 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.wf - -import forja.* -import forja.sexpr.lang.{SAtom, SList} - -enum Shape: - case Atom, AnyShape - case Choice(choices: Set[Token | EmbedMeta[?]]) - case Repeat(choice: Shape.Choice, minCount: Int) - case Fields(fields: List[Shape.Choice]) - - def asNode: Node = - this match - case Atom => SAtom("atom") - case AnyShape => SAtom("anyShape") - case Choice(choices) if choices.sizeIs == 1 => - choices.head match - case token: Token => SAtom(token.name) - case embed: EmbedMeta[?] => SAtom(embed.canonicalName) - case Choice(choices) => - val result = SList( - SAtom("choice"), - ) - result.children.addAll: - choices.iterator - .map: - case token: Token => SAtom(token.name) - case embed: EmbedMeta[?] => - SAtom(embed.canonicalName) - result - case Repeat(choice, minCount) => - SList( - SAtom("repeat"), - choice.asNode, - SList( - SAtom("minCount"), - SAtom(minCount.toString()), - ), - ) - case Fields(fields) => - val result = SList( - SAtom("fields"), - ) - result.children.addAll(fields.iterator.map(_.asNode)) - result - -object Shape: - extension (choice: Shape.Choice) - @scala.annotation.targetName("combineChoices") - def |(other: Shape.Choice): Shape.Choice = - Shape.Choice(choice.choices ++ other.choices) - - trait Ops: - def choice(tokens: (Token | EmbedMeta[?])*): Shape.Choice = - Shape.Choice(tokens.toSet) - - def choice(tokens: Set[Token | EmbedMeta[?]]): Shape.Choice = - Shape.Choice(tokens) - - def repeated(choice: Shape.Choice, minCount: Int = 0): Shape.Repeat = - Shape.Repeat(choice, minCount) - - inline def embedded[T: EmbedMeta]: Shape.Choice = - Shape.Choice(Set(summon[EmbedMeta[T]])) - - def fields(fields: Shape.Choice*): Shape.Fields = - Shape.Fields(fields.toList) - - import scala.language.implicitConversions - // TODO: once `into` keyword works, use that - implicit def tokenAsChoice(token: Token): Shape.Choice = - Shape.Choice(Set(token)) - end Ops -end Shape diff --git a/src/wf/Wellformed.scala b/src/wf/Wellformed.scala deleted file mode 100644 index dcf0201..0000000 --- a/src/wf/Wellformed.scala +++ /dev/null @@ -1,598 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.wf - -import cats.syntax.all.given - -import forja.* -import forja.dsl.* -import forja.sexpr.lang.{SAtom, SList} -import forja.source.{Source, SourceRange} -import forja.util.TailCallsUtils.* - -import scala.collection.mutable -import scala.util.control.TailCalls.* - -final class Wellformed private ( - val assigns: Map[Token, Shape], - val topShape: Shape, -): - locally: - val reached = mutable.HashSet.empty[Token] - def assertReachable(tokenOrEmbed: Token | EmbedMeta[?]): TailRec[Unit] = - tokenOrEmbed match - case token: Token => - if !reached(token) - then - reached += token - require( - assigns.contains(token), - s"token $token must be assigned a shape", - ) - tailcall(assertReachableFromShapeOrEmbed(assigns(token))) - else done(()) - case _: EmbedMeta[?] => done(()) - - def assertReachableFromShapeOrEmbed( - shape: Shape | EmbedMeta[?], - ): TailRec[Unit] = - shape match - case _: EmbedMeta[?] => done(()) - case Shape.Atom => done(()) - case Shape.AnyShape => done(()) - case Shape.Choice(choices) => - choices.iterator.traverse(assertReachable) - case Shape.Repeat(choice, _) => - choice.choices.iterator.traverse(assertReachable) - case Shape.Fields(fields) => - fields.iterator.traverse: field => - field.choices.iterator.traverse(assertReachable) - - assertReachableFromShapeOrEmbed(topShape).result - - private lazy val knownMetasByName: Map[SourceRange, EmbedMeta[?]] = - def implForShape(shape: Shape): Iterator[(SourceRange, EmbedMeta[?])] = - shape match - case Shape.Atom | Shape.AnyShape => Iterator.empty - case Shape.Choice(choices) => - choices.iterator.flatMap(implForTokenOrEmbed) - case Shape.Fields(fields) => - fields.iterator.flatMap(implForShape) - case Shape.Repeat(choice, _) => - implForShape(choice) - - def implForTokenOrEmbed( - tokenOrEmbed: Token | EmbedMeta[?], - ): Option[(SourceRange, EmbedMeta[?])] = - tokenOrEmbed match - case token: Token => None - case embed: EmbedMeta[?] => - val name = SourceRange.entire(Source.fromString(embed.canonicalName)) - Some(name -> embed) - - assigns.iterator - .map(_._2) - .flatMap(implForShape) - .toMap - - private lazy val shortNameByToken: Map[Token, SourceRange] = - val acc = mutable.HashMap[List[String], mutable.ListBuffer[Token]]() - acc(Nil) = assigns.iterator.map(_._1).to(mutable.ListBuffer) - acc(Nil) += Node.EmbedT // we use this for serialization to mark embed sites - - def namesToFix: List[List[String]] = - acc.iterator - .collect: - case (name, buf) if buf.size > 1 => name - .toList - - @scala.annotation.tailrec - def impl(): Unit = - val toFix = namesToFix - if toFix.nonEmpty - then - toFix.foreach: nameTooShort => - val toks = acc(nameTooShort) - acc.remove(nameTooShort) - - var didntGrowCount = 0 - toks.foreach: token => - val longerName = token.nameSegments.takeRight(nameTooShort.size + 1) - if longerName == nameTooShort - then - assert( - didntGrowCount == 0, - s"duplicate name $nameTooShort in group ${toks.iterator.map(_.name).mkString(", ")}", - ) - didntGrowCount += 1 - - val buf = acc.getOrElseUpdate(longerName, mutable.ListBuffer.empty) - buf += token - - impl() - - impl() - acc.iterator - .filter: (parts, unitList) => - if unitList.isEmpty - then - assert(parts == Nil) - false - else true - .map: (parts, unitList) => - unitList.head -> SourceRange.entire( - Source.fromString(parts.mkString(".")), - ) - .toMap - - private lazy val tokenByShortName: Map[SourceRange, Token] = - shortNameByToken.map(_.reverse) - - def shapeOf(tokenOrTop: Token | Node.Top.type): Shape = - tokenOrTop match - case token: Token => assigns(token) - case Node.Top => topShape - - /** Manip version of [[this.markErrors]], which gets the current node and - * applies the method to it. - */ - lazy val markErrorsPass: Manip[Unit] = - getNode.tapEffect(markErrors).void - - /** Traverses the entire tree, asserting this Wellformed and replacing - * incorrect nodes with [[forja.Builtin.Error]] nodes. - * - * The generated errors have a [[forja.Builtin.Error.Message]] describing - * what went wrong, and a [[forja.Builtin.Error.AST]] whose subtrees are the - * 0 or more offending nodes. Existing errors (that is, subtrees of a node - * with token [[forja.Builtin.Error]], even if they were not created by this - * method) will be skipped, since they will necessarily contain - * non-conforming trees. - */ - def markErrors(node: Node.All): Unit = - def implShape( - desc: String, - parent: Node.Parent, - shape: Shape, - ): TailRec[Unit] = - shape match - case Shape.AnyShape => done(()) - case Shape.Atom => - implShape(desc, parent, Shape.Fields(Nil)) - case choice @ Shape.Choice(_) => - implShape(desc, parent, Shape.Fields(List(choice))) - case Shape.Fields(fields) => - if parent.children.size != fields.size - then - done: - val wrongSize = parent.children.size - parent.children = List( - Node(Builtin.Error)( - Builtin.Error.Message( - s"$desc should have exactly ${fields.size} children, but it had $wrongSize instead", - ), - Builtin.Error.AST(parent.unparentedChildren), - ), - ) - else - parent.children.iterator - .zip(fields.iterator) - .traverse: (child, choice) => - child match - case node: Node => - if node.token == Builtin.Error - then done(()) - else if choice.choices(node.token) - then tailcall(implForNode(child)) - else - node.replaceThis: - Builtin.Error( - s"in $desc, found token ${node.token}, but expected ${choice.choices.mkString(" or ")}", - child.unparent(), - ) - done(()) - case embed: Node.Embed[?] => - if choice.choices(embed.meta) - then done(()) - else - embed.replaceThis: - Builtin.Error( - s"in $desc, found embed ${embed.meta.canonicalName}, but expected ${choice.choices.mkString(" or ")}", - embed.unparent(), - ) - done(()) - case Shape.Repeat(choice, minCount) => - if parent.children.size < minCount - then - done: - val wrongSize = parent.children.size - parent.children = List( - Node(Builtin.Error)( - Builtin.Error.Message( - s"$desc should have at least $minCount children, but it had $wrongSize instead", - ), - Builtin.Error.AST(parent.unparentedChildren), - ), - ) - else - parent.children.iterator - .traverse: - case node: Node => - if node.token == Builtin.Error - then done(()) - else if choice.choices(node.token) - then tailcall(implForNode(node)) - else - node.replaceThis: - Builtin.Error( - s"in $desc, found token ${node.token}, but expected ${choice.choices.mkString(" or ")}", - node.unparent(), - ) - done(()) - case _: Node.Embed[?] => ??? - - def implForNode(node: Node.All): TailRec[Unit] = - node match - case top: Node.Top => - tailcall(implShape("top", top, topShape)) - case node: Node => - assert(assigns.contains(node.token)) - tailcall(implShape(node.token.name, node, assigns(node.token))) - case embed: Node.Embed[?] => ??? - - implForNode(node).result - - /** Derives a new Wellformed based on this one via the mutations defined in - * fn. - * - * Creates a new [[forja.wf.Wellformed.Builder]] pre-filled with the same - * definitions as the existing one. The transformations in fn are applied to - * it, adding/removing/replacing definitions. With those changes made, a new - * immutable Wellformed instance is constructed and returned. - * - * Use this method in any case where you want "something like that but with a - * few changes", rather than a completely new set of definitions. - */ - def makeDerived(fn: Wellformed.Builder ?=> Unit): Wellformed = - val builder = - Wellformed.Builder(mutable.HashMap.from(assigns), Some(topShape)) - fn(using builder) - builder.build() - - def serializeTree(tree: Node.All): tree.This = - val src = SourceRange.entire(Source.fromString(":src")) - val txt = SourceRange.entire(Source.fromString(":txt")) - - extension [P <: Node.Parent](parent: P) - def addSerializedChildren( - children: IterableOnce[Node.Child], - ): TailRec[P] = - children.iterator - .traverse: child => - tailcall(impl(child)).map(parent.children.addOne) - .map(_ => parent) - - extension (token: Token) - def shortName = - shortNameByToken.getOrElse( - token, - SourceRange.entire(Source.fromString(token.name)), - ) - - def impl(tree: Node.All): TailRec[tree.This] = - val result: TailRec[Node.All] = - tree match - case top: Node.Top => - val result = Node.Top() - result.addSerializedChildren(top.children) - case nd @ Node(token) if !token.showSource => - done(SAtom(token.shortName)) - case node: Node => - val result = SList(SAtom(node.token.shortName)) - if node.token.showSource - then - val srcRange = node.sourceRange - result.children.addOne: - SList( - SAtom(txt), - SAtom(srcRange), - ) - srcRange.source.origin match - case None => - case Some(origin) => - result.children.addOne: - SList( - SAtom(src), - SAtom(origin.toString), - SAtom(srcRange.offset.toString()), - SAtom(srcRange.length.toString()), - ) - - result.addSerializedChildren(node.children) - case embed: Node.Embed[?] => - val bytes = - Source.fromWritable(embed.meta.serialize(embed.value)) - done: - SList( - SAtom(shortNameByToken(Node.EmbedT)), - SAtom(embed.meta.canonicalName), - SAtom(SourceRange.entire(bytes)), - ) - - result.asInstanceOf[TailRec[tree.This]] - - impl(tree).result - end serializeTree - - def deserializeTree(tree: Node.All): Node.All = - val src = SourceRange.entire(Source.fromString(":src")) - val txt = SourceRange.entire(Source.fromString(":txt")) - val srcMap = mutable.HashMap.empty[os.Path, Source] - - lazy val nodeManip: Manip[TailRec[Node.All]] = - on( - tok(SAtom) - .map(_.sourceRange) - .restrict(tokenByShortName) - <* refine(atFirstChild(on(atEnd).check)), - ).value.map: token => - done(token()) - | on( - tok(SList).withChildren: - skip(tok(SAtom).src(shortNameByToken(Node.EmbedT))) - ~ field(tok(SAtom).map(_.sourceRange).restrict(knownMetasByName)) - ~ field(tok(SAtom).map(_.sourceRange)) - ~ eof, - ).value.map: (meta, src) => - done(Node.Embed(meta.deserialize(src))(using meta)) - | on( - anyNode - *: tok(SList).withChildren: - field: - tok(SAtom) - .map(_.sourceRange) - .restrict(tokenByShortName) - ~ field: - optional: - tok(SList).withChildren: - skip(tok(SAtom).src(txt)) - ~ field(tok(SAtom).map(_.sourceRange)) - ~ eof - ~ field: - optional: - tok(SList).withChildren: - skip(tok(SAtom).src(src)) - ~ field(tok(SAtom)) - ~ field(tok(SAtom)) - ~ field(tok(SAtom)) - ~ eof - ~ trailing, - ).value.map: (node, token, txtOpt, srcOpt) => - val result = token() - srcOpt match - case None => - case Some((path, offset, len)) => - val pathVal = os.Path(path.sourceRange.decodeString()) - val offsetVal = offset.sourceRange.decodeString().toInt - val lenVal = len.sourceRange.decodeString().toInt - val loc = - srcMap.getOrElseUpdate(pathVal, Source.mapFromFile(pathVal)) - result.at(SourceRange(loc, offsetVal, lenVal)) - txtOpt match - case None => - case Some(text) if srcOpt.nonEmpty => - assert(text == result.sourceRange) - case Some(text) => - result.at(text) - - val skipCount = - 1 + (if srcOpt.nonEmpty then 1 else 0) + (if txtOpt.nonEmpty then 1 - else 0) - tailcall: - result - .addDeserializedChildren(node.children.iterator.drop(skipCount)) - .map(_ => result) - | on( - anyNode, - ).value.map: badNode => - done( - Builtin.Error( - "could not parse node", - badNode.clone(), - ), - ) - - extension [P <: Node.Parent](parent: P) - def addDeserializedChildren( - children: IterableOnce[Node.Child], - ): TailRec[P] = - children.iterator - .traverse: child => - tailcall(impl(child)).map(parent.children.addOne) - .map(_ => parent) - - def impl[N <: Node.All](tree: N): TailRec[N] = - (tree: @unchecked) match - case top: Node.Top => - val result: Node.Top = Node.Top() - result - .addDeserializedChildren(top.children) - .map(_ => result.asInstanceOf[N]) - case node: Node => - initNode(node)(nodeManip) - .perform() - .asInstanceOf[TailRec[N]] - - impl(tree).result - end deserializeTree - - def asNode: Node = - val result = SList( - SAtom("wellformed"), - SList( - SAtom("top"), - topShape.asNode, - ), - ) - result.children.addAll: - assigns.toArray - .sortInPlaceBy(_._1.name) - .iterator - .map: (tok, shape) => - SList( - SAtom(tok.name), - shape.asNode, - ) - result - - override def toString(): String = - sexpr.serialize.toPrettyString(Node.Top(asNode)) -end Wellformed - -object Wellformed: - val empty: Wellformed = Wellformed: - Node.Top ::= Shape.AnyShape - - def apply(fn: Builder ?=> Unit): Wellformed = - val builder = Builder(mutable.HashMap.empty, None) - fn(using builder) - builder.build() - - final class Builder private[forja] ( - assigns: mutable.HashMap[Token, Shape], - private var topShapeOpt: Option[Shape], - ): - extension (token: Token | Node.Top.type) - def undef(): Unit = - token match - case token: Token => - require( - assigns.contains(token), - s"cannot undef undefined token $token", - ) - assigns.remove(token) - case Node.Top => - require(topShapeOpt.nonEmpty, s"top is undefined, cannot undef") - topShapeOpt = None - - def ::=(shape: Shape): Unit = - token match - case token: Token => - require(!assigns.contains(token), s"$token already has a shape") - assigns(token) = shape - case Node.Top => - require(topShapeOpt.isEmpty, s"top already has a shape") - topShapeOpt = Some(shape) - - @scala.annotation.targetName("resetTopShape") - def ::=!(shape: Shape): Unit = - token match - case token: Token => - require(assigns.contains(token), s"$token has no shape to replace") - assigns(token) = shape - case Node.Top => - require(topShapeOpt.nonEmpty, s"top has no shape to replace") - topShapeOpt = Some(shape) - - def existingShape: Shape = - token match - case token: Token => - require(assigns.contains(token)) - assigns(token) - case Node.Top => - require(topShapeOpt.nonEmpty) - topShapeOpt.get - - def existingCases: Set[Token | EmbedMeta[?]] = - existingShape match - case Shape.Choice(choices) => choices - case Shape.Repeat(choices, _) => choices.choices - case shape: (Shape.Fields | Shape.Atom.type | Shape.AnyShape.type) => - throw IllegalArgumentException( - s"$token's shape doesn't have cases ($shape)", - ) - - def removeCases(cases: (Token | EmbedMeta[?])*): Unit = - existingShape match - case Shape.Choice(choices) => - token ::=! Shape.Choice(choices -- cases) - case Shape.Repeat(choices, minCount) => - token ::=! Shape.Repeat( - Shape.Choice(choices.choices -- cases), - minCount, - ) - case Shape.Fields(fields) => - token ::=! Shape.Fields( - fields.map(choice => Shape.Choice(choice.choices -- cases)), - ) - case Shape.AnyShape | Shape.Atom => - /* maybe it helps to assert something here, but it is technically - * correct to just do nothing */ - - def addCases(cases: (Token | EmbedMeta[?])*): Unit = - existingShape match - case Shape.Choice(choices) => - token ::=! Shape.Choice(choices ++ cases) - case Shape.Repeat(choice, minCount) => - token ::=! Shape.Repeat( - Shape.Choice(choice.choices ++ cases), - minCount, - ) - case shape: (Shape.Fields | Shape.Atom.type | Shape.AnyShape.type) => - throw IllegalArgumentException( - s"$token's shape is not appropriate for adding cases ($shape)", - ) - - def deleteShape(): Unit = - token match - case Node.Top => - require(topShapeOpt.nonEmpty) - topShapeOpt = None - case token: Token => - require(assigns.contains(token)) - assigns.remove(token) - - def importFrom(wf2: Wellformed): Unit = - def fillFromShape(shape: Shape): Unit = - shape match - case Shape.Atom => - case Shape.AnyShape => - case Shape.Choice(choices) => - choices.foreach(fillFromTokenOrEmbed) - case Shape.Repeat(choice, _) => - choice.choices.foreach(fillFromTokenOrEmbed) - case Shape.Fields(fields) => - fields.foreach(_.choices.foreach(fillFromTokenOrEmbed)) - - def fillFromTokenOrEmbed(tokenOrEmbed: Token | EmbedMeta[?]): Unit = - tokenOrEmbed match - case token: Token => - if !assigns.contains(token) - then - assigns(token) = wf2.assigns(token) - fillFromShape(wf2.assigns(token)) - case _: EmbedMeta[?] => - - token match - case Node.Top => - topShapeOpt = Some(wf2.topShape) - fillFromShape(wf2.topShape) - case token: Token => fillFromTokenOrEmbed(token) - - private[forja] def build(): Wellformed = - require(topShapeOpt.nonEmpty) - new Wellformed(assigns.toMap, topShape = topShapeOpt.get) - end Builder -end Wellformed diff --git a/src/wf/Wellformed.test.scala b/src/wf/Wellformed.test.scala deleted file mode 100644 index 4ed701e..0000000 --- a/src/wf/Wellformed.test.scala +++ /dev/null @@ -1,176 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.wf - -import forja.dsl.* -import forja.sexpr.lang.{SAtom, SList} -import forja.source.{Source, SourceRange} -import forja.test.newlineUtils.ensureLf - -class WellformedTests extends munit.FunSuite: - import WellformedTests.* - val wf = Wellformed: - Node.Top ::= AnyShape - tok1 ::= AnyShape - tok2 ::= AnyShape - - // dummy to make sure the wf knows how to embed ints - tok3 ::= embedded[Int] - - val tmpSrc = Source.mapFromFile(os.temp("foo")) - val src1 = SourceRange.entire(tmpSrc).take(2) - val src2 = SourceRange.entire(tmpSrc).drop(2) - val src3 = SourceRange.entire(Source.fromString("bar")) - - def joinN( - breadth: Int, - iterFn: () => Iterator[Node.Child], - ): Iterator[List[Node.Child]] = - breadth match - case 0 => Iterator.single(Nil) - case breadth => - for - hd <- iterFn() - tl <- joinN(breadth - 1, iterFn) - yield hd.clone() :: tl - - def exampleNodes(depth: Int): Iterator[Node.Child] = - depth match - case 0 => Iterator.empty - case _ if depth > 0 => - for - breadth <- (0 until 3).iterator - parent <- - Iterator(tok1(src1), tok2(src2), tok1(src3), tok1()) - ++ locally: - if breadth == 0 - then Iterator.single(Node.Embed(42)) - else Iterator.empty - children <- joinN(breadth, () => exampleNodes(depth - 1)) - yield locally: - val p = parent.clone() - if breadth != 0 - then p.asParent.children.addAll(children) - p - - def examples(depth: Int): Iterator[Node.Top] = - for - breadth <- (0 until 3).iterator - children <- joinN(breadth, () => exampleNodes(depth - 1)) - yield Node.Top(children) - - test("serialization back and forth"): - examples(3).foreach: tree => - val orig = tree.clone() - val ser = wf.serializeTree(tree) - if orig.children.nonEmpty - then assertNotEquals(ser, orig) - - val deser = wf.deserializeTree(ser) - assertEquals(deser, orig) - - def expectNoErrors(using - munit.Location, - )(wf: Wellformed)( - ast: Node.Top, - ): Unit = - wf.markErrors(ast) - if ast.hasErrors - then fail(s"unexpected errors: ${ast.toPrettyString(wf)}") - - def expectErrors(using munit.Location)(wf: Wellformed)(ast: Node.Top): Unit = - wf.markErrors(ast) - if !ast.hasErrors - then fail(s"should have had errors: ${ast.toPrettyString(wf)}") - - val wf1 = Wellformed: - Node.Top ::= tok2 - tok2 ::= fields(tok3, tok3) - tok3 ::= Atom - - test("wf1 asNode"): - assertEquals( - wf1.asNode, - SList( - SAtom("wellformed"), - SList( - SAtom("top"), - SAtom("forja.wf.WellformedTests.tok2"), - ), - SList( - SAtom("forja.wf.WellformedTests.tok2"), - SList( - SAtom("fields"), - SAtom("forja.wf.WellformedTests.tok3"), - SAtom("forja.wf.WellformedTests.tok3"), - ), - ), - SList( - SAtom("forja.wf.WellformedTests.tok3"), - SAtom("atom"), - ), - ), - ) - - test("wf1 toString"): - assertEquals( - wf1.toString(), - """(wellformed - | (top - | forja.wf.WellformedTests.tok2) - | (forja.wf.WellformedTests.tok2 - | (fields - | forja.wf.WellformedTests.tok3 - | forja.wf.WellformedTests.tok3)) - | (forja.wf.WellformedTests.tok3 - | atom))""".stripMargin.ensureLf, - ) - - test("fields: correct"): - expectNoErrors(wf1): - Node.Top( - tok2( - tok3(), - tok3(), - ), - ) - - test("fields: no fields"): - expectErrors(wf1): - Node.Top(tok2()) - test("fields: wrong tok"): - expectErrors(wf1): - Node.Top(tok3()) - test("fields: swapped colors"): - expectErrors(wf1): - Node.Top( - tok3( - tok2(), - tok2(), - ), - ) - test("fields: one is not an atom"): - expectErrors(wf1): - Node.Top( - tok2( - tok3(tok3()), - tok3(), - ), - ) - -object WellformedTests: - object tok1 extends Token.ShowSource - object tok2 extends Token - object tok3 extends Token diff --git a/src/wf/WellformedDef.scala b/src/wf/WellformedDef.scala deleted file mode 100644 index 66dbd73..0000000 --- a/src/wf/WellformedDef.scala +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright 2024-2025 Forja Team -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package forja.wf - -import forja.dsl.* -import forja.util.Named - -import scala.collection.mutable -import scala.util.control.NonFatal - -trait WellformedDef: - def topShape: Shape - - abstract class t protected (shapeFn: => Shape)(using Named.OwnName) - extends Token: - private[WellformedDef] lazy val shape = shapeFn - - protected def forceDef(tok: t): Unit = - tok ::= tok.shape - - protected given builder: Wellformed.Builder = - Wellformed.Builder(mutable.HashMap.empty, None) - - final lazy val wf: Wellformed = - val visited = mutable.HashSet[Token]() - // If .wf crashes (throw during lazy val init), make a best effort to reset - /* mutable state. That is, if you see it crash repeatedly, all the stack - * traces */ - // show the same (hopefully actual) problem, not just the effect of - // running part of this procedure twice, which will almost definitely - // crash due to redefinitions. - try - Node.Top ::= topShape - - def implTok(tok: Token): Unit = - if !visited(tok) - then - tok match - case tok: t => - tok ::= tok.shape - // add after ::= for exception safety (see catch below) - visited += tok - implShape(tok.shape) - case tok => - visited += tok - implShape(tok.existingShape) - - def implShape(shape: Shape): Unit = - shape match - case Shape.Atom => - case Shape.AnyShape => - case Shape.Choice(choices) => - choices.foreach: - case tok: Token => implTok(tok) - case _ => - case Shape.Repeat(choice, minCount) => - implShape(choice) - case Shape.Fields(fields) => - fields.foreach(implShape) - - implShape(topShape) - - builder.build() - catch - case NonFatal(ex) => - Node.Top.undef() - visited.foreach: - case tok: t => - tok.undef() - case _ => // skip - throw ex - end wf -end WellformedDef